Belajar Django - Auth Lanjutan & Permissions
Episode 20 of 27

Belajar Django - Auth Lanjutan & Permissions

Menyempurnakan autentikasi untuk API SPA: JWT refresh & token rotation, integrasi OAuth2 dengan django-oauth-toolkit dan social auth, MFA/2FA dengan otp, serta object-level permission agar user hanya bisa mengakses data miliknya.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Episode 10 memberi autentikasi dasar, episode 19 menambahkan throttling. Tapi API SPA modern menuntut lebih: token yang aman di banyak perangkat, login sosial, lapisan kedua (2FA), dan kontrol akses per-objek. Di episode ini kita menyelesaikan semuanya — Auth Lanjutan & Permissions.

Mengapa topik ini penting? Karena ancaman modern menyerang token (bukan password lagi): JWT bocor, session permanen, atau permission model-level yang terlalu longgar. Auth yang production-grade adalah kombinasi beberapa lapis — refresh rotation, MFA, dan object-level checks — bukan satu fitur tunggal.

JWT Refresh & Token Rotation

Di episode 11 kita memakai SimpleJWT dengan access (15 menit) dan refresh (7 hari). Praktik lanjutan yang membedakan produksi dari prototype:

  1. Refresh rotation — setiap kali refresh dipakai, terbitkan refresh baru dan invalidasi yang lama. Token bocor jadi tidak berguna setelah sekali pakai.
  2. Blacklist — refresh token yang di-revoke masuk daftar hitam, mencegah replay.
  3. Refresh yang di-deteksi — jika refresh token lama dipakai lagi setelah rotasi, anggap bocor dan cabut semua session user.
Install blacklist app
pip install djangorestframework-simplejwt
python manage.py startapp token_blacklist  # atau add app bawaan

Sebenarnya SimpleJWT menyediakan rest_framework_simplejwt.token_blacklist sebagai app:

Pythonsettings.py - rotasi & blacklist
INSTALLED_APPS = [
    ...
    "rest_framework_simplejwt.token_blacklist",
]
 
SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
    "UPDATE_LAST_LOGIN": True,
}
Jalankan migrasi blacklist
python manage.py migrate

ROTATE_REFRESH_TOKENS: True menerbitkan refresh baru saat /api/token/refresh/ dipanggil; BLACKLIST_AFTER_ROTATION: True mem-blacklist refresh lama. UPDATE_LAST_LOGIN: True mencatat aktivitas login (berguna untuk audit dan deteksi anomali).

Tip

Rotasi refresh mengubah model mental "satu token untuk semua perangkat": sekarang setiap login punya rantai refresh sendiri. Karena itu client harus menyimpan refresh terbaru setelah setiap panggilan refresh — client lama yang terus memakai token usang akan di-blacklist dan ter-logout. Dokumentasikan kontrak ini untuk tim frontend.

OAuth2 dengan django-oauth-toolkit

Ketika aplikasi pihak ketiga butuh akses tanpa password user (misal "Login dengan devblog"), gunakan OAuth2 authorization code. django-oauth-toolkit (DOT) menyediakan server OAuth2:

Install OAuth toolkit
pip install django-oauth-toolkit
Pythonsettings.py - DOT
INSTALLED_APPS = [
    ...
    "oauth2_provider",
]
 
OAUTH2_PROVIDER = {
    "OAUTH2_BACKEND_CLASS": "oauth2_provider.oauth2_backends.JSONOAuth2CoreBackend",
}
Pythondevblog/urls.py - route OAuth
from django.urls import include, path
 
urlpatterns = [
    path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
]

Alurnya: client meminta authorization code → user login & menyetujui → client menukar code dengan access token → client memanggil API dengan Authorization: Bearer. DRF mengintegrasikan DOT via OAuth2Authentication. Alur lengkap (client registration, scopes, redirect URI) kita praktikkan di project — intinya: password user tidak pernah jatuh ke pihak ketiga.

Untuk social login (Google, GitHub), django-allauth adalah pilihan standar:

Install allauth
pip install django-allauth
Pythonsettings.py - allauth
INSTALLED_APPS = [
    ...
    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "allauth.socialaccount.providers.google",
]
 
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
]

allauth menangani redirect OAuth, pembuatan user, dan penautan email — integrasi provider Google/GitHub tinggal konfigurasi di admin.

