Authentication

Authentication for the MCP endpoint is a Litestar middleware concern. Apps that already have an authentication middleware (JWT backends, Google IAP, custom token validators) get MCP authentication for free — the middleware populates request.user and request.auth before any route handler runs, including MCP tool handlers.

Authentication only establishes caller identity. See Security for object-level authorization patterns, transport identity boundaries, and safe file/path argument guidance.

MCPAuthConfig is metadata-only: it describes the auth surface advertised by /.well-known/oauth-protected-resource so MCP clients can discover how to obtain a token. Enforcement lives in whichever middleware you install on the app.

Three Integration Paths

Path A — Bring your own auth middleware. Use this when your Litestar app already ships an AbstractAuthenticationMiddleware (or Litestar's built-in JWT backends). MCP tool handlers inherit request.user / request.auth automatically. No MCPAuthBackend needed. See docs/examples/notes/sqlspec/google_iap.py.

Path B — Built-in MCPAuthBackend. Install MCPAuthBackend via DefineMiddleware. It validates bearer tokens against OIDC providers and/or a custom token_validator, then populates connection.user via an optional user_resolver. See docs/examples/notes/sqlspec/cloud_run_jwt.py.

MCPAuthBackend with a custom validator
app = Litestar(
    route_handlers=[],
    plugins=[LitestarMCP(MCPConfig(auth=MCPAuthConfig(issuer="https://auth.example.com")))],
    middleware=[DefineMiddleware(MCPAuthBackend, token_validator=validate_token)],
)

Path C — MCPAuthBackend with OIDC providers. For production OIDC workloads, pass one or more OIDCProviderConfig entries. The backend handles JWKS discovery, caching, and signature verification.

MCPAuthBackend with OIDC auto-discovery
app = Litestar(
    route_handlers=[],
    plugins=[
        LitestarMCP(
            MCPConfig(
                auth=MCPAuthConfig(
                    issuer="https://auth.example.com",
                    audience="https://api.example.com/mcp",
                )
            )
        )
    ],
    middleware=[
        DefineMiddleware(
            MCPAuthBackend,
            providers=[
                OIDCProviderConfig(
                    issuer="https://auth.example.com",
                    audience="https://api.example.com/mcp",
                    algorithms=["RS256"],
                )
            ],
        )
    ],
)

Identity Proxies (custom header / prefix)

Cloud identity proxies verify the caller and inject a signed JWT into a non-standard header. Google Cloud IAP uses X-Goog-IAP-JWT-Assertion with the raw token (no Bearer prefix). Point the built-in validation engine at that header with header_name / token_prefix instead of writing a bespoke middleware — caching and user_resolver are unchanged. IAP signs with ES256 and publishes a fixed JWK set (there is no OIDC discovery document), so set jwks_uri and algorithms explicitly:

from litestar.middleware import DefineMiddleware
from litestar_mcp import MCPAuthBackend, OIDCProviderConfig

DefineMiddleware(
    MCPAuthBackend,
    providers=[
        OIDCProviderConfig(
            issuer="https://cloud.google.com/iap",
            audience="/projects/PROJECT_NUMBER/apps/PROJECT_ID",
            jwks_uri="https://www.gstatic.com/iap/verify/public_key-jwk",
            algorithms=["ES256"],
        ),
    ],
    header_name="X-Goog-IAP-JWT-Assertion",
    token_prefix="",
)

header_name lookup is case-insensitive. When token_prefix is empty, the entire header value is the token, and an absent header is reported as a missing-header error (401 with WWW-Authenticate) rather than an invalid-token error. The defaults (Authorization header with the Bearer prefix) preserve standard bearer behaviour, so existing deployments need no changes.

The same header_name / token_prefix extraction applies to any proxy that forwards a token in a custom header (e.g. AWS ALB's X-Amzn-Oidc-Data). Providers whose keys are not published as a JWK set — ALB signs with per-region PEM keys rather than a JWKS document — need a custom token_validator for the validation step rather than OIDCProviderConfig.

Composable OIDC Factory

create_oidc_validator() returns an async callable that validates a single token against an OIDC issuer. Pass it as MCPAuthBackend(token_validator=...) or use it inside your own middleware. Both clock_skew and jwks_cache_ttl are configurable.

Injectable JWKS Cache

JWKSCache is a protocol-shaped seam for apps that already run their own JWKS / OIDC discovery cache. Pass a shared instance to every validator to avoid redundant network fetches:

docs/examples/snippets/jwks_cache_shared.py
from litestar_mcp import DefaultJWKSCache, create_oidc_validator
from litestar_mcp.auth import OIDCProviderConfig

shared_cache = DefaultJWKSCache()

validator_a = create_oidc_validator(
    "https://company.okta.com",
    "api://mcp-tools",
    jwks_cache=shared_cache,
)
provider_b = OIDCProviderConfig(
    issuer="https://company.okta.com",
    audience="api://admin",
    jwks_cache=shared_cache,
)

When no cache is passed, the validator uses a process-wide default — matching 0.4.0 behaviour — so existing apps need no code changes. Any object implementing async get / async set(*, ttl=int) / async invalidate satisfies the protocol, so a Redis-backed or application-specific cache can drop in cleanly.

Authorization via Guards

Scopes declared on @mcp_tool(scopes=[...]) are discovery metadata only — they surface under tools[].annotations.scopes in tools/list and in /.well-known/oauth-protected-resource. MCP tool dispatch does not enforce scopes inline; attach a Litestar Guard to the route / router / controller for authorization. Guards receive the same ASGIConnection that an HTTP request does, so existing requires_x guards work unchanged on MCP:

docs/examples/snippets/authorization_guard.py
from litestar import Controller, Litestar, get
from litestar.exceptions import PermissionDeniedException

from litestar_mcp import LitestarMCP

if TYPE_CHECKING:
    from litestar.connection import ASGIConnection
    from litestar.handlers.base import BaseRouteHandler


_UNAUTHENTICATED = "Unauthenticated"


def requires_authenticated_user(
    connection: "ASGIConnection[Any, Any, Any, Any]",
    handler: "BaseRouteHandler",
) -> "None":
    """Reject callers whose middleware did not populate ``scope['auth']``."""
    _ = handler
    if connection.scope.get("auth") is None:
        raise PermissionDeniedException(_UNAUTHENTICATED)


class ReportController(Controller):
    path = "/reports"
    guards = [requires_authenticated_user]

    @get("/", mcp_tool="generate_report", scopes=["report:read"], sync_to_thread=False)
    def generate_report(self) -> "dict[str, str]":
        return {"status": "ok"}


app = Litestar(route_handlers=[ReportController], plugins=[LitestarMCP()])

Discovery Metadata

When MCPAuthConfig is attached to MCPConfig, the plugin publishes /.well-known/oauth-protected-resource with the configured issuer, audience, and scopes. This endpoint is always unauthenticated (via Litestar's exclude_from_auth opt key) so clients can bootstrap their auth flow.

Mapping Claims to Users

Middleware populates request.auth with the validated claims dict and request.user with the resolved user object (if a user_resolver is configured). Tool handlers access these via normal Litestar DI:

  • Read request.user directly in the handler signature.

  • Write a Provide(...) dependency that extracts the identity from request.user and returns a domain type.

  • Enforce scopes or roles via guards that inspect request.auth.

See the reference-notes examples for end-to-end wiring across JWT, Dishka, Advanced Alchemy, and Google IAP variants.