Guards and Authentication¶
This guide covers how to protect your routes using litestar-api-auth guards.
Basic Authentication¶
The require_api_key guard ensures that a valid API key is present in the request:
from litestar import get
from litestar_api_auth import require_api_key
@get("/api/data", guards=[require_api_key])
async def get_data() -> dict:
"""This route requires a valid API key."""
return {"message": "Authenticated!"}
Revocation Timing¶
Guards check the APIKeyInfo snapshot APIKeyMiddleware captured from a
single backend lookup at the start of the request – they do not query the
backend again. Expiry is still evaluated live (the snapshotted expires_at
is compared against the current time on every guard check), but is_active
and scopes are copied from that one lookup and are not re-fetched.
In practice this means: if a key is revoked, or has a scope removed, while
a request using that key is already past APIKeyMiddleware and running
through its guards/handler, that one in-flight request finishes using the
pre-change snapshot. This is normal request-scoped caching, not a reusable
bypass – every request that starts after the change lands is rejected (or
denied the removed scope) as expected, since it performs its own fresh
backend lookup.
Multiple Scopes¶
Require multiple scopes for a single route:
from litestar_api_auth import require_all_scopes, require_any_scope
# Require ALL scopes
@delete("/api/admin/purge", guards=[require_all_scopes("admin", "dangerous:write")])
async def purge_data() -> None:
"""Requires both 'admin' AND 'dangerous:write' scopes."""
pass
# Require ANY scope
@get("/api/reports", guards=[require_any_scope("reports:read", "admin")])
async def get_reports() -> list:
"""Requires either 'reports:read' OR 'admin' scope."""
return []
Accessing the API Key¶
Access the authenticated API key in your route handler:
from litestar import get, Request
from litestar_api_auth import require_api_key, APIKey
@get("/api/whoami", guards=[require_api_key])
async def whoami(request: Request) -> dict:
"""Return information about the authenticated API key."""
api_key: APIKey = request.state.api_key
return {
"key_id": api_key.id,
"name": api_key.name,
"owner_id": api_key.owner_id,
"scopes": api_key.scopes,
}
Custom Guards¶
Create custom guards for specialized authentication logic:
from litestar import Request
from litestar.connection import ASGIConnection
from litestar.handlers import BaseRouteHandler
from litestar_api_auth import APIKey
from litestar_api_auth.exceptions import InsufficientScopeError
def require_owner(connection: ASGIConnection, handler: BaseRouteHandler) -> None:
"""Ensure the API key owner matches the requested resource owner."""
api_key: APIKey = connection.state.api_key
resource_owner_id = connection.path_params.get("owner_id")
if api_key.owner_id != resource_owner_id:
raise InsufficientScopeError("You can only access your own resources")
@get("/api/users/{owner_id:str}/data", guards=[require_api_key, require_owner])
async def get_user_data(owner_id: str) -> dict:
"""Only accessible by the resource owner."""
return {"owner_id": owner_id}
Error Handling¶
Guards raise specific exceptions that you can handle:
from litestar import Litestar, Response
from litestar_api_auth.exceptions import (
InvalidAPIKeyError,
ExpiredAPIKeyError,
RevokedAPIKeyError,
InsufficientScopeError,
)
async def handle_invalid_key(request, exc: InvalidAPIKeyError) -> Response:
return Response(
{"error": "Invalid API key"},
status_code=401,
)
async def handle_expired_key(request, exc: ExpiredAPIKeyError) -> Response:
return Response(
{"error": "API key has expired"},
status_code=401,
)
async def handle_insufficient_scope(request, exc: InsufficientScopeError) -> Response:
return Response(
{"error": "Insufficient permissions", "required": exc.required_scope},
status_code=403,
)
app = Litestar(
exception_handlers={
InvalidAPIKeyError: handle_invalid_key,
ExpiredAPIKeyError: handle_expired_key,
InsufficientScopeError: handle_insufficient_scope,
},
)