Skip to content

API Reference

Complete reference documentation for pico-client-auth's public API.

Module: pico_client_auth

Decorators

Decorator Description
@allow_anonymous Skip authentication for an endpoint
@requires_role(*roles) Require at least one of the specified roles

Classes

Class Description
SecurityContext Static accessor for authenticated claims and roles
TokenClaims Frozen dataclass with JWT claim fields
RoleResolver Protocol for custom role extraction
AuthClientSettings Configuration dataclass for auth settings

Exceptions

Exception Description
AuthClientError Base exception for all pico-client-auth errors
MissingTokenError No Bearer token found in request
TokenExpiredError JWT token has expired
TokenInvalidError JWT is malformed or has invalid signature
InsufficientPermissionsError User lacks required role(s)
AuthConfigurationError Auth configuration is missing or invalid

Decorator Reference

@allow_anonymous

Marks an endpoint as publicly accessible without authentication.

@allow_anonymous
async def my_handler(): ...

Sets _pico_allow_anonymous = True on the function. The auth middleware checks this attribute before validating the token.


@requires_role

Requires the authenticated user to have at least one of the specified roles.

@requires_role(*roles: str)

Parameters:

Parameter Type Description
*roles str One or more role names (user must have at least one)

Example:

@requires_role("admin")
async def admin_only(): ...

@requires_role("editor", "admin")
async def editor_or_admin(): ...

Sets _pico_required_roles = frozenset(roles) on the function. The middleware checks this after token validation.


Class Reference

SecurityContext

Static class for accessing authenticated user state within a request.

Method Returns Raises Description
get() TokenClaims \| None -- Current claims or None
require() TokenClaims MissingTokenError Current claims (must exist)
get_roles() list[str] -- Resolved roles (returns copy)
has_role(role) bool -- Check if user has role
require_role(*roles) None InsufficientPermissionsError Assert at least one role
set(claims, roles) None -- Internal: populate context
clear() None -- Internal: clear context

TokenClaims

Immutable dataclass representing essential JWT claims.

@dataclass(frozen=True)
class TokenClaims:
    sub: str       # Subject (user ID)
    email: str     # User email
    role: str      # Primary role claim
    org_id: str    # Organisation ID
    jti: str       # JWT ID

RoleResolver (Protocol)

Protocol for custom role extraction from JWT claims.

@runtime_checkable
class RoleResolver(Protocol):
    async def resolve(self, claims: TokenClaims, raw_claims: dict) -> list[str]: ...

The default implementation (DefaultRoleResolver) returns [claims.role] if non-empty, else [].

Override by registering a @component that satisfies this protocol.


AuthClientSettings

Configuration dataclass loaded from the auth_client config prefix.

@configured(target="self", prefix="auth_client", mapping="tree")
@dataclass
class AuthClientSettings:
    enabled: bool = True
    issuer: str = ""
    audience: str = ""
    jwks_ttl_seconds: int = 300
    jwks_endpoint: str = ""
    accepted_algorithms: tuple[str, ...] = ("RS256",)
Field Type Default Description
enabled bool True Enable/disable auth middleware
issuer str "" Expected JWT issuer
audience str "" Expected JWT audience
jwks_ttl_seconds int 300 JWKS cache TTL (seconds)
jwks_endpoint str "" JWKS URL (default: {issuer}/api/v1/auth/jwks)
accepted_algorithms tuple[str, ...] ("RS256",) Accepted JWT signing algorithms (e.g. RS256, ML-DSA-65, ML-DSA-87)

Exception Reference

AuthClientError

Base exception for all pico-client-auth errors.

try:
    # pico-client-auth operations
except AuthClientError as e:
    # Handle any auth error

MissingTokenError

Raised when no Bearer token is found in the Authorization header, or when SecurityContext.require() is called without an authenticated context.

TokenExpiredError

Raised when the JWT exp claim indicates the token has expired.

TokenInvalidError

Raised when the JWT is malformed, has an invalid signature, wrong issuer, or wrong audience.

InsufficientPermissionsError

Raised when SecurityContext.require_role() is called and the user lacks all specified roles.

AuthConfigurationError

Raised at startup if auth_client.enabled is True but issuer or audience are empty.


