Belajar FastAPI - Authentication (OAuth2/JWT)
Episode 12 of 28

Belajar FastAPI - Authentication (OAuth2/JWT)

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.

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

Pendahuluan

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.

Alur OAuth2 Password Flow

100%

Dua tahap: klien menukar credentials dengan token, lalu token dipakai untuk request berikutnya. FastAPI menyediakan tooling untuk alur ini, termasuk integrasi ke Swagger UI.

Hashing Password dengan Bcrypt

Password tidak boleh disimpan sebagai teks. Kita simpan hanya hash-nya:

Install package auth
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.

Pythonapp/security.py
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.

OAuth2 Password Bearer

OAuth2PasswordBearer mendefinisikan skema keamanan — sekaligus memberi tahu Swagger UI untuk menampilkan tombol "Authorize":

Pythonapp/deps.py
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 Token

Endpoint login menerima form (bukan JSON — spesifikasi OAuth2), memverifikasi password, lalu menerbitkan JWT:

Pythonapp/routers/auth.py
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.

Pembuatan dan Verifikasi JWT

Pythonapp/security.py - JWT helper
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 Proteksi Route

Dependency get_current_user memproteksi route — ia memverifikasi token dan memuat user:

Pythonapp/deps.py
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 user

Lalu route terproteksi tinggal meminta user:

PythonRoute terproteksi
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_user

Jika 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.

Common Pitfalls

PitfallSolusi
passlib + bcrypt 4.x errorPakai bcrypt langsung
JWT tanpa expToken berlaku selamanya
Form login dikirim sebagai JSONKirim application/x-www-form-urlencoded
401 tanpa WWW-AuthenticateTambahkan header sesuai spesifikasi
Secret key di kodePindah ke environment (episode 16)

Penutup

Inti yang harus dibawa pulang:

  • OAuth2 password flow: tukar credentials dengan token di /token, pakai token lewat Bearer.
  • Hash password dengan bcrypt (salt acak, lambat secara sengaja); jangan pernah simpan plaintext.
  • JWT: 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!

Belajar FastAPI - Authentication (OAuth2/JWT) | Belajar FastAPI