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!"}

Scope-Based Authorization

Use require_scope for fine-grained access control:

from litestar import get, post, delete
from litestar_api_auth import require_scope

@get("/api/users", guards=[require_scope("users:read")])
async def list_users() -> list:
    """Requires 'users:read' scope."""
    return []

@post("/api/users", guards=[require_scope("users:write")])
async def create_user(data: dict) -> dict:
    """Requires 'users:write' scope."""
    return data

@delete("/api/users/{user_id:str}", guards=[require_scope("users:delete")])
async def delete_user(user_id: str) -> None:
    """Requires 'users:delete' scope."""
    pass

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,
    },
)