Auto-generated API

pico_client_auth

pico-client-auth: JWT authentication client for pico-fastapi.

Provides automatic Bearer token validation, a request-scoped SecurityContext, role-based access control decorators, and JWKS key rotation support.

Public API

Models: TokenClaims Context: SecurityContext Decorators: allow_anonymous, requires_role Protocols: RoleResolver Configuration: AuthClientSettings Errors: AuthClientError, MissingTokenError, TokenExpiredError, TokenInvalidError, InsufficientPermissionsError, AuthConfigurationError

AuthClientSettings dataclass

Type-safe settings for the auth client, loaded from configuration sources.

Populated automatically from configuration sources using the auth_client prefix via pico-ioc's @configured decorator.

Attributes:

Name Type Description
enabled bool

Whether authentication middleware is active.

issuer str

Expected JWT issuer (iss claim).

audience str

Expected JWT audience (aud claim).

jwks_ttl_seconds int

How long to cache the JWKS key set (seconds).

jwks_endpoint str

URL to fetch JWKS from. Defaults to {issuer}/api/v1/auth/jwks.

Source code in src/pico_client_auth/config.py
@configured(target="self", prefix="auth_client", mapping="tree")
@dataclass
class AuthClientSettings:
    """Type-safe settings for the auth client, loaded from configuration sources.

    Populated automatically from configuration sources using the ``auth_client``
    prefix via pico-ioc's ``@configured`` decorator.

    Attributes:
        enabled: Whether authentication middleware is active.
        issuer: Expected JWT issuer (``iss`` claim).
        audience: Expected JWT audience (``aud`` claim).
        jwks_ttl_seconds: How long to cache the JWKS key set (seconds).
        jwks_endpoint: URL to fetch JWKS from. Defaults to ``{issuer}/api/v1/auth/jwks``.
    """

    enabled: bool = True
    issuer: str = ""
    audience: str = ""
    jwks_ttl_seconds: int = 300
    jwks_endpoint: str = ""
    accepted_algorithms: tuple[str, ...] = ("RS256",)

AuthClientError

Bases: Exception

Base error for all pico-client-auth exceptions.

Source code in src/pico_client_auth/errors.py
class AuthClientError(Exception):
    """Base error for all pico-client-auth exceptions."""

AuthConfigurationError

Bases: AuthClientError

Authentication configuration is missing or invalid.

Source code in src/pico_client_auth/errors.py
class AuthConfigurationError(AuthClientError):
    """Authentication configuration is missing or invalid."""

InsufficientPermissionsError

Bases: AuthClientError

The authenticated user lacks the required role(s).

Source code in src/pico_client_auth/errors.py
class InsufficientPermissionsError(AuthClientError):
    """The authenticated user lacks the required role(s)."""

MissingTokenError

Bases: AuthClientError

No Bearer token found in the Authorization header.

Source code in src/pico_client_auth/errors.py
class MissingTokenError(AuthClientError):
    """No Bearer token found in the Authorization header."""

TokenExpiredError

Bases: AuthClientError

The JWT token has expired.

Source code in src/pico_client_auth/errors.py
class TokenExpiredError(AuthClientError):
    """The JWT token has expired."""

TokenInvalidError

Bases: AuthClientError

The JWT token is malformed or has an invalid signature.

Source code in src/pico_client_auth/errors.py
class TokenInvalidError(AuthClientError):
    """The JWT token is malformed or has an invalid signature."""

TokenClaims dataclass

Immutable representation of the essential JWT claims.

Attributes:

Name Type Description
sub str

Subject identifier (user ID).

email str

User email address.

role str

Primary role claim from the token.

org_id str

Organisation identifier.

jti str

Unique token identifier (JWT ID).

Source code in src/pico_client_auth/models.py
@dataclass(frozen=True)
class TokenClaims:
    """Immutable representation of the essential JWT claims.

    Attributes:
        sub: Subject identifier (user ID).
        email: User email address.
        role: Primary role claim from the token.
        org_id: Organisation identifier.
        jti: Unique token identifier (JWT ID).
    """

    sub: str
    email: str
    role: str
    org_id: str
    jti: str
    groups: tuple[str, ...] = ()

RoleResolver

