OAuth2 and JWT authentication in FastAPI combines a standard login flow with signed tokens. A client submits a username and password once, receives a short-lived Bearer token, and presents it to protected routes. JWT is not encryption: anyone holding the token can read its payload, although changing it invalidates the signature.

This tutorial builds on the guide to creating a REST API with FastAPI. In production, require HTTPS, persist users in a database, rate-limit login attempts, and keep keys outside source code.

Install dependencies and understand the flow

uv add "fastapi[standard]" pyjwt "pwdlib[argon2]"

The /token endpoint receives an OAuth2 form, verifies the password, and returns access_token plus token_type. OAuth2PasswordBearer reads the Authorization header. A dependency checks the signature, expiration, and current user before the endpoint runs.

from datetime import datetime, timedelta, timezone
import jwt
from pwdlib import PasswordHash

ALGORITHM = "HS256"
password_hash = PasswordHash.recommended()

def create_access_token(subject: str, secret: str) -> str:
    expires = datetime.now(timezone.utc) + timedelta(minutes=20)
    return jwt.encode({"sub": subject, "exp": expires}, secret, algorithm=ALGORITHM)

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

Load secret from infrastructure designed for secrets. A hard-coded value stops being secret as soon as it is committed.

Validate tokens in a dependency

from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

async def current_username(
    token: Annotated[str, Depends(oauth2_scheme)],
) -> str:
    error = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        if not isinstance(username, str):
            raise error
    except jwt.InvalidTokenError:
        raise error
    return username

Supply a fixed algorithm allowlist instead of trusting the token header. After reading sub, fetch the user and verify that the account is still active. Keep the subject stable and free from confidential data.

Issue tokens and protect routes

from typing import Annotated
from fastapi import FastAPI, Depends
from fastapi.security import OAuth2PasswordRequestForm

app = FastAPI()

@app.post("/token")
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
    user = await find_user(form.username)
    if user is None or not verify_password(form.password, user.password_hash):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    token = create_access_token(user.username, SECRET_KEY)
    return {"access_token": token, "token_type": "bearer"}

@app.get("/me")
async def me(username: Annotated[str, Depends(current_username)]):
    return {"username": username}

Return the same error for an unknown username and a wrong password to reduce account enumeration. Add OAuth2 scopes or a separate authorization dependency for permissions. Authentication establishes identity; authorization decides what that identity may do.

Production checklist

Use short expiration times, key rotation, synchronized clocks, and a revocation strategy. Access tokens should not become permanent sessions. If you add refresh tokens, protect and rotate them, support revocation, and detect reuse. Never log passwords or complete tokens.

Test expired tokens, bad signatures, disabled users, and missing headers. The official FastAPI OAuth2 and JWT tutorial, accessed July 28, 2026, provides the maintained reference flow for password hashing and Bearer tokens.