Skip to main content

Pico-Client-Auth

PyPI Ask DeepWiki License: MIT CI (tox matrix) codecov Quality Gate Status Duplicated Lines (%) Maintainability Rating PyPI Downloads Docs Interactive Lab

Pico-Client-Auth provides JWT authentication for pico-fastapi applications. It integrates with the pico-ioc container to deliver automatic Bearer token validation, a request-scoped SecurityContext, role-based access control, and JWKS key rotation support.

Requires Python 3.11+ (tested on 3.11, 3.12, 3.13 and 3.14) Built on pico-fastapi + pico-ioc Fully async-compatible Real JWKS-based token validation Auth by default with opt-out via @allow_anonymous


Why pico-client-auth?

Concern DIY Middleware pico-client-auth
Token validation Implement yourself Built-in with JWKS
Key rotation Manual handling Automatic on unknown kid
Security context request.state ad-hoc Typed SecurityContext with ContextVar
Role checking Scattered if/else @requires_role decorator
Configuration Hardcoded @configured from YAML/env
Testing Build your own fixtures RSA keypair + make_token pattern

Core Features

  • Auth by default on all routes
  • @allow_anonymous to opt out specific endpoints
  • @requires_role("admin") for declarative role-based authorization
  • @requires_group("team-id") for group-based access control
  • @requires_scope("treasury:*") for agent-identity authorization (dual-token, see below)
  • SecurityContext accessible from controllers, services, and any code within a request
  • JWKS fetch with TTL cache and automatic key rotation
  • Optional jti revocation denylist with bounded propagation (opt-in)
  • Extensible RoleResolver protocol
  • Fail-fast startup if issuer/audience are missing
  • Auto-discovered via pico_boot.modules entry point
  • Post-quantum ready: ML-DSA-65 / ML-DSA-87 signature verification (optional pqc extra)

Installation

pip install pico-client-auth

# With post-quantum (ML-DSA) support
pip install pico-client-auth[pqc]

Quick Example

# application.yaml
auth_client:
  issuer: https://auth.example.com
  audience: my-api
from pico_fastapi import controller, get
from pico_client_auth import SecurityContext, allow_anonymous, requires_role, requires_group

@controller(prefix="/api")
class ApiController:

    @get("/me")
    async def get_me(self):
        claims = SecurityContext.require()
        return {"sub": claims.sub, "email": claims.email}

    @get("/health")
    @allow_anonymous
    async def health(self):
        return {"status": "ok"}

    @get("/admin")
    @requires_role("admin")
    async def admin_panel(self):
        return {"admin": True}
from pico_boot import init
from pico_ioc import configuration, YamlTreeSource
from fastapi import FastAPI

config = configuration(YamlTreeSource("application.yaml"))
container = init(modules=["controllers"], config=config)
app = container.get(FastAPI)
# pico-client-auth is auto-discovered — all routes are now protected

Quick Example (without pico-boot)

from pico_ioc import init, configuration, YamlTreeSource
from fastapi import FastAPI

config = configuration(YamlTreeSource("application.yaml"))
container = init(
    modules=[
        "controllers",
        "pico_fastapi",
        "pico_client_auth",  # Required without pico-boot
    ],
    config=config,
)
app = container.get(FastAPI)

SecurityContext

Access authenticated user information from anywhere within a request:

from pico_client_auth import SecurityContext

# In controller, service, or repository
claims = SecurityContext.require()    # TokenClaims (raises if not auth'd)
claims = SecurityContext.get()         # TokenClaims | None
roles  = SecurityContext.get_roles()   # list[str]
SecurityContext.has_role("admin")      # bool
SecurityContext.require_role("admin")  # raises InsufficientPermissionsError
groups = SecurityContext.get_groups()  # tuple[str, ...]
SecurityContext.has_group("team-id")   # bool
SecurityContext.require_group("team")  # raises InsufficientPermissionsError

Agent Identity & Scopes (v0.4.2+)