Bases: Protocol

Protocol for resolving user roles from JWT claims.

Implement this protocol to customise how roles are extracted (e.g. from a roles array claim, from an external service, etc.). Register your implementation as a @component and it will automatically replace the default resolver.

Source code in src/pico_client_auth/role_resolver.py
@runtime_checkable
class RoleResolver(Protocol):
    """Protocol for resolving user roles from JWT claims.

    Implement this protocol to customise how roles are extracted
    (e.g. from a ``roles`` array claim, from an external service, etc.).
    Register your implementation as a ``@component`` and it will
    automatically replace the default resolver.
    """

    async def resolve(self, claims: TokenClaims, raw_claims: dict) -> list[str]: ...

SecurityContext

Singleton-style accessor for the current request's authentication state.

All methods are static; the underlying storage uses ContextVar so each async task / thread has its own isolated copy.

Source code in src/pico_client_auth/security_context.py
class SecurityContext:
    """Singleton-style accessor for the current request's authentication state.

    All methods are static; the underlying storage uses ``ContextVar`` so
    each async task / thread has its own isolated copy.
    """

    @staticmethod
    def get() -> Optional[TokenClaims]:
        """Return the current claims, or ``None`` if not authenticated."""
        return _claims_var.get()

    @staticmethod
    def require() -> TokenClaims:
        """Return the current claims, raising if not authenticated.

        Raises:
            MissingTokenError: If no authentication context is set.
        """
        claims = _claims_var.get()
        if claims is None:
            raise MissingTokenError("No authenticated user in SecurityContext")
        return claims

    @staticmethod
    def get_roles() -> list[str]:
        """Return the resolved roles for the current request."""
        return list(_roles_var.get())

    @staticmethod
    def has_role(role: str) -> bool:
        """Check whether the current user has the given role."""
        return role in _roles_var.get()

    @staticmethod
    def require_role(*roles: str) -> None:
        """Assert that the current user has at least one of the given roles.

        Raises:
            InsufficientPermissionsError: If none of the roles match.
        """
        current = set(_roles_var.get())
        if not current.intersection(roles):
            raise InsufficientPermissionsError(f"Required one of {roles}, but user has {sorted(current)}")

    @staticmethod
    def get_groups() -> tuple[str, ...]:
        """Return the group IDs for the current request."""
        return _groups_var.get()

    @staticmethod
    def has_group(group_id: str) -> bool:
        """Check whether the current user belongs to the given group."""
        return group_id in _groups_var.get()

    @staticmethod
    def require_group(*group_ids: str) -> None:
        """Assert that the current user belongs to at least one of the given groups.

        Raises:
            InsufficientPermissionsError: If none of the groups match.
        """
        current = set(_groups_var.get())
        if not current.intersection(group_ids):
            raise InsufficientPermissionsError(f"Required one of groups {group_ids}, but user has {sorted(current)}")

    @staticmethod
    def set(claims: TokenClaims, roles: list[str]) -> None:
        """Populate the security context (called by the middleware)."""
        _claims_var.set(claims)
        _roles_var.set(roles)
        _groups_var.set(claims.groups)

    @staticmethod
    def clear() -> None:
        """Clear the security context (called by the middleware in ``finally``)."""
        _claims_var.set(None)
        _roles_var.set([])
        _groups_var.set(())

get() staticmethod

Return the current claims, or None if not authenticated.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def get() -> Optional[TokenClaims]:
    """Return the current claims, or ``None`` if not authenticated."""
    return _claims_var.get()

require() staticmethod

Return the current claims, raising if not authenticated.

Raises:

Type Description
MissingTokenError

If no authentication context is set.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def require() -> TokenClaims:
    """Return the current claims, raising if not authenticated.

    Raises:
        MissingTokenError: If no authentication context is set.
    """
    claims = _claims_var.get()
    if claims is None:
        raise MissingTokenError("No authenticated user in SecurityContext")
    return claims

get_roles() staticmethod

Return the resolved roles for the current request.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def get_roles() -> list[str]:
    """Return the resolved roles for the current request."""
    return list(_roles_var.get())

has_role(role) staticmethod

Check whether the current user has the given role.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def has_role(role: str) -> bool:
    """Check whether the current user has the given role."""
    return role in _roles_var.get()

