AtomLearnAtomLearn
DashboardGoalsPathAchievementsReviewSign In
DashboardFastAPIFastAPI JWT Auth
FastAPINot Started

FastAPI JWT Auth

Protect routes with OAuth2 password flow, JWT tokens, and python-jose

0%
Knowledge0%
Learn & Drill
Fluency0%
Drill & Speed
Retention0%
Mastery & Review
Confidence0%
All modes
Practice
Knowledge
Fluency
Retention

Knowledge Debt detected

You can study this freely — but your score may plateau if these foundations have gaps. The Mastery badge requires them to be solid.

Explanation

FastAPI's auth story is built around OAuth2 with Bearer tokens — the standard pattern for stateless REST APIs.

The flow:

  • 1. User POSTs credentials → your route verifies them → issues a JWT
  • 2. Client stores the JWT and sends it as Authorization: Bearer <token>
  • 3. Protected routes call Depends(get_current_user) which decodes & validates the JWT

Minimal setup:

python from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext from datetime import datetime, timedelta SECRET_KEY = "your-secret-key" # use a long random string in production ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

Login route — issues a JWT:

python @app.post("/token") def login(form_data: OAuth2PasswordRequestForm = Depends()): user = authenticate_user(form_data.username, form_data.password) if not user: raise HTTPException(status_code=401, detail="Incorrect credentials", headers={"WWW-Authenticate": "Bearer"}) token = create_access_token({"sub": user.username}, expires_delta=timedelta(minutes=30)) return {"access_token": token, "token_type": "bearer"} def create_access_token(data: dict, expires_delta: timedelta) -> str: payload = data.copy() payload["exp"] = datetime.utcnow() + expires_delta return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

Dependency that decodes the token on every protected request:

python def get_current_user(token: str = Depends(oauth2_scheme)) -> User: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception except JWTError: raise credentials_exception user = get_user(username) if user is None: raise credentials_exception return user # Protected route @app.get("/me") def read_me(current_user: User = Depends(get_current_user)): return current_user

Password hashing:

python # Hash on registration hashed = pwd_context.hash(plain_password) # Verify on login is_valid = pwd_context.verify(plain_password, hashed_from_db) `` Never store plain-text passwords. bcrypt is the standard — it's slow by design to resist brute-force.

Examples

Role-based access with a dependency chain

Dependency chains compose cleanly — require_admin reuses get_current_user without duplication

from fastapi import Depends, HTTPException

def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    ...  # decode JWT, return User

def require_admin(user: User = Depends(get_current_user)) -> User:
    if user.role != "admin":
        raise HTTPException(status_code=403, detail="Admins only")
    return user

@app.delete("/users/{user_id}")
def delete_user(user_id: int, admin: User = Depends(require_admin)):
    # only reachable if JWT is valid AND user.role == "admin"
    ...

Refresh tokens pattern

Short-lived access tokens + long-lived refresh tokens: compromise of an access token expires in 15 min, not 30 days

# Issue two tokens on login
@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(form_data.username, form_data.password)
    access = create_access_token({"sub": user.username},
                                  timedelta(minutes=15))   # short-lived
    refresh = create_access_token({"sub": user.username, "type": "refresh"},
                                   timedelta(days=30))     # long-lived
    return {"access_token": access, "refresh_token": refresh}

@app.post("/refresh")
def refresh(refresh_token: str = Body(...)):
    payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
    if payload.get("type") != "refresh":
        raise HTTPException(400, "Invalid token type")
    new_access = create_access_token({"sub": payload["sub"]}, timedelta(minutes=15))
    return {"access_token": new_access}

How well did you understand this?

Next in FastAPI

FastAPI Background Tasks

Continue