Skip to content

Authentication (middleware, authenticator, backend)

An AuthenticationBackend pairs a transport with a strategy: the transport reads token material from the request (for example Authorization: Bearer or cookies), and the strategy validates or issues tokens and resolves the user through the user manager. Login and logout flow through the same pair so issuing, invalidation, and response shaping stay consistent.

An Authenticator holds an ordered list of backends plus a UserManagerProtocol. For each request it tries backends in order and returns the first user that authenticates, so you can stack transports (Bearer plus cookie, multiple named backends, and so on) without duplicating strategy wiring.

API-key backends use ApiKeyTransport with ApiKeyStrategy. Successful API-key authentication sets request.auth to ApiKeyContext rather than the backend name string, so scope guards can inspect the authenticated key id, prefix environment, and key scopes.

LitestarAuthMiddleware plugs the authenticator into Litestar’s ASGI pipeline so connection.user and related auth state are populated from the same backend list your routes and guards use.

Signed API-key requests need pre-auth body buffering so the HMAC check can commit to the raw request body. Plugin-managed applications opt into that buffering automatically when the startup backend inventory includes an ApiKeyTransport. Direct integrations that construct LitestarAuthMiddleware outside the plugin fail closed by default: LitestarAuthMiddlewareConfig.api_key_backend_present defaults to False, so signed-body buffering is skipped unless you pass api_key_backend_present=True for a backend list that contains an ApiKeyTransport.

For diagrams, transport vs strategy tables, and plugin-oriented setup, see Backends: transports and strategies.

from litestar_auth.authentication import (
    AuthenticationBackend,
    Authenticator,
    LitestarAuthMiddleware,
)

# Typical wiring: build AuthenticationBackend(name, transport, strategy) instances,
# wrap them in Authenticator(backends, user_manager), then register LitestarAuthMiddleware.

litestar_auth.authentication

Authentication package.

AuthenticationBackend(*, name, transport, strategy)

Compose a transport and strategy into a reusable auth backend.

Store backend components used for auth flows.

Source code in litestar_auth/authentication/backend.py
def __init__(self, *, name: str, transport: TransportProtocol, strategy: StrategyProtocol[UP, ID]) -> None:
    """Store backend components used for auth flows."""
    self.name = name
    self.transport = transport
    self.strategy = strategy

authenticate(connection, user_manager) async

Resolve a user from the current request via transport and strategy.

Returns:

Type Description
UP | None

Authenticated user or None when no valid token is present.

Source code in litestar_auth/authentication/backend.py
async def authenticate(
    self,
    connection: ASGIConnection[Any, Any, Any, Any],
    user_manager: UserManagerProtocol[UP, ID],
) -> UP | None:
    """Resolve a user from the current request via transport and strategy.

    Returns:
        Authenticated user or ``None`` when no valid token is present.
    """
    result = await self.authenticate_with_context(connection, user_manager)
    return None if result is None else result[0]

authenticate_with_context(connection, user_manager) async

Resolve a user and request auth context from the current request.

Returns:

Type Description
tuple[UP, object] | None

Authenticated user plus request auth context, or None when no valid token is present.

Source code in litestar_auth/authentication/backend.py
async def authenticate_with_context(
    self,
    connection: ASGIConnection[Any, Any, Any, Any],
    user_manager: UserManagerProtocol[UP, ID],
) -> tuple[UP, object] | None:
    """Resolve a user and request auth context from the current request.

    Returns:
        Authenticated user plus request auth context, or ``None`` when no valid token is present.
    """
    token = await self.transport.read_token(connection)
    if isinstance(self.strategy, ContextualStrategy):
        contextual_strategy = cast(
            "ContextualStrategy[UP, ID, _AuthenticationResultWithContext[UP]]",
            self.strategy,
        )
        result = await contextual_strategy.read_token_with_context(token, user_manager)
        if result is None:
            return None
        return result.user, result.context

    user = await self.strategy.read_token(token, user_manager)
    if user is None:
        return None
    return user, self.name

login(user) async

Issue a token through the configured strategy and transport.

Returns:

Type Description
Response[Any]

Response mutated by the configured transport for login.

Source code in litestar_auth/authentication/backend.py
async def login(self, user: UP) -> Response[Any]:
    """Issue a token through the configured strategy and transport.

    Returns:
        Response mutated by the configured transport for login.
    """
    token = await self.strategy.write_token(user)
    return self.transport.set_login_token(Response(content=None), token)

logout(user, token) async

Invalidate a token and clear transport-managed state.

When the transport is a :class:CookieTransport, the refresh-token cookie is also expired so the browser does not retain it after logout.

Returns:

Type Description
Response[Any]

Response mutated by the configured transport for logout.

Raises:

