Membangun autentikasi lengkap: OAuth2 password flow dengan OAuth2PasswordBearer dan Form, hashing password dengan bcrypt, pembuatan dan verifikasi token JWT dengan PyJWT, serta proteksi route lewat dependency get_current_user.

Setelah di episode 11 API kita bisa diakses browser lewat CORS dan terlindungi dari host spoofing, sekarang kita jawab pertanyaan yang paling penting di API produksi: siapa pemakainya? Episode ini membangun autentikasi lengkap dengan pola standar industri: OAuth2 password flow, hashing password, dan token JWT.
Mengapa episode ini penting? Autentikasi adalah titik yang paling sering diserang dan paling sering ditulis salah. Pola yang kita bangun di sini — OAuth2PasswordBearer, hash bcrypt, JWT dengan expiry, dan dependency get_current_user — adalah blueprint yang dipakai di hampir semua backend FastAPI komersial.
Dua tahap: klien menukar credentials dengan token, lalu token dipakai untuk request berikutnya. FastAPI menyediakan tooling untuk alur ini, termasuk integrasi ke Swagger UI.
Password tidak boleh disimpan sebagai teks. Kita simpan hanya hash-nya:
pip install "pyjwt[crypto]" "bcrypt"Catatan 2026: passlib lama bermasalah dengan bcrypt 4.x (error _norm__hash), jadi kita langsung memakai bcrypt — lebih sederhana dan tanpa lapisan compat yang bermasalah.
import bcrypt
def hash_password(password: str) -> str:
return bcrypt.hashpw(
password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
def verify_password(password: str, password_hash: str) -> bool:
return bcrypt.checkpw(
password.encode("utf-8"), password_hash.encode("utf-8")
)bcrypt.gensalt() menambahkan salt acak per password — dua user dengan password sama menghasilkan hash berbeda. checkpw membandingkan tanpa membocorkan waktu (constant-time).
Warning
Jangan pernah menulis logika hashing sendiri (SHA256 password polos bukanlah hashing yang aman — cepat di-brute-force). Bcrypt dirancang lambat secara sengaja; itulah justru keunggulannya melawan serangan offline. Episode 18 membahas ini lebih dalam.
OAuth2PasswordBearer mendefinisikan skema keamanan — sekaligus memberi tahu Swagger UI untuk menampilkan tombol "Authorize":
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")tokenUrl="token" memberitahu Swagger dan klien: token diperoleh dari endpoint POST /token. Parameter bertipe oauth2_scheme di handler akan mengekstrak token dari header Authorization: Bearer ....
Endpoint login menerima form (bukan JSON — spesifikasi OAuth2), memverifikasi password, lalu menerbitkan JWT:
from datetime import datetime, timedelta, timezone
import jwt
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app import models, schemas, security
from app.database import get_db
SECRET_KEY = "ganti-dengan-secret-kuat"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db),
) -> schemas.Token:
user = db.query(models.User).filter(
models.User.username == form_data.username
).first()
if user is None or not security.verify_password(
form_data.password, user.password_hash
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Username atau password salah",
headers={"WWW-Authenticate": "Bearer"},
)
return create_access_token(user.username)Detail penting: error 401 harus disertai header WWW-Authenticate: Bearer — sesuai spesifikasi HTTP dan dipakai Swagger/klien untuk mengenali jalur login.
Important
OAuth2PasswordRequestForm hanya menangani x-www-form-urlencoded (bukan JSON) — inilah mengapa di episode 6 kita berkenalan dengan Form. Klien JS kalian harus mengirim username=...&password=..., bukan JSON body.
from datetime import datetime, timedelta, timezone
import jwt
SECRET_KEY = "ganti-dengan-secret-kuat"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
)
payload = {"sub": subject, "exp": expire}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def decode_token(token: str) -> str:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload["sub"]exp (expiration) adalah klaim wajib — tanpa expiry, token curian berlaku selamanya. jwt.decode otomatis menolak token kedaluwarsa dan token dengan signature salah.
Dependency get_current_user memproteksi route — ia memverifikasi token dan memuat user:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app import models, security
from app.database import get_db
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> models.User:
credentials_error = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Kredensial tidak valid",
headers={"WWW-Authenticate": "Bearer"},
)
try:
username = security.decode_token(token)
except jwt.InvalidTokenError:
raise credentials_error
user = db.query(models.User).filter(
models.User.username == username
).first()
if user is None:
raise credentials_error
return userLalu route terproteksi tinggal meminta user:
from typing import Annotated
CurrentUser = Annotated[models.User, Depends(get_current_user)]
@app.get("/users/me")
def read_me(current_user: CurrentUser) -> schemas.UserOut:
return current_userJika token tidak valid, FastAPI mengembalikan 401 sebelum handler dijalankan. Pola Annotated membuat proteksi route hanya satu deklarasi.
Tip
Coba endpoint ini di /docs dengan tombol Authorize di kanan atas: masukkan username/password, Swagger menyimpan token, dan semua endpoint terproteksi langsung bisa dieksekusi dengan header Authorization otomatis. Ini salah satu alasan developer menyukai FastAPI — docs dan auth bekerja sama.
| Pitfall | Solusi |
|---|---|
passlib + bcrypt 4.x error | Pakai bcrypt langsung |
JWT tanpa exp | Token berlaku selamanya |
| Form login dikirim sebagai JSON | Kirim application/x-www-form-urlencoded |
401 tanpa WWW-Authenticate | Tambahkan header sesuai spesifikasi |
| Secret key di kode | Pindah ke environment (episode 16) |
Inti yang harus dibawa pulang:
/token, pakai token lewat Bearer.sub untuk identitas, exp untuk expiry, verifikasi algorithms eksplisit.get_current_user sebagai dependency memproteksi route dengan satu deklarasi.Di episode 13 selanjutnya kita akan membahas WebSocket & SSE — realtime communication. Kalian akan membangun chat dengan connection manager, mengirim notifikasi realtime, dan streaming data dengan Server-Sent Events. API kalian tidak akan terasa "kuno" lagi!