Chapter 15: JWT Authentication + Token-Based Auth + Login API

Let’s Start With a Real Problem

At the end of Chapter 14, your API can issue a JWT on login. That’s genuinely useful — but right now, absolutely nothing is actually checking that token. Any route in your app would happily respond to a request whether the client sent a valid token, an expired token, a token for a different app entirely, or no token at all. Issuing a token means nothing if nobody ever verifies it.

This chapter closes that gap. You’ll build the piece that turns “we can issue tokens” into “we can actually protect routes” — a reusable dependency that decodes and verifies a token, and hands your route function the real, currently-logged-in user. Then we’ll go one step further and add refresh tokens, so users don’t get logged out the moment their short-lived access token expires.


What You’ll Learn

  • How to verify a JWT on every request using HTTPBearer
  • How to build a reusable get_current_user dependency — usable on any route with one line
  • How to protect routes so they reject missing, invalid, or expired tokens correctly
  • The difference between an access token and a refresh token, and why real apps use both
  • How to build a /auth/refresh endpoint that issues a new access token without asking the user to log in again
  • Theory + hands-on assignment questions to lock in what you’ve learned

💡 Where this fits in: We’re using FastAPI’s HTTPBearer security scheme here — a straightforward way to extract a Bearer token from the Authorization header. In Chapter 16, we’ll upgrade to FastAPI’s official OAuth2PasswordBearer flow, which is what unlocks Swagger UI’s built-in “Authorize” button and is the pattern FastAPI’s own documentation centers its security tutorial around.


Theory: Verifying a Token vs. Issuing One

Issuing a token (Chapter 14) and verifying a token (this chapter) are two completely separate responsibilities:

Issuing a TokenVerifying a Token
WhenAt loginOn every subsequent protected request
What it doesSigns a new token containing the user’s identityChecks the signature, checks expiry, extracts the identity back out
Where it livesPOST /auth/loginA dependency, reused across many routes

Verification typically happens in three steps:

  1. Extract the token from the incoming request (usually the Authorization: Bearer <token> header).
  2. Decode and validate it — check the signature matches, and check it hasn’t expired.
  3. Look up the identified user, so the route function receives a real, usable User object — not just a raw string from the token.

Because this exact three-step process is needed by every protected route in your app, it’s the perfect candidate for a FastAPI dependency — write it once, then attach it to any route with Depends(get_current_user).


Program 1: Building a get_current_user Dependency

Program Objective

Build a small, standalone FastAPI project with a reusable get_current_user dependency that verifies a JWT from the Authorization header and returns the real logged-in user — then use it to protect a GET /users/me endpoint.

Step-by-Step Implementation

Step 1: Create the project

uv init protected-routes-demo
cd protected-routes-demo
uv add fastapi --extra standard
uv add pydantic-settings
uv add sqlalchemy
uv add "passlib[bcrypt]"
uv add "python-jose[cryptography]"

Step 2: Create the project structure

mkdir -p app/core app/db app/models app/schemas app/services app/routers
touch app/__init__.py app/core/__init__.py app/db/__init__.py app/models/__init__.py app/schemas/__init__.py app/services/__init__.py app/routers/__init__.py
protected-routes-demo/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── config.py
│   │   ├── security.py
│   │   └── dependencies.py
│   ├── db/
│   │   ├── __init__.py
│   │   └── database.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── user.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── token.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py
│   └── routers/
│       ├── __init__.py
│       ├── users.py
│       └── auth.py
├── pyproject.toml
└── uv.lock

Step 3: Create each file with the code below. Step 4: Run and test.

Complete Working Source Code

app/core/config.py

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Protected Routes Demo"
    app_version: str = "1.0.0"
    database_url: str = "sqlite:///./app.db"

    # NOTE: Hardcoded placeholder secret for learning purposes only.
    # Chapter 17 covers moving secrets like this into a proper .env file.
    jwt_secret_key: str = "a-very-insecure-demo-secret-change-this"
    jwt_algorithm: str = "HS256"
    access_token_expire_minutes: int = 30


settings = Settings()

app/core/security.py

from datetime import datetime, timedelta, timezone

from jose import jwt
from passlib.context import CryptContext

from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def hash_password(plain_password: str) -> str:
    return pwd_context.hash(plain_password)


def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)