MFA / 2FA dengan otp

Lapisan kedua: TOTP 2FA (Time-based One-Time Password). Library django-otp + django-otp-webauthn/otp_totp:

Install django-otp
pip install django-otp
Pythonsettings.py - django-otp
INSTALLED_APPS = [
    ...
    "django_otp",
    "django_otp.plugins.otp_totp",
]
 
MIDDLEWARE = [
    ...
    "django_otp.middleware.OTPMiddleware",
]
Migrasi dan verifikasi
python manage.py migrate
python manage.py shell -c "
from django_otp.plugins.otp_totp.models import TOTPDevice
from django.contrib.auth import get_user_model
u = get_user_model().objects.get(username='admin')
d = TOTPDevice.objects.create(user=u, confirmed=False)
print(d.config_url)   # otpauth://totp/...  -> scan QR
"

Alur: user login password → cek TOTPDevice → verifikasi 6 digit → baru session penuh. Kombinasi password (something you know) + TOTP (something you have) membuat akun jauh lebih sulit dibobol meskipun password bocor. Enforce 2FA untuk staf admin — mereka pintu gerbang nilai tertinggi.

Warning

Jangan pernah mewajibkan 2FA tanpa jalur pemulihan: recovery codes, backup device, atau alur admin. User yang kehilangan authenticator tanpa recovery code = akun terkunci permanen. Sedikit effort di desain pemulihan menghemat dukungan teknis yang mahal nantinya.

Object-Level Permission

Permission model-level (episode 10) memeriksa "boleh ubah post apa pun" — tapi biasanya user hanya boleh mengubah post miliknya. Object-level permission di DRF via get_permissions dan query scope:

Pythonblog/permissions.py
from rest_framework import permissions
 
class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.author == request.user
 
    def has_permission(self, request, view):
        if request.method in permissions.SAFE_METHODS:
            return True
        return request.user.is_authenticated
Pythonblog/views.py - terapkan object permission
from rest_framework import viewsets
 
from .permissions import IsOwnerOrReadOnly
from .models import Post
from .serializers import PostSerializer
 
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsOwnerOrReadOnly]
 
    def get_queryset(self):
        user = self.request.user
        if user.is_staff:
            return Post.objects.all()
        return Post.objects.filter(author=user)
 
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

IsOwnerOrReadOnly membolehkan baca publik, tetapi write hanya oleh pemilik objek. Dua lapis sekaligus: get_queryset memfilter level list (data yang dilihat), IsOwnerOrReadOnly.has_object_permission memeriksa level objek (detail/update/delete). Ini prinsip defense in depth — jangan pernah andalkan satu lapis saja.

API yang Aman untuk SPA

Rangkuman arsitektur auth untuk SPA production:

100%
  • Access token pendek di memori (bukan localStorage) — minimal window bocor.
  • Refresh token di HttpOnly cookie terpisah untuk rotasi yang transparan.
  • Throttling di endpoint login/refresh (episode 19).
  • 2FA untuk akun bernilai tinggi; OAuth2/allauth untuk login sosial.

Note

Menyimpan JWT di localStorage membuat token bisa dibaca JavaScript — satu XSS (episode 18) = token bocor. Arsitektur yang lebih aman menaruh access di memori dan refresh di cookie HttpOnly, sehingga JavaScript tidak pernah melihat token rahasia. Trade-off: refresh lewat cookie butuh CSRF handling khusus — bandingkan dengan kebutuhan aplikasi kalian.

Penutup

Inti yang harus dibawa pulang:

  • JWT lanjutan: ROTATE_REFRESH_TOKENS + BLACKLIST_AFTER_ROTATION mematikan token bekas.
  • OAuth2 (django-oauth-toolkit) untuk aplikasi pihak ketiga; allauth untuk social login.
  • 2FA TOTP (django-otp) menambah lapisan "something you have"; sediakan recovery codes.
  • Object-level permission (IsOwnerOrReadOnly) + filter get_queryset = defense in depth.
  • Access token di memori, refresh di HttpOnly cookie — hindari localStorage.

Di episode 21 selanjutnya kita masuk dunia realtime: Channels & WebSockets — beralih ke ASGI, menulis consumers dan routing, channel layers dengan Redis, serta membangun chat/notifikasi realtime di blog devblog. Sampai jumpa di episode 21!