A request can carry two tokens. Authorization: Bearer <service-token> proves which service is calling ( SecurityContext). X-Agent-Authorization: Bearer <agent-token> proves which LLM agent is acting, on behalf of which user, with which scopes ( AgentContext). Both are validated through the same TokenValidator/JWKS.

Gate an endpoint on agent scopes with @requires_scope — scope matching is a :-segmented glob, so treasury:* matches treasury:write:budget:opex:

from pico_client_auth import requires_scope, AgentContext

@app.get("/treasury/payments")
@requires_role("treasury-service")    # service identity (Authorization)
@requires_scope("treasury:write:*")   # agent identity (X-Agent-Authorization)
async def make_payment():
    agent = AgentContext.get()        # AgentClaims | None
    return {"agent": agent.sub, "user": agent.user_id, "scopes": agent.scopes}
  • An endpoint with @requires_scope returns 401 if the agent header is missing and 403 if its scopes don't satisfy the requirement.
  • Endpoints without @requires_scope ignore the agent header (it's optional; AgentContext is still populated if a valid one is present).

Revocation denylist (opt-in)

Set revocation_endpoint to have the validator reject tokens whose jti is on the issuer's denylist. The cache is polled every revocation_ttl_seconds (default 15s — the worst-case window between an operator revoking and validators rejecting). Empty endpoint (default) = signature-only validation. JWKS rotation remains the instant-kill path.

If a poll fails, tokens carrying a jti are rejected: a denylist that cannot be reached means revocation status cannot be confirmed. Set revocation_fail_open: true to accept them instead, trading prompt revocation for availability.

Token Validation Hardening (v0.6.0+)

Three rules the validator enforces unconditionally, independent of configuration:

  • exp is mandatory. A token without an expiry is rejected rather than treated as non-expiring. nbf is enforced when present.
  • Symmetric HS* algorithms are rejected, even if listed in accepted_algorithms. This path verifies with public keys, so an HS256 token implies an RS/HS confusion attack — signing with the public key as the HMAC secret.
  • JWKS and revocation endpoints must be https. Plain http is permitted only for localhost, 127.0.0.1 and ::1, so development still works.

Custom Role Resolver

Override how roles are extracted from tokens:

from pico_ioc import component
from pico_client_auth import RoleResolver, TokenClaims

@component
class MyRoleResolver:
    async def resolve(self, claims: TokenClaims, raw_claims: dict) -> list[str]:
        return raw_claims.get("roles", [])

Custom Key Source (v0.7.0+)

By default keys are fetched from the issuer's JWKS endpoint over HTTPS. Register your own component under the JWKSClient key to source them elsewhere — an auth server validating its own tokens, for instance, can read them straight from its signer instead of calling itself over the network:

from pico_ioc import component
from pico_client_auth import JWKSClient

@component(name=JWKSClient, primary=True)
class LocalJWKSProvider:
    def __init__(self, signer: MySigner):
        self._keys = {k["kid"]: k for k in signer.jwks()["keys"]}

    async def get_key(self, kid: str) -> dict:
        return self._keys[kid]

No subclassing: JWKSClient is the container key, and any component providing async get_key(kid) -> dict satisfies it. Raising KeyError for an unknown kid is what signals "no such key" to the validator.


Configuration

Key Default Description
auth_client.enabled true Enable/disable auth middleware
auth_client.issuer "" Expected JWT issuer (iss claim)
auth_client.audience "" Expected JWT audience (aud claim)
auth_client.jwks_ttl_seconds 300 JWKS cache TTL in seconds
auth_client.jwks_endpoint "" JWKS URL (default: {issuer}/api/v1/auth/jwks)
auth_client.accepted_algorithms ["RS256"] List of accepted JWT signing algorithms
auth_client.revocation_endpoint "" jti denylist URL to poll. Empty = revocation disabled
auth_client.revocation_ttl_seconds 15 Poll interval / worst-case revokereject window
auth_client.revocation_bearer "" Optional bearer token for the revocation endpoint
auth_client.revocation_fail_open false Accept tokens when the denylist fetch fails instead of rejecting them

Testing

from pico_client_auth import SecurityContext, TokenClaims
from pico_client_auth.errors import MissingTokenError

def test_require_raises_when_empty():
    SecurityContext.clear()
    with pytest.raises(MissingTokenError):
        SecurityContext.require()

def test_authenticated_flow():
    claims = TokenClaims(sub="u1", email="a@b.com", role="admin",
                         org_id="o1", jti="j1")
    SecurityContext.set(claims, ["admin"])
    assert SecurityContext.require().sub == "u1"
    assert SecurityContext.has_role("admin")
    SecurityContext.clear()

For full e2e testing with mock JWKS and signed tokens, see the Testing Guide.


Post-Quantum (ML-DSA) Support

pico-client-auth supports ML-DSA-65 (NIST Level 3) and ML-DSA-87 (NIST Level 5) post-quantum signature verification via the optional pqc extra.

auth_client:
  issuer: https://auth.example.com
  audience: my-api
  accepted_algorithms:
    - RS256
    - ML-DSA-65

ML-DSA tokens use the draft-ietf-cose-dilithium JOSE standard:

  • kty: "AKP" (Algorithm Key Pair)
  • alg: "ML-DSA-65" or "ML-DSA-87"
  • pub: base64url-encoded raw public key

Requires liboqs-python (installed automatically with pip install pico-client-auth[pqc]). When liboqs is not installed, ML-DSA tokens are rejected with AuthConfigurationError.


How It Works

  • AuthFastapiConfigurer (priority=10) registers as an inner middleware
  • Every request: extract Bearer token validate JWT via JWKS resolve roles populate SecurityContext
  • Algorithm dispatch: RS256 tokens use PyJWT, ML-DSA tokens use liboqs
  • @allow_anonymous endpoints skip validation entirely
  • @requires_role endpoints check resolved roles, return 403 if missing
  • @requires_group endpoints check group membership, return 403 if missing
  • SecurityContext is cleared in finally — no leakage between requests

Built for AI-assisted development

pico-client-auth is part of an ecosystem designed for humans and coding agents building software together. Every package ships AGENTS.md working conventions, an llms.txt machine-readable docs index and documented behaviour pinned by regression tests; pico-testing gives agents a verification loop for their own changes, and releases are gated by the whole ecosystem booting together against real infrastructure. The full story: Built for AI-assisted development.

Install the agent skills for Claude Code or OpenAI Codex:

curl -sL https://raw.githubusercontent.com/dperezcabrera/pico-skills/main/install.sh | bash

The pico-conventions skill teaches the assistant this module's API surface and invariants; /add-component and /add-tests scaffold components and tests that use it.

License

MIT

Metadata

Release files for pico-client-auth 0.7.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pico-client-auth 0.7.1
File Size Uploaded
pico_client_auth-0.7.1.tar.gz 96.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pico-client-auth 0.7.1
File Interpreter ABI Platform
pico_client_auth-0.7.1-py3-none-any.whl Python 3 none any Details

Total release size: 123.4 kB

Release files / pico_client_auth-0.7.1.tar.gz

Download URL pico_client_auth-0.7.1.tar.gz
Size 96.0 kB
Tags Source
SHA-256 checksum
How to use checksums
2f115eff174910593fd83b3ac1ebf92abdf6fb5f803f53f86c2b3cc646856256
BLAKE2b-256 checksum
How to use checksums
60c632f438deb8a9d439697ad7807e5aa8aa2230b27b0ace46495db0e491db22
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / pico_client_auth-0.7.1-py3-none-any.whl

Download URL pico_client_auth-0.7.1-py3-none-any.whl
Size 27.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
93c27b27f7da4e0ef460aa1f244d9bec25ef35d36fa01076861bb330617844aa
BLAKE2b-256 checksum
How to use checksums
be24def0620fde2b81cc8636ef0493583258d8b61b78b6ff422e5b688a85ffbf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.1 This release

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page