🚀
7. Auth Middleware
++++
Engineering
Apr 2026×14 min read

A complete walkthrough of the JWT and basic auth system — from token issuance to protected route enforcement, with the exact design decisions behind each part.

7. Auth Middleware

Driptanil Datta
Driptanil DattaSoftware Developer

The Two Auth Flows

This template supports two forms of authentication, and understanding both helps clarify why the middleware is structured the way it is.

The first flow is token issuance: a client sends credentials to POST /api/auth/token and receives a short-lived JWT. It then uses that JWT as a Bearer token on subsequent protected requests.

The second flow is basic auth: a client sends base64-encoded email:password directly in the Authorization header on every request. This is useful for tooling, API testing, and admin clients that don't want to manage token refresh.

Both flows converge in the middleware, which validates whichever scheme was presented before the request reaches any protected route.

Issuing a Token

The AuthService in server/api/src/services/auth_service.py handles credential validation and token creation. Credentials are read from environment variables at startup — there's no database user table:

# server/api/src/services/auth_service.py
def issue_access_token(self, email: str, password: str) -> AccessToken | None:
    user = self.authenticate_basic(email=email, password=password)
    if user is None:
        return None
 
    expires_at = datetime.now(UTC) + timedelta(minutes=self._jwt_exp_minutes)
    payload = {
        "id": str(user.id),
        "name": user.name,
        "email": user.email,
        "exp": int(expires_at.timestamp()),
    }
    token = jwt.encode(payload, self._jwt_secret, algorithm=self._jwt_algorithm)
    return AccessToken(token=token, expires_at=expires_at)

The payload contains id, name, email, and an expiration timestamp. These four fields are then required during validation — tokens without all of them are rejected. The auth_router calls this from POST /api/auth/token:

curl -X POST http://127.0.0.1:3100/api/auth/token \
  -H 'content-type: application/json' \
  -d '{"email":"admin@example.com","password":"admin123"}'

On success, the response includes access_token, token_type, and expires_at.

Validating Tokens

When a Bearer token is presented, validate_access_token decodes it and verifies every required claim:

def validate_access_token(self, token: str) -> AuthUser | None:
    try:
        payload = jwt.decode(
            token,
            self._jwt_secret,
            algorithms=[self._jwt_algorithm],
            options={"require": ["exp", "id", "name", "email"]},
        )
    except jwt.PyJWTError:
        return None
    ...

options={"require": [...]} is the key line. PyJWT will raise an error if any of the listed claims are missing, so a token that was tampered with or issued by a different service (without all four claims) is automatically rejected. Expiration is always checked — jwt.decode verifies exp by default.

The Middleware: One Enforcement Point

The middleware in server/api/src/middleware/auth_middleware.py is attached as an HTTP middleware via attach_auth_middleware(app). It intercepts every request and applies auth logic based on the path:

# server/api/src/middleware/auth_middleware.py
PUBLIC_PATH_PREFIXES = (
    "/docs",
    "/redoc",
    "/openapi.json",
    "/api/auth/token",
)
PROTECTED_PATH_PREFIXES = ("/api/todos", "/api/auth/me")

Requests to public paths pass through immediately. Requests to protected paths must carry a valid credential. Anything else that doesn't match either list also passes through — the /health endpoint, for example, needs no authentication.

When a protected path is hit, the middleware extracts the Authorization header, parses the scheme, and dispatches to the appropriate validation path:

def _authenticate_request(request, auth_service) -> AuthUser | None:
    header = request.headers.get("Authorization")
    if not header:
        return None
 
    scheme, _, credentials = header.partition(" ")
    if not credentials:
        return None
 
    if scheme.lower() == "bearer":
        return auth_service.validate_access_token(credentials)
 
    if scheme.lower() == "basic":
        decoded = _decode_basic_credentials(credentials)
        if decoded is None:
            return None
        email, password = decoded
        return auth_service.authenticate_basic(email=email, password=password)
 
    return None

If the result is None, the request is rejected with a 401 before it ever reaches a route handler. If the result is an AuthUser, it's stored on request.state.auth_user and the request continues:

user = _authenticate_request(request=request, auth_service=auth_service)
if user is None:
    return _unauthorized("Invalid or missing authentication credentials")
 
request.state.auth_user = user
return await call_next(request)

Accessing the Authenticated User in Routes

Once the middleware sets request.state.auth_user, route handlers can access it through a dependency. The GET /api/auth/me endpoint demonstrates this:

# server/api/src/routers/auth_router.py
@router.get("/me", response_model=AuthUserResponse)
async def get_authenticated_user(user: CurrentUserDep) -> AuthUserResponse:
    return AuthUserResponse.from_domain(user)

The CurrentUserDep alias in server/api/src/dependencies.py extracts the auth_user from request state. The route handler receives a fully typed AuthUser object — it never touches the raw header or does any validation itself.

The Dangers of Skipping Middleware

The two patterns that undermine this completely:

# Trusting user-supplied identity — never do this
@router.get("/api/auth/me")
async def bad_me(user_id: str):
    return {"id": user_id}
 
# Decoding without expiration check — any token works forever
payload = jwt.decode(token, secret, algorithms=["HS256"], options={"verify_exp": False})

The first pattern means any client can claim to be any user by changing a query parameter. The second means a token stolen six months ago is still valid today. Neither is defensible.

The centralized middleware approach means you can't accidentally forget to check auth in a new route — if the path prefix is in PROTECTED_PATH_PREFIXES, authentication is enforced regardless of which handler serves it.

Drip

Driptanil Datta

Software Developer

Building full-stack systems, one commit at a time. This blog is a centralized learning archive for developers.

Legal Notes
Disclaimer

The content provided on this blog is for educational and informational purposes only. While I strive for accuracy, all information is provided "as is" without any warranties of completeness, reliability, or accuracy. Any action you take upon the information found on this website is strictly at your own risk.

Copyright & IP

Certain technical content, interview questions, and datasets are curated from external educational sources to provide a centralized learning resource. Respect for original authorship is maintained; no copyright infringement is intended. All trademarks, logos, and brand names are the property of their respective owners.

System Operational

© 2026 Driptanil Datta. All rights reserved.