def create_access_token(subject: str) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
    to_encode = {"sub": subject, "exp": expire}
    return jwt.encode(to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)

app/core/dependencies.py

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session

from app.core.config import settings
from app.db.database import get_db
from app.models.user import User
from app.services.user_service import get_user_by_username

bearer_scheme = HTTPBearer()


def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
    db: Session = Depends(get_db),
) -> User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials.",
        headers={"WWW-Authenticate": "Bearer"},
    )

    token = credentials.credentials

    try:
        payload = jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
        username = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    user = get_user_by_username(db, username)
    if user is None:
        raise credentials_exception

    return user

app/db/database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

from app.core.config import settings

engine = create_engine(settings.database_url, connect_args={"check_same_thread": False})

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)


class Base(DeclarativeBase):
    pass


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

app/models/user.py

from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column

from app.db.database import Base


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, index=True)
    username: Mapped[str] = mapped_column(String(30), unique=True, index=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    hashed_password: Mapped[str] = mapped_column(String(255))
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)

app/schemas/user.py

from typing import Annotated

from pydantic import BaseModel, ConfigDict, EmailStr, Field


class UserCreate(BaseModel):
    username: Annotated[str, Field(min_length=3, max_length=30)]
    email: Annotated[EmailStr, Field()]
    password: Annotated[str, Field(min_length=8, max_length=64)]


class UserOut(BaseModel):
    id: int
    username: str
    email: EmailStr
    is_active: bool

    model_config = ConfigDict(from_attributes=True)

app/schemas/token.py

from pydantic import BaseModel


class LoginRequest(BaseModel):
    username: str
    password: str


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"

app/services/user_service.py

from sqlalchemy.orm import Session

from app.core.security import hash_password
from app.models.user import User
from app.schemas.user import UserCreate


def create_user(db: Session, user_in: UserCreate) -> User:
    new_user = User(
        username=user_in.username,
        email=user_in.email,
        hashed_password=hash_password(user_in.password),
    )
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    return new_user


def get_user_by_username(db: Session, username: str) -> User | None:
    return db.query(User).filter(User.username == username).first()

app/routers/auth.py

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.core.security import create_access_token, verify_password
from app.db.database import get_db
from app.schemas.token import LoginRequest, TokenResponse
from app.services.user_service import get_user_by_username

router = APIRouter(prefix="/auth", tags=["Auth"])


@router.post("/login", response_model=TokenResponse)
def login(credentials: LoginRequest, db: Session = Depends(get_db)):
    user = get_user_by_username(db, credentials.username)

    if user is None or not verify_password(credentials.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password.",
        )

    access_token = create_access_token(subject=user.username)
    return TokenResponse(access_token=access_token)

app/routers/users.py

from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session

from app.core.dependencies import get_current_user
from app.db.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserOut
from app.services.user_service import create_user

router = APIRouter(prefix="/users", tags=["Users"])


@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_user(user_in: UserCreate, db: Session = Depends(get_db)):
    return create_user(db, user_in)


@router.get("/me", response_model=UserOut)
def read_current_user(current_user: User = Depends(get_current_user)):
    return current_user

app/main.py

from fastapi import FastAPI

from app.core.config import settings
from app.db.database import Base, engine
from app.models import user  # noqa: F401
from app.routers import auth, users

Base.metadata.create_all(bind=engine)

app = FastAPI(title=settings.app_name, version=settings.app_version)

app.include_router(users.router)
app.include_router(auth.router)


@app.get("/", tags=["Root"])
def read_root():
    return {"message": f"Welcome to {settings.app_name}"}

Detailed Code Explanation

  • HTTPBearer() is a FastAPI security utility that expects an Authorization: Bearer <token> header on the request. When used with Depends(bearer_scheme), FastAPI automatically extracts the header and gives you an HTTPAuthorizationCredentials object — if the header is missing entirely, FastAPI rejects the request with a 403 before your dependency function even runs.
  • get_current_user is the centerpiece of this chapter:
    • It first prepares one reusable credentials_exception — since “invalid token,” “malformed payload,” and “user no longer exists” should all look identical to the client (another anti-enumeration habit, just like Chapter 14’s login).
    • jwt.decode(token, settings.jwt_secret_key, algorithms=[...]) verifies the signature and checks the "exp" claim automatically — if the token has expired, python-jose raises a JWTError on its own, which we catch and turn into our generic 401.
    • payload.get("sub") retrieves the username we originally embedded in Chapter 14’s create_access_token.
    • Finally, it looks the user up in the database — this extra step matters because it means a token remains “valid” only as long as that user still genuinely exists in your system.
  • routers/users.py shows the entire point of this pattern in one line: current_user: User = Depends(get_current_user). FastAPI runs the whole verification chain automatically before read_current_user ever executes — if it fails at any step, your route function’s body never runs at all, and the client gets a clean 401.

