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+ Built on pico-fastapi + pico-ioc Fully async-compatible Real JWKS-based token validation Auth by default with opt-out via
@allow_anonymous
| 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 |
- Auth by default on all routes
@allow_anonymousto 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)SecurityContextaccessible from controllers, services, and any code within a request- JWKS fetch with TTL cache and automatic key rotation
- Optional
jtirevocation denylist with bounded propagation (opt-in) - Extensible
RoleResolverprotocol - Fail-fast startup if issuer/audience are missing
- Auto-discovered via
pico_boot.modulesentry point - Post-quantum ready: ML-DSA-65 / ML-DSA-87 signature verification (optional
pqcextra)
pip install pico-client-auth
# With post-quantum (ML-DSA) support
pip install pico-client-auth[pqc]# application.yaml
auth_client:
issuer: https://auth.example.com
audience: my-apifrom 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 protectedfrom 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)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 InsufficientPermissionsErrorA 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_scopereturns 401 if the agent header is missing and 403 if its scopes don't satisfy the requirement. - Endpoints without
@requires_scopeignore the agent header (it's optional;AgentContextis still populated if a valid one is present).
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.
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", [])| 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 |
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.
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-65ML-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.
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_anonymousendpoints skip validation entirely@requires_roleendpoints check resolved roles, return 403 if missing@requires_groupendpoints check group membership, return 403 if missing- SecurityContext is cleared in
finally— no leakage between requests
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 | bashThe 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.
MIT