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_userdependency — 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/refreshendpoint 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
HTTPBearersecurity scheme here — a straightforward way to extract aBearertoken from theAuthorizationheader. In Chapter 16, we’ll upgrade to FastAPI’s officialOAuth2PasswordBearerflow, 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 Token | Verifying a Token | |
|---|---|---|
| When | At login | On every subsequent protected request |
| What it does | Signs a new token containing the user’s identity | Checks the signature, checks expiry, extracts the identity back out |
| Where it lives | POST /auth/login | A dependency, reused across many routes |
Verification typically happens in three steps:
- Extract the token from the incoming request (usually the
Authorization: Bearer <token>header). - Decode and validate it — check the signature matches, and check it hasn’t expired.
- Look up the identified user, so the route function receives a real, usable
Userobject — 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 anAuthorization: Bearer <token>header on the request. When used withDepends(bearer_scheme), FastAPI automatically extracts the header and gives you anHTTPAuthorizationCredentialsobject — if the header is missing entirely, FastAPI rejects the request with a403before your dependency function even runs.get_current_useris 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-joseraises aJWTErroron its own, which we catch and turn into our generic401.payload.get("sub")retrieves the username we originally embedded in Chapter 14’screate_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.
- It first prepares one reusable
routers/users.pyshows the entire point of this pattern in one line:current_user: User = Depends(get_current_user). FastAPI runs the whole verification chain automatically beforeread_current_userever executes — if it fails at any step, your route function’s body never runs at all, and the client gets a clean401.
Execution & Testing
uv run fastapi dev app/main.py
- Open
http://127.0.0.1:8000/docs, register a user, then log in viaPOST /auth/loginto get anaccess_token. - Call
GET /users/mewithout any authorization — confirm you get a403(no credentials were supplied at all). - In Swagger UI, expand
GET /users/me, click the padlock icon or manually add anAuthorization: Bearer <your_token>header, and execute again — confirm you now get your own user data back. - 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 bothcreate_access_tokenandcreate_refresh_tokencall 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 anyJWTError(expired, malformed, wrong signature) into a plainValueError— a small abstraction that keepspython-jose-specific exception types out of our routers and dependencies.get_current_usernow explicitly checkspayload.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/refreshperforms 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
- Register a user, then call
POST /auth/login— confirm the response now contains bothaccess_tokenandrefresh_token. - Call
GET /users/meusing theaccess_token— confirm it works, exactly as in Program 1. - Call
GET /users/meagain, but this time paste therefresh_tokeninto the Authorization header instead — confirm you get a401, proving the"type"check is working correctly. - Call
POST /auth/refreshwith{"refresh_token": "<your refresh token>"}— confirm you receive a brand-newaccess_token. - Call
POST /auth/refreshagain, but this time send your access token in therefresh_tokenfield — confirm you get a401 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_userdependency 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
- Security — OAuth2 with Password (and hashing), Bearer with JWT tokens
- Security — Get Current User
- HTTPBearer — Security Utilities
- Dependencies with yield
- python-jose Documentation
Theory Questions
- Why is issuing a token and verifying a token treated as two completely separate responsibilities?
- What does
HTTPBearer()actually do when used withDepends(...), and what happens if theAuthorizationheader is missing entirely? - Why does
get_current_userlook the user up in the database instead of trusting the token’s claims alone? - Why do access tokens and refresh tokens typically have very different expiry lengths?
- What security problem does checking the
"type"claim prevent, and what could go wrong without it?
Practical Assignment
- Add an
is_activecheck: Starting from Program 1’s project, modifyget_current_userto also raise a401ifuser.is_activeisFalse— simulating what should happen if an admin deactivates someone’s account. - 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.
- 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_userdependency usingHTTPBearer - 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/refreshendpoint, and how a"type"claim prevents the two token types from being used interchangeably
