Lompat ke konten
dokumentasi.id
Commit 32b2830
v2
Star0

2.2

Autentikasi, 2FA, dan Izin

Setiap endpoint REST melewati satu autentikasi dan satu permission class bawaan. REST_FRAMEWORK mengisi DEFAULT_AUTHENTICATION_CLASSES dengan inti.autentikasi.AutentikasiSesi dan DEFAULT_PERMISSION_CLASSES dengan inti.izin.PunyaIzin, jadi view yang tidak menulis permission_classes sendiri tetap diperiksa dengan aturan yang sama. AutentikasiSesi mengembalikan header Session supaya request tanpa sesi berakhir 401, bukan 403 backend/config/settings.py:147-159, backend/inti/autentikasi.py:4-8.

Alur login lewat allauth headless

Django memasang allauth, allauth.account, allauth.mfa, dan allauth.headless di INSTALLED_APPS, lalu config.urls menyambungkan prefiks api/_allauth/ ke allauth.headless.urls backend/config/settings.py:26-45, backend/config/urls.py:38-47. Login hanya menerima username, pendaftaran mandiri ditutup, verifikasi email dimatikan, klien headless dibatasi ke browser, dan tipe MFA yang didukung hanya totp serta recovery_codes backend/config/settings.py:114-124. AdapterAkun.is_open_for_signup selalu mengembalikan False, sehingga akun hanya lahir lewat PenggunaViewSet backend/akun/adapter.py:4-6.

Frontend mengirim request login lewat panggilAuth, yang memakai basis /api/_allauth/browser/v1 dan menyertakan X-CSRFToken frontend/src/lib/api.ts:82-107. Helper api dipakai untuk endpoint bisnis dan membaca token dari cookie csrftoken frontend/src/lib/api.ts:14-17, frontend/src/lib/api.ts:31-58.

sequenceDiagram
    autonumber
    participant B as Browser
    participant A as allauth headless
    participant S as SesiView
    B->>A: POST /auth/login dengan username dan password
    A-->>B: 401 dengan flows mfa_authenticate is_pending
    B->>A: POST /auth/2fa/authenticate dengan kode TOTP
    A-->>B: 200 dan cookie sesi
    B->>S: GET /api/auth/sesi/
    S-->>B: terautentikasi true dan daftar izin
    B->>B: invalidateQueries sesi lalu redirect ke /
%% fig: Login dua tahap lewat allauth headless
%% desc: Halaman login memanggil endpoint allauth headless, melewati tahap kode TOTP bila diminta, lalu membaca status sesi dari SesiView.
Fig. 1: Login dua tahap lewat allauth headlessHalaman login memanggil endpoint allauth headless, melewati tahap kode TOTP bila diminta, lalu membaca status sesi dari SesiView.

HalamanLogin memperlakukan balasan 200 atau 409 sebagai login selesai. Balasan 401 yang memuat flow mfa_authenticate dengan is_pending memunculkan medan kode, lalu kode dikirim ke /auth/2fa/authenticate frontend/src/app/login/page.tsx:27-31, frontend/src/app/login/page.tsx:55-90.

Percobaan login dibatasi axes: AXES_FAILURE_LIMIT = 5, AXES_COOLOFF_TIME = 0.5, kunci hitungan per ["username", "ip_address"], dan hitungan direset saat login berhasil backend/config/settings.py:125-131. Alamat IP diambil ip_klien dari HTTP_X_REAL_IP karena Nginx menimpanya, dan nama pengguna diambil username_axes backend/akun/adapter.py:9-15. Halaman login memetakan status 403 dan 429 ke pesan "Terlalu banyak percobaan. Coba lagi nanti." frontend/src/app/login/page.tsx:55-90.

Sesi disimpan di cookie HttpOnly dengan SameSite=Lax, umur 12 jam, dan Secure aktif saat DEBUG mati backend/config/settings.py:133-138.

Pemeriksaan sesi di /api/auth/sesi/

SesiView adalah satu-satunya endpoint status sesi. Kelas ini memakai permission_classes = [AllowAny] dan ensure_csrf_cookie pada dispatch, sehingga request anonim tetap mendapat cookie CSRF dan balasan {"terautentikasi": False, "pengguna": None} backend/akun/views.py:33-44. Saat sesi ada, SesiPenggunaSerializer mengirim id, username, nama, peran, peran_nama, izin, wajib_2fa, dan punya_2fa backend/akun/serializers.py:11-27.

Di frontend, useSesi menyimpan balasan itu di query ["sesi"] dengan staleTime 60_000 dan mengekspos punya(izin) untuk mengecek satu izin di komponen frontend/src/lib/kait.ts:51-63.

EndpointPenanganCatatan
POST /api/_allauth/browser/v1/auth/loginallauth headlessmetode username saja
POST /api/_allauth/browser/v1/auth/2fa/authenticateallauth headlessmenerima kode TOTP
DELETE /api/_allauth/browser/v1/auth/sessionallauth headlesslogout, dipanggil keluar di Kerangka
GET /api/auth/sesi/SesiViewAllowAny, sumber useSesi
POST /api/pengguna/{id}/atur-password/PenggunaViewSet.atur_passwordizin kelola_pengguna
POST /api/pengguna/{id}/reset-2fa/PenggunaViewSet.reset_2faizin kelola_pengguna

Kewajiban TOTP bagi peran pemilik

Pengguna.izin membaca IZIN_PERAN berdasarkan peran_id; punya_2fa menanyakan Authenticator bertipe TOTP; wajib_2fa hanya benar ketika peran_id == PEMILIK; dan belum_aktifkan_2fa menggabungkan keduanya backend/akun/models.py:20-42.

