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.

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.
Di episode 11 kita memakai SimpleJWT dengan access (15 menit) dan refresh (7 hari). Praktik lanjutan yang membedakan produksi dari prototype:
pip install djangorestframework-simplejwt
python manage.py startapp token_blacklist # atau add app bawaanSebenarnya SimpleJWT menyediakan rest_framework_simplejwt.token_blacklist sebagai app:
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,
}python manage.py migrateROTATE_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.
Ketika aplikasi pihak ketiga butuh akses tanpa password user (misal "Login dengan devblog"), gunakan OAuth2 authorization code. django-oauth-toolkit (DOT) menyediakan server OAuth2:
pip install django-oauth-toolkitINSTALLED_APPS = [
...
"oauth2_provider",
]
OAUTH2_PROVIDER = {
"OAUTH2_BACKEND_CLASS": "oauth2_provider.oauth2_backends.JSONOAuth2CoreBackend",
}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:
pip install django-allauthINSTALLED_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.
Lapisan kedua: TOTP 2FA (Time-based One-Time Password). Library django-otp + django-otp-webauthn/otp_totp:
pip install django-otpINSTALLED_APPS = [
...
"django_otp",
"django_otp.plugins.otp_totp",
]
MIDDLEWARE = [
...
"django_otp.middleware.OTPMiddleware",
]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.
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:
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_authenticatedfrom 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.
Rangkuman arsitektur auth untuk SPA production:
HttpOnly cookie terpisah untuk rotasi yang transparan.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.
Inti yang harus dibawa pulang:
ROTATE_REFRESH_TOKENS + BLACKLIST_AFTER_ROTATION mematikan token bekas.django-oauth-toolkit) untuk aplikasi pihak ketiga; allauth untuk social login.django-otp) menambah lapisan "something you have"; sediakan recovery codes.IsOwnerOrReadOnly) + filter get_queryset = defense in depth.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!