Execution & Testing

uv run fastapi dev app/main.py
  1. Open http://127.0.0.1:8000/docs, register a user, then log in via POST /auth/login to get an access_token.
  2. Call GET /users/me without any authorization — confirm you get a 403 (no credentials were supplied at all).
  3. In Swagger UI, expand GET /users/me, click the padlock icon or manually add an Authorization: Bearer <your_token> header, and execute again — confirm you now get your own user data back.
  4. Deliberately change one character in your token and try again — confirm you get the generic 401 Could not validate credentials, since the signature no longer matches.

Program 2: Access Tokens + Refresh Tokens

Program Objective

Extend the login flow to issue two tokens — a short-lived access token and a longer-lived refresh token — and build a POST /auth/refresh endpoint that exchanges a valid refresh token for a brand-new access token, without requiring the user to log in again.

Step-by-Step Implementation

Step 1: Create the project

uv init refresh-token-demo
cd refresh-token-demo
uv add fastapi --extra standard
uv add pydantic-settings
uv add sqlalchemy
uv add "passlib[bcrypt]"
uv add "python-jose[cryptography]"

Step 2: Create the project structure

mkdir -p app/core app/db app/models app/schemas app/services app/routers
touch app/__init__.py app/core/__init__.py app/db/__init__.py app/models/__init__.py app/schemas/__init__.py app/services/__init__.py app/routers/__init__.py
refresh-token-demo/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── config.py
│   │   ├── security.py
│   │   └── dependencies.py
│   ├── db/
│   │   ├── __init__.py
│   │   └── database.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── user.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── token.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py
│   └── routers/
│       ├── __init__.py
│       ├── users.py
│       └── auth.py
├── pyproject.toml
└── uv.lock

Step 3: Create each file with the code below. Step 4: Run and test.

Complete Working Source Code

app/core/config.py

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Refresh Token Demo"
    app_version: str = "1.0.0"
    database_url: str = "sqlite:///./app.db"

    jwt_secret_key: str = "a-very-insecure-demo-secret-change-this"
    jwt_algorithm: str = "HS256"
    access_token_expire_minutes: int = 30
    refresh_token_expire_days: int = 7


settings = Settings()

app/core/security.py

from datetime import datetime, timedelta, timezone

from jose import JWTError, jwt
from passlib.context import CryptContext

from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def hash_password(plain_password: str) -> str:
    return pwd_context.hash(plain_password)


def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)


def _create_token(subject: str, token_type: str, expires_delta: timedelta) -> str:
    expire = datetime.now(timezone.utc) + expires_delta
    to_encode = {"sub": subject, "type": token_type, "exp": expire}
    return jwt.encode(to_encode, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)


def create_access_token(subject: str) -> str:
    return _create_token(
        subject,
        token_type="access",
        expires_delta=timedelta(minutes=settings.access_token_expire_minutes),
    )


def create_refresh_token(subject: str) -> str:
    return _create_token(
        subject,
        token_type="refresh",
        expires_delta=timedelta(days=settings.refresh_token_expire_days),
    )


def decode_token(token: str) -> dict:
    try:
        return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm])
    except JWTError as exc:
        raise ValueError("Invalid or expired token.") from exc

app/core/dependencies.py

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session

from app.core.security import decode_token
from app.db.database import get_db
from app.models.user import User
from app.services.user_service import get_user_by_username

bearer_scheme = HTTPBearer()


def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
    db: Session = Depends(get_db),
) -> User:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials.",
        headers={"WWW-Authenticate": "Bearer"},
    )

    try:
        payload = decode_token(credentials.credentials)
    except ValueError:
        raise credentials_exception

    if payload.get("type") != "access":
        raise credentials_exception

    username = payload.get("sub")
    if username is None:
        raise credentials_exception

    user = get_user_by_username(db, username)
    if user is None:
        raise credentials_exception

    return user