Type Description
ClientException

When token revocation cannot be recorded (for example, an in-memory denylist at capacity), surfaced as HTTP 503 with the library error code in the JSON body.

Source code in litestar_auth/authentication/backend.py
async def logout(self, user: UP, token: str) -> Response[Any]:
    """Invalidate a token and clear transport-managed state.

    When the transport is a :class:`CookieTransport`, the refresh-token
    cookie is also expired so the browser does not retain it after logout.

    Returns:
        Response mutated by the configured transport for logout.

    Raises:
        ClientException: When token revocation cannot be recorded (for example, an
            in-memory denylist at capacity), surfaced as HTTP 503 with the library
            error ``code`` in the JSON body.
    """
    try:
        await self.strategy.destroy_token(token, user)
    except TokenError as exc:
        raise ClientException(
            status_code=503,
            detail=str(exc),
            extra={"code": exc.code},
        ) from exc
    response = self.transport.set_logout(Response(content=None))
    if isinstance(self.transport, CookieTransport):
        self.transport.clear_refresh_token(response)
    return response

terminate_session(connection, user) async

Terminate the current authenticated session for a connection.

This method orchestrates logout in one explicit place by reading the current transport token and delegating token invalidation plus transport cleanup to logout.

Returns:

Type Description
Response[Any]

Response mutated by the configured transport for logout.

Raises:

Type Description
NotAuthorizedException

If the current transport token is unavailable.

Source code in litestar_auth/authentication/backend.py
async def terminate_session(
    self,
    connection: ASGIConnection[Any, Any, Any, Any],
    user: UP,
) -> Response[Any]:
    """Terminate the current authenticated session for a connection.

    This method orchestrates logout in one explicit place by reading the
    current transport token and delegating token invalidation plus transport
    cleanup to ``logout``.

    Returns:
        Response mutated by the configured transport for logout.

    Raises:
        NotAuthorizedException: If the current transport token is unavailable.
    """
    read_token = self.transport.read_token
    if isinstance(self.transport, LogoutTokenReadable):
        read_token = self.transport.read_logout_token
    token = await read_token(connection)
    if token is None:
        msg = "Authentication credentials were not provided."
        raise NotAuthorizedException(detail=msg)
    await _invalidate_refresh_artifacts(self.strategy, user)
    return await self.logout(user, token)

with_session(session)

Return a backend whose strategy is rebound to the provided session when supported.

Source code in litestar_auth/authentication/backend.py
def with_session[S](self, session: S) -> AuthenticationBackend[UP, ID]:
    """Return a backend whose strategy is rebound to the provided session when supported."""
    bound_strategy = _bind_strategy_session(self.strategy, session)
    if bound_strategy is self.strategy:
        return self

    return type(self)(name=self.name, transport=self.transport, strategy=bound_strategy)

Authenticator(backends, user_manager)

Try configured authentication backends in order.

Store backends and the user manager used for token resolution.

Source code in litestar_auth/authentication/authenticator.py
def __init__(
    self,
    backends: list[AuthenticationBackend[UP, ID]],
    user_manager: UserManagerProtocol[UP, ID],
) -> None:
    """Store backends and the user manager used for token resolution."""
    self.backends = backends
    self.user_manager = user_manager

authenticate(connection) async

Return the first authenticated user and request auth context.

Returns:

Type Description
UP | None

Tuple of authenticated user and request auth context, or (None, None)

object | None

when no backend resolves the request.

Source code in litestar_auth/authentication/authenticator.py
async def authenticate(
    self,
    connection: ASGIConnection[Any, Any, Any, Any],
) -> tuple[UP | None, object | None]:
    """Return the first authenticated user and request auth context.

    Returns:
        Tuple of authenticated user and request auth context, or ``(None, None)``
        when no backend resolves the request.
    """
    for backend in self.backends:
        if callable(getattr(type(backend), "authenticate_with_context", None)):
            result = await backend.authenticate_with_context(connection, self.user_manager)
        else:
            user = await backend.authenticate(connection, self.user_manager)
            result = None if user is None else (user, backend.name)
        if result is not None:
            return result

    return None, None

LitestarAuthMiddleware(app, *, config=None, **options)

LitestarAuthMiddleware(app: ASGIApp, *, config: LitestarAuthMiddlewareConfig[UP, ID])
LitestarAuthMiddleware(app: ASGIApp, **options: Unpack[LitestarAuthMiddlewareOptions[UP, ID]])

Bases: AbstractAuthenticationMiddleware

Resolve request users through an authenticator built with the request-scoped DB session.

Initialize the middleware.

Parameters:

Name Type Description Default
app ASGIApp

ASGI app to wrap.

required
config LitestarAuthMiddlewareConfig[UP, ID] | None

