Skip to content

How-to: custom auth middleware and request.state

A pattern for storing authentication/authorization info on request.state from a custom middleware and retrieving it in handlers via Depends().


1. Define an AuthUser dataclass

Define an immutable dataclass representing the authenticated user.

python
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class AuthUser:
    user_id: str
    roles: list[str]

2. Store on request.state in a JWT middleware

Subclass BaseHTTPMiddleware to implement custom JWT verification and store an AuthUser on request.state.user.

python
import jwt
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.types import ASGIApp

SECRET = "your-32-char-or-longer-secret-key"

class JwtAuthMiddleware(BaseHTTPMiddleware):
    EXCLUDE_PATHS = {"/health", "/login"}

    def __init__(self, app: ASGIApp, secret: str) -> None:
        super().__init__(app)
        self._secret = secret

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        if request.url.path in self.EXCLUDE_PATHS:
            return await call_next(request)

        auth = request.headers.get("Authorization", "")
        if not auth.startswith("Bearer "):
            return JSONResponse({"error": "Unauthorized"}, status_code=401)

        token = auth[7:]
        try:
            payload = jwt.decode(token, self._secret, algorithms=["HS256"])
            request.state.user = AuthUser(
                user_id=payload["sub"],
                roles=payload.get("roles", []),
            )
        except jwt.InvalidTokenError:
            return JSONResponse({"error": "Invalid token"}, status_code=401)

        return await call_next(request)

app = FastAPI()
app.add_middleware(JwtAuthMiddleware, secret=SECRET)

3. Retrieve AuthUser with a Depends factory

Define a Depends factory that reads the AuthUser from request.state.user.

python
from fastapi import Request

def get_current_user(request: Request) -> AuthUser:
    user: AuthUser = request.state.user  # type: ignore[attr-defined]  # reason: always set by JwtAuthMiddleware
    return user

Why type: ignore[attr-defined] is needed: request.state is a starlette.datastructures.State with dynamic attributes, so mypy can't see the user attribute. It's safe because the middleware guarantees it is set.


4. Role-based access control

Define a role-checking Depends such as require_admin() and apply it to endpoints.

python
from typing import Annotated
from fastapi import Depends, HTTPException

def require_admin(user: Annotated[AuthUser, Depends(get_current_user)]) -> AuthUser:
    if "admin" not in user.roles:
        raise HTTPException(status_code=403, detail="Admin required")
    return user

@app.get("/admin/users")
def admin_list_users(
    user: Annotated[AuthUser, Depends(require_admin)],
) -> JSONResponse:
    return JSONResponse({"admin": user.user_id, "users": [...]})

5. When to use this vs. BearerTokenMiddleware

PatternHow it's usedStores on request.state
BearerTokenMiddleware + make_require_auth()Verify/obtain the token stringNo (via Depends)
Custom JwtAuthMiddlewareVerify the JWT payload, build AuthUserYes (request.state.user)

When a custom middleware fits:

  • you want the JWT payload (roles, claims) in handlers
  • you want to share the auth result across the whole request scope

When BearerTokenMiddleware fits:

  • you only want to check token validity
  • you want to swap the verification logic via TokenVerifierProtocol

6. Test pattern

python
import jwt
from fastapi.testclient import TestClient

SECRET = "your-32-char-or-longer-secret-key"

def make_token(user_id: str, roles: list[str]) -> str:
    return jwt.encode({"sub": user_id, "roles": roles}, SECRET, algorithm="HS256")

def test_profile_with_valid_token() -> None:
    with TestClient(app) as client:
        token = make_token("alice", ["user"])
        r = client.get("/profile", headers={"Authorization": f"Bearer {token}"})
        assert r.status_code == 200
        assert r.json()["user_id"] == "alice"

Released under the MIT License.