require_role(*roles) staticmethod

Assert that the current user has at least one of the given roles.

Raises:

Type Description
InsufficientPermissionsError

If none of the roles match.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def require_role(*roles: str) -> None:
    """Assert that the current user has at least one of the given roles.

    Raises:
        InsufficientPermissionsError: If none of the roles match.
    """
    current = set(_roles_var.get())
    if not current.intersection(roles):
        raise InsufficientPermissionsError(f"Required one of {roles}, but user has {sorted(current)}")

get_groups() staticmethod

Return the group IDs for the current request.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def get_groups() -> tuple[str, ...]:
    """Return the group IDs for the current request."""
    return _groups_var.get()

has_group(group_id) staticmethod

Check whether the current user belongs to the given group.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def has_group(group_id: str) -> bool:
    """Check whether the current user belongs to the given group."""
    return group_id in _groups_var.get()

require_group(*group_ids) staticmethod

Assert that the current user belongs to at least one of the given groups.

Raises:

Type Description
InsufficientPermissionsError

If none of the groups match.

Source code in src/pico_client_auth/security_context.py
@staticmethod
def require_group(*group_ids: str) -> None:
    """Assert that the current user belongs to at least one of the given groups.

    Raises:
        InsufficientPermissionsError: If none of the groups match.
    """
    current = set(_groups_var.get())
    if not current.intersection(group_ids):
        raise InsufficientPermissionsError(f"Required one of groups {group_ids}, but user has {sorted(current)}")

set(claims, roles) staticmethod

Populate the security context (called by the middleware).

Source code in src/pico_client_auth/security_context.py
@staticmethod
def set(claims: TokenClaims, roles: list[str]) -> None:
    """Populate the security context (called by the middleware)."""
    _claims_var.set(claims)
    _roles_var.set(roles)
    _groups_var.set(claims.groups)

clear() staticmethod

Clear the security context (called by the middleware in finally).

Source code in src/pico_client_auth/security_context.py
@staticmethod
def clear() -> None:
    """Clear the security context (called by the middleware in ``finally``)."""
    _claims_var.set(None)
    _roles_var.set([])
    _groups_var.set(())

allow_anonymous(fn)

Mark an endpoint as accessible without authentication.

When applied to a controller method, the auth middleware will skip token validation for that route.

Source code in src/pico_client_auth/decorators.py
def allow_anonymous(fn: F) -> F:
    """Mark an endpoint as accessible without authentication.

    When applied to a controller method, the auth middleware will skip
    token validation for that route.
    """
    setattr(fn, PICO_ALLOW_ANONYMOUS, True)
    return fn

requires_group(*group_ids)

Require the authenticated user to belong to at least one of the specified groups.

Parameters:

Name Type Description Default
*group_ids str

One or more group IDs. The user must belong to at least one.

()

Returns:

Type Description
Callable[[F], F]

A decorator that attaches group metadata to the endpoint.

Source code in src/pico_client_auth/decorators.py
def requires_group(*group_ids: str) -> Callable[[F], F]:
    """Require the authenticated user to belong to at least one of the specified groups.

    Args:
        *group_ids: One or more group IDs. The user must belong to at least one.

    Returns:
        A decorator that attaches group metadata to the endpoint.
    """

    def decorator(fn: F) -> F:
        setattr(fn, PICO_REQUIRED_GROUPS, frozenset(group_ids))
        return fn

    return decorator

requires_role(*roles)

Require the authenticated user to have at least one of the specified roles.

Parameters:

Name Type Description Default
*roles str

One or more role names. The user must have at least one.

()

Returns:

Type Description
Callable[[F], F]

A decorator that attaches role metadata to the endpoint.

Source code in src/pico_client_auth/decorators.py
def requires_role(*roles: str) -> Callable[[F], F]:
    """Require the authenticated user to have at least one of the specified roles.

    Args:
        *roles: One or more role names. The user must have at least one.

    Returns:
        A decorator that attaches role metadata to the endpoint.
    """

    def decorator(fn: F) -> F:
        setattr(fn, PICO_REQUIRED_ROLES, frozenset(roles))
        return fn

    return decorator