app/db/database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

from app.core.config import settings

engine = create_engine(settings.database_url, connect_args={"check_same_thread": False})

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)


class Base(DeclarativeBase):
    pass


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

app/models/user.py

from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column

from app.db.database import Base


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, index=True)
    username: Mapped[str] = mapped_column(String(30), unique=True, index=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    hashed_password: Mapped[str] = mapped_column(String(255))
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)

app/schemas/user.py

from typing import Annotated

from pydantic import BaseModel, ConfigDict, EmailStr, Field


class UserCreate(BaseModel):
    username: Annotated[str, Field(min_length=3, max_length=30)]
    email: Annotated[EmailStr, Field()]
    password: Annotated[str, Field(min_length=8, max_length=64)]


class UserOut(BaseModel):
    id: int
    username: str
    email: EmailStr
    is_active: bool

    model_config = ConfigDict(from_attributes=True)

app/schemas/token.py

from pydantic import BaseModel


class LoginRequest(BaseModel):
    username: str
    password: str


class TokenResponse(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "bearer"


class RefreshRequest(BaseModel):
    refresh_token: str


class AccessTokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"

app/services/user_service.py

from sqlalchemy.orm import Session

from app.core.security import hash_password
from app.models.user import User
from app.schemas.user import UserCreate


def create_user(db: Session, user_in: UserCreate) -> User:
    new_user = User(
        username=user_in.username,
        email=user_in.email,
        hashed_password=hash_password(user_in.password),
    )
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    return new_user


def get_user_by_username(db: Session, username: str) -> User | None:
    return db.query(User).filter(User.username == username).first()

app/routers/auth.py

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.core.security import create_access_token, create_refresh_token, decode_token, verify_password
from app.db.database import get_db
from app.schemas.token import AccessTokenResponse, LoginRequest, RefreshRequest, TokenResponse
from app.services.user_service import get_user_by_username

router = APIRouter(prefix="/auth", tags=["Auth"])


@router.post("/login", response_model=TokenResponse)
def login(credentials: LoginRequest, db: Session = Depends(get_db)):
    user = get_user_by_username(db, credentials.username)

    if user is None or not verify_password(credentials.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password.",
        )

    return TokenResponse(
        access_token=create_access_token(subject=user.username),
        refresh_token=create_refresh_token(subject=user.username),
    )


@router.post("/refresh", response_model=AccessTokenResponse)
def refresh_access_token(data: RefreshRequest, db: Session = Depends(get_db)):
    invalid_token_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or expired refresh token.",
    )

    try:
        payload = decode_token(data.refresh_token)
    except ValueError:
        raise invalid_token_exception

    if payload.get("type") != "refresh":
        raise invalid_token_exception

    username = payload.get("sub")
    user = get_user_by_username(db, username) if username else None
    if user is None:
        raise invalid_token_exception

    return AccessTokenResponse(access_token=create_access_token(subject=user.username))

app/routers/users.py

from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session

from app.core.dependencies import get_current_user
from app.db.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserOut
from app.services.user_service import create_user

router = APIRouter(prefix="/users", tags=["Users"])


@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_user(user_in: UserCreate, db: Session = Depends(get_db)):
    return create_user(db, user_in)


@router.get("/me", response_model=UserOut)
def read_current_user(current_user: User = Depends(get_current_user)):
    return current_user

app/main.py

from fastapi import FastAPI

from app.core.config import settings
from app.db.database import Base, engine
from app.models import user  # noqa: F401
from app.routers import auth, users

Base.metadata.create_all(bind=engine)

app = FastAPI(title=settings.app_name, version=settings.app_version)

app.include_router(users.router)
app.include_router(auth.router)


@app.get("/", tags=["Root"])
def read_root():
    return {"message": f"Welcome to {settings.app_name}"}

Detailed Code Explanation

  • Why two tokens? An access token is intentionally short-lived (30 minutes here) — if it’s ever stolen, the damage window is small. But logging a user out every 30 minutes would be a terrible experience. A refresh token solves this: it lives much longer (7 days here), and its only job is to be exchanged for a new access token — it can’t be used to access protected data directly.
  • _create_token(subject, token_type, expires_delta) is a shared private helper both create_access_token and create_refresh_token call into, avoiding duplicated logic. The critical addition here is the "type" claim — embedding "access" or "refresh" directly inside the token payload, so we can tell the two apart later.
  • decode_token(token) centralizes decoding and turns any JWTError (expired, malformed, wrong signature) into a plain ValueError — a small abstraction that keeps python-jose-specific exception types out of our routers and dependencies.
  • get_current_user now explicitly checks payload.get("type") != "access" before proceeding. This is an important security detail: without this check, a client could use a refresh token directly as if it were an access token, defeating the entire purpose of having two separate token types with different lifetimes and privileges.
  • POST /auth/refresh performs the mirror-image check — it only accepts a token whose "type" is "refresh", and if everything checks out, mints a brand-new access token only. Notice the refresh endpoint never issues a new refresh token in this simple version — real production systems often add refresh token rotation and revocation lists, which is a more advanced topic worth exploring once you’re comfortable with these fundamentals.

Execution & Testing

uv run fastapi dev app/main.py
  1. Register a user, then call POST /auth/login — confirm the response now contains both access_token and refresh_token.
  2. Call GET /users/me using the access_token — confirm it works, exactly as in Program 1.
  3. Call GET /users/me again, but this time paste the refresh_token into the Authorization header instead — confirm you get a 401, proving the "type" check is working correctly.
  4. Call POST /auth/refresh with {"refresh_token": "<your refresh token>"} — confirm you receive a brand-new access_token.
  5. Call POST /auth/refresh again, but this time send your access token in the refresh_token field — confirm you get a 401 Invalid or expired refresh token, proving the reverse check works too.

Tips & Instructions for Developers

  • ✅ Always embed a "type" claim (or similar) when your app issues more than one kind of token — never assume a token is being used for its intended purpose without checking.
  • ✅ Keep access tokens short-lived and refresh tokens longer-lived — this is the standard, widely recommended balance between security and user experience.
  • ✅ Build your get_current_user dependency once, and reuse it everywhere — resist the temptation to manually re-decode tokens inside individual route functions.
  • ✅ Return generic, consistent error messages for any kind of token failure — don’t reveal why a token failed (expired vs. tampered vs. wrong type) to the client.
  • ✅ Look up the user from the database inside get_current_user, not just trust the token’s claims blindly — this ensures a deleted or deactivated user is immediately locked out, even with a technically still-valid token.
  • ❌ Don’t let a refresh token be usable anywhere an access token is expected, or vice versa — that quietly defeats the entire point of separating them.
  • ❌ Don’t skip testing your dependency’s failure paths — missing tokens, expired tokens, and tampered tokens are exactly the scenarios real attackers will try first.

Official FastAPI Documentation References


Theory Questions

  1. Why is issuing a token and verifying a token treated as two completely separate responsibilities?
  2. What does HTTPBearer() actually do when used with Depends(...), and what happens if the Authorization header is missing entirely?
  3. Why does get_current_user look the user up in the database instead of trusting the token’s claims alone?
  4. Why do access tokens and refresh tokens typically have very different expiry lengths?
  5. What security problem does checking the "type" claim prevent, and what could go wrong without it?

Practical Assignment

  1. Add an is_active check: Starting from Program 1’s project, modify get_current_user to also raise a 401 if user.is_active is False — simulating what should happen if an admin deactivates someone’s account.
  2. Add a logout concept: Starting from Program 2’s project, research (and briefly describe in your own words, as a comment in your code) how a real production system might “revoke” a refresh token before it naturally expires — since JWTs are stateless and can’t simply be “deleted” the way a database session could be.
  3. Full flow test: Starting from Program 2’s project, write out (as a numbered list, either on paper or as a comment block) the exact sequence of API calls a mobile app would make over a user’s session: registering, logging in, calling a protected endpoint, refreshing the access token after it (hypothetically) expires, and calling the protected endpoint again with the new token.

Summary

In this chapter, you learned:

  • How to verify a JWT and build a reusable get_current_user dependency using HTTPBearer
  • How to protect routes so they properly reject missing, invalid, or expired tokens
  • The difference between access tokens and refresh tokens, and why real applications use both
  • How to build a /auth/refresh endpoint, and how a "type" claim prevents the two token types from being used interchangeably
Scroll to Top