Middleware runtime configuration.

None
**options Unpack[LitestarAuthMiddlewareOptions[UP, ID]]

Individual middleware settings. Do not combine with config.

{}

Raises:

Type Description
ValueError

If config and keyword options are combined.

Source code in litestar_auth/authentication/middleware.py
def __init__(
    self,
    app: ASGIApp,
    *,
    config: LitestarAuthMiddlewareConfig[UP, ID] | None = None,
    **options: Unpack[LitestarAuthMiddlewareOptions[UP, ID]],
) -> None:
    """Initialize the middleware.

    Args:
        app: ASGI app to wrap.
        config: Middleware runtime configuration.
        **options: Individual middleware settings. Do not combine with
            ``config``.

    Raises:
        ValueError: If ``config`` and keyword options are combined.
    """
    if config is not None and options:
        msg = "Pass either LitestarAuthMiddlewareConfig or keyword options, not both."
        raise ValueError(msg)
    settings = LitestarAuthMiddlewareConfig(**options) if config is None else config
    super().__init__(
        app=app,
        exclude=settings.exclude,
        exclude_from_auth_key=settings.exclude_from_auth_key,
        exclude_http_methods=settings.exclude_http_methods,
        scopes=settings.scopes,
    )
    self.get_request_session = settings.get_request_session
    self.authenticator_factory = settings.authenticator_factory
    self.auth_cookie_names = settings.auth_cookie_names
    self.api_key_use_rate_limit = settings.api_key_use_rate_limit
    self._api_key_backend_present = settings.api_key_backend_present
    self.api_key_signed_body_max_bytes = settings.api_key_signed_body_max_bytes
    self.api_key_signed_body_max_messages = settings.api_key_signed_body_max_messages
    self.superuser_role_name = normalize_superuser_role_name(settings.superuser_role_name)

__call__(scope, receive, send) async

Buffer signed request bodies before authentication so signatures cover raw bytes.

Source code in litestar_auth/authentication/middleware.py
@override
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
    """Buffer signed request bodies before authentication so signatures cover raw bytes."""
    buffered_signed_body = False
    if self._api_key_backend_present and _has_signed_api_key_authorization_header(scope.get("headers", [])):
        body, receive = await _buffer_body_for_signature(
            receive,
            max_body_bytes=self.api_key_signed_body_max_bytes,
            max_messages=self.api_key_signed_body_max_messages,
        )
        cast("dict[str, Any]", scope)[API_KEY_SIGNED_BODY_SCOPE_KEY] = body
        buffered_signed_body = True
    try:
        await super().__call__(scope, receive, send)
    finally:
        if buffered_signed_body:
            cast("dict[str, Any]", scope).pop(API_KEY_SIGNED_BODY_SCOPE_KEY, None)

authenticate_request(connection) async

Authenticate the request and return the resolved user or None.

Returns:

Type Description
AuthenticationResult

Authentication result containing the resolved user and backend name.

Source code in litestar_auth/authentication/middleware.py
@override
async def authenticate_request(
    self,
    connection: ASGIConnection[Any, Any, Any, Any],
) -> AuthenticationResult:
    """Authenticate the request and return the resolved user or ``None``.

    Returns:
        Authentication result containing the resolved user and backend name.
    """
    set_scope_superuser_role_name(connection.scope, self)
    session = self.get_request_session(connection.app.state, connection.scope)
    authenticator = self.authenticator_factory(session)
    user, auth_context = await authenticator.authenticate(connection)

    if user is not None:
        await _record_successful_api_key_use_if_applicable(
            connection,
            authenticator=authenticator,
            auth_context=auth_context,
        )

    if user is None and _request_supplied_auth_credentials(connection, auth_cookie_names=self.auth_cookie_names):
        await _raise_api_key_authentication_failure_if_applicable(
            connection,
            authenticator=authenticator,
            api_key_use_rate_limit=self.api_key_use_rate_limit,
        )
        logger.warning("Authentication token validation failed", extra={"event": "token_validation_failed"})
    return AuthenticationResult(user=user, auth=auth_context)

LitestarAuthMiddlewareConfig(get_request_session, authenticator_factory, auth_cookie_names=frozenset(), api_key_use_rate_limit=None, api_key_backend_present=False, api_key_signed_body_max_bytes=_DEFAULT_API_KEY_SIGNED_BODY_MAX_BYTES, api_key_signed_body_max_messages=_DEFAULT_API_KEY_SIGNED_BODY_MAX_MESSAGES, superuser_role_name=DEFAULT_SUPERUSER_ROLE_NAME, exclude=None, exclude_from_auth_key='exclude_from_auth', exclude_http_methods=None, scopes=None) dataclass

Configuration for :class:LitestarAuthMiddleware.