graph LR
    Login["login berhasil"] --> Cek["Pengguna.belum_aktifkan_2fa"]
    Cek -->|"wajib_2fa benar"| Tolak["Wajib2FA kode wajib_2fa"]
    Cek -->|"wajib_2fa salah"| Lanjut["izin_untuk lalu PunyaIzin"]
    Tolak --> Keamanan["Kerangka memaksa rute /keamanan/"]
    Keamanan --> Pasang["POST /account/authenticators/totp"]
    Pasang --> Cek
    style Login fill:#2d7d46,color:#fff
    style Lanjut fill:#2d7d46,color:#fff
    style Pasang fill:#2d7d46,color:#fff
    style Cek fill:#f59e0b,color:#000
    style Keamanan fill:#f59e0b,color:#000
    style Tolak fill:#dc2626,color:#fff
%% fig: Gerbang 2FA pada PunyaIzin
%% desc: Peran pemilik tanpa Authenticator TOTP ditolak dengan kode wajib_2fa, diarahkan ke halaman keamanan, lalu lolos setelah TOTP terpasang.
Fig. 2: Gerbang 2FA pada PunyaIzinPeran pemilik tanpa Authenticator TOTP ditolak dengan kode wajib_2fa, diarahkan ke halaman keamanan, lalu lolos setelah TOTP terpasang.

PunyaIzin.has_permission mengembalikan False untuk pengguna anonim, lalu melempar Wajib2FA bila belum_aktifkan_2fa benar dan view tidak menyetel lewati_2fa backend/inti/izin.py:52-64. Wajib2FA adalah turunan PermissionDenied dengan default_code = "wajib_2fa" dan detail "Aktifkan 2FA terlebih dahulu untuk melanjutkan." backend/inti/izin.py:38-40. penangan_galat menyeragamkan setiap APIException menjadi {"kode", "pesan", "rincian"}, jadi klien menerima kode bernilai wajib_2fa pada status 403 backend/inti/galat.py:21-33.

Frontend memakai kode itu di dua tempat. minta mengalihkan halaman ke /keamanan/ saat data.kode === "wajib_2fa" dan ke /login/ saat status 401 frontend/src/lib/api.ts:31-58. Kerangka menghitung perlu2fa sebelum render, memaksa rute /keamanan/ selama TOTP belum ada, dan fungsi keluar mengirim DELETE /auth/session frontend/src/components/kerangka.tsx:126-145.

Aktivasi TOTP berjalan di halaman keamanan dengan empat panggilan berurutan: GET /account/authenticators/totp untuk secret dan totp_url, POST /auth/reauthenticate untuk memverifikasi password, POST /account/authenticators/totp untuk kode 6 digit, lalu GET /account/authenticators/recovery-codes untuk kode pemulihan frontend/src/app/(app)/keamanan/page.tsx:20-56. Penerbit TOTP disetel MFA_TOTP_ISSUER = "Hydro" backend/config/settings.py:114-124.

Pemilik yang kehilangan perangkat dipulihkan oleh pemilik lain. reset_2fa menghapus seluruh Authenticator milik pengguna sasaran dan membalas 204 backend/akun/views.py:71-76.

⚠️ reset_2fa menghapus semua authenticator pengguna itu, bukan hanya TOTP. Pemilik tersebut langsung kehilangan syarat punya_2fa dan kembali dialihkan ke /keamanan/ pada request berikutnya.

Uji regresi test_pemilik_tanpa_2fa_diarahkan_mengaktifkan memastikan pemilik tanpa TOTP menerima 403 berkode wajib_2fa, sementara /api/auth/sesi/ tetap melaporkan punya_2fa bernilai False backend/tests/test_izin.py:139-149.

Deklarasi izin per view

inti/izin.py mendefinisikan delapan izin, penanda HANYA_LOGIN untuk endpoint yang cukup login, tiga kode peran, dan peta IZIN_PERAN backend/inti/izin.py:4-35. izin_untuk mengambil atribut izin pada view: bila nilainya dict, kuncinya adalah action atau request.method.lower(); bila tidak ada deklarasi, hasilnya None backend/inti/izin.py:43-49. PunyaIzin menerjemahkan hasil itu menjadi True hanya ketika nilai izin sama dengan HANYA_LOGIN atau ada di pengguna.izin backend/inti/izin.py:52-64.

peran_idIsi IZIN_PERAN
admin_penjualanlihat_produk, kelola_transaksi
staf_gudanglihat_produk, kelola_stok
pemilikseluruh 8 izin di SEMUA_IZIN
ViewBentuk izinNilai
PenggunaViewSetstringkelola_pengguna
PeranViewSetstringkelola_pengguna
LogAktivitasViewSetstringkelola_pengguna
TransaksiViewSetdict per actionkelola_transaksi, setujui_pembatalan
DasborViewstringlihat_produk

PenggunaViewSet menyetel izin = KELOLA_PENGGUNA untuk semua action, termasuk atur_password dan reset_2fa; PeranViewSet dan LogAktivitasViewSet memakai nilai yang sama backend/akun/views.py:56-76, backend/akun/views.py:79-88. TransaksiViewSet memakai bentuk dict, dengan setujui_batal dan tolak_batal dipetakan ke SETUJUI_PEMBATALAN sementara action lain memakai KELOLA_TRANSAKSI backend/pembelian/views.py:67-77. DasborView mendeklarasikan izin = LIHAT_PRODUK lalu memeriksa request.user.izin di dalam handler untuk menyembunyikan angka keuangan dari peran yang tidak boleh melihat laporan backend/laporan/views.py:166-173.

⚠️ View tanpa atribut izin selalu ditolak walaupun penggunanya sudah login, dan False dari PunyaIzin muncul sebagai 403. Perilaku ini dikunci oleh test_view_baru_tanpa_deklarasi_izin_ditolak backend/tests/test_izin.py:123-131.