JWT Authentication in FastAPI

A
Author
24 Jul, 2026 1 min read 315 views

This guide adds a /auth/token login endpoint that returns a JWT, and a dependency that protects routes. It follows the pattern FastAPI's own docs use.

Install

pip install pyjwt "pwdlib[argon2]"

Hashing and tokens

# security.py
from datetime import datetime, timedelta, timezone
import jwt
from pwdlib import PasswordHash

SECRET_KEY = settings.jwt_secret          # long random string from env
ALGORITHM = "HS256"
password_hash = PasswordHash.recommended()  # Argon2

def hash_password(plain: str) -> str:
    return password_hash.hash(plain)

def verify_password(plain: str, hashed: str) -> bool:
    return password_hash.verify(plain, hashed)

def create_access_token(user_id: int, minutes: int = 30) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
    return jwt.encode({"sub": str(user_id), "exp": expire}, SECRET_KEY, algorithm=ALGORITHM)

Login endpoint

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm

@app.post("/auth/token")
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()], db: DbSession):
    user = await users.by_email(db, form.username)
    if not user or not verify_password(form.password, user.password_hash):
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Incorrect email or password")
    return {"access_token": create_access_token(user.id), "token_type": "bearer"}

Protecting routes

from fastapi.security import OAuth2PasswordBearer
from jwt import InvalidTokenError

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")

async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: DbSession) -> User:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    except InvalidTokenError:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired token",
                            headers={"WWW-Authenticate": "Bearer"})
    user = await db.get(User, int(payload["sub"]))
    if user is None:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User not found")
    return user

@app.get("/me")
async def me(user: Annotated[User, Depends(get_current_user)]) -> UserOut:
    return user

Hardening

  • Always pass algorithms=[...] to jwt.decode; never accept the algorithm from the token.
  • Keep access tokens short-lived and use refresh tokens (stored securely) for long sessions.
  • Rate-limit the login endpoint.
  • For browser apps, consider HttpOnly cookies instead of storing tokens in localStorage.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.