← Back to FastAPI map
FastAPI · Advanced

Auth & security

Logging in with a JWT token, a dependency that hands you the current user, role checks, and API keys.

JWT

Overview

Authentication proves who the caller is; authorization decides what they may do. The common FastAPI pattern: a login endpoint checks a hashed password and issues a signed JWT, the client sends it in the Authorization: Bearer header, and a get_current_user dependency verifies its signature and expiry on every request. Roles and permissions are simply more dependencies layered on top.

Key concepts

Password hashing
Store only slow, salted hashes (Argon2 or bcrypt) - never plain or reversibly encrypted passwords.
JWT
header.payload.signature. Anyone can read the payload; only someone with the secret can produce a valid signature. "sub" identifies the user and "exp" sets the expiry.
OAuth2PasswordBearer
Pulls the token from the Authorization header and makes the Authorize button in /docs work.
401 vs 403
401 means not authenticated - the token is missing or invalid. 403 means authenticated but not allowed.
API keys
A simpler scheme for server-to-server calls. Compare keys with secrets.compare_digest to avoid timing attacks.

Best practices

  • Load the signing secret from settings, keep access tokens short-lived, and use refresh tokens for long sessions.
  • Never put sensitive data in a JWT payload - it is only encoded, not encrypted.
  • Rate-limit the login endpoint to slow down password guessing.

JWT authentication

Security utilities

Step 1
POST /auth/login→Issue JWT→Client stores token→Authorization: Bearer→Verify JWT
# pip install pyjwt "pwdlib[argon2]" from datetime import datetime, timedelta, timezone import jwt from pwdlib import PasswordHash SECRET_KEY = "change-me-in-production-min-32-chars" # load from settings ALGORITHM = "HS256" password_hash = PasswordHash.recommended() # Argon2 def hash_password(pw: str) -> str: return password_hash.hash(pw) def verify_password(plain: str, hashed: str) -> bool: return password_hash.verify(plain, hashed) def create_access_token(user_id: int, expires_min: int = 30) -> str: expire = datetime.now(timezone.utc) + timedelta(minutes=expires_min) return jwt.encode({"sub": str(user_id), "exp": expire}, SECRET_KEY, algorithm=ALGORITHM)

Tip: The current FastAPI docs use PyJWT and pwdlib. python-jose and passlib, common in older tutorials, are no longer actively maintained.

get_current_user dependency

Step 2
from fastapi.security import OAuth2PasswordBearer from fastapi import Depends, HTTPException, status from jwt.exceptions import InvalidTokenError oauth2 = OAuth2PasswordBearer(tokenUrl="auth/login") async def get_current_user( token: str = Depends(oauth2), db = Depends(get_db) ): exc = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id = payload.get("sub") if not user_id: raise exc except InvalidTokenError: # bad signature, expired, malformed raise exc user = await db.get(User, int(user_id)) if not user or not user.is_active: raise exc return user # Role-based authorization def require_role(*roles: str): async def check(user = Depends(get_current_user)): if user.role not in roles: raise HTTPException(403, "Forbidden") return user return check # Use in routes @app.get("/me") async def me(user = Depends(get_current_user)): return user @app.delete("/users/{id}") async def delete(id: int, user = Depends(require_role("admin"))): ...

Watch out: Always pass algorithms=[...] to jwt.decode. Accepting whatever algorithm the token header claims is a classic JWT vulnerability.

Login endpoint

Step 3
from fastapi.security import OAuth2PasswordRequestForm @app.post("/auth/login") async def login( form: OAuth2PasswordRequestForm = Depends(), db = Depends(get_db) ): user = await get_user_by_email(db, form.username) if not user or not verify_password(form.password, user.hashed_password): raise HTTPException(401, "Invalid credentials") return { "access_token": create_access_token(user.id), "token_type": "bearer" }

Tip: Return the same error for "no such user" and "wrong password", so the endpoint does not reveal which emails are registered.

API key auth - alternative

API Keys
from fastapi.security import APIKeyHeader import secrets api_key_scheme = APIKeyHeader(name="X-API-Key") VALID_KEYS = {"key1", "key2"} # store in DB in production async def verify_api_key(key: str = Depends(api_key_scheme)): # secrets.compare_digest prevents timing attacks if not any(secrets.compare_digest(key, k) for k in VALID_KEYS): raise HTTPException(403, "Invalid API key") @app.get("/data", dependencies=[Depends(verify_api_key)]) async def data(): return {"data": "..."}

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...