JWT authentication client for pico-fastapi. Provides automatic Bearer token validation, SecurityContext, role-based access control, and JWKS key rotation.
Project description
Pico-Client-Auth
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
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_anonymousto opt out specific endpoints@requires_role("admin")for declarative authorizationSecurityContextaccessible from controllers, services, and any code within a request- JWKS fetch with TTL cache and automatic key rotation
- Extensible
RoleResolverprotocol - Fail-fast startup if issuer/audience are missing
- Auto-discovered via
pico_boot.modulesentry point
Installation
pip install pico-client-auth
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
@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
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", [])
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) |
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.
How It Works
AuthFastapiConfigurer(priority=10) registers as an inner middleware- Every request: extract Bearer token → validate JWT via JWKS → resolve roles → populate SecurityContext
@allow_anonymousendpoints skip validation entirely@requires_roleendpoints check resolved roles, return 403 if missing- SecurityContext is cleared in
finally— no leakage between requests
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pico_client_auth-0.2.1.tar.gz.
File metadata
- Download URL: pico_client_auth-0.2.1.tar.gz
- Upload date:
- Size: 46.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
347aaba59115c4e26c8536c9f392d24fd57ecd1b703548794f153e88e750d835
|
|
| MD5 |
e127bcb455efa8a87ddbc6c83dee81d1
|
|
| BLAKE2b-256 |
be072d6caec7625b1b72bd01743810844d26a0c5fc570451b203e5ba775b1197
|
Provenance
The following attestation bundles were made for pico_client_auth-0.2.1.tar.gz:
Publisher:
publish-to-pypi.yml on dperezcabrera/pico-client-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pico_client_auth-0.2.1.tar.gz -
Subject digest:
347aaba59115c4e26c8536c9f392d24fd57ecd1b703548794f153e88e750d835 - Sigstore transparency entry: 974908938
- Sigstore integration time:
-
Permalink:
dperezcabrera/pico-client-auth@b1a094c6200988c16dc0feccd5e82f7ed7cd68da -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/dperezcabrera
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@b1a094c6200988c16dc0feccd5e82f7ed7cd68da -
Trigger Event:
release
-
Statement type:
File details
Details for the file pico_client_auth-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pico_client_auth-0.2.1-py3-none-any.whl
- Upload date:
- Size: 13.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
98d0612edafd03a4e99872e5f323fcb9914385fdd13111c14dae1f785dc22a82
|
|
| MD5 |
87f56b61a0fd04e88cc2b09503842d23
|
|
| BLAKE2b-256 |
9e6000bb6891c72fa3a0ed6221cb485c8039345ce0d9bfec942ae8142eaedcc2
|
Provenance
The following attestation bundles were made for pico_client_auth-0.2.1-py3-none-any.whl:
Publisher:
publish-to-pypi.yml on dperezcabrera/pico-client-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pico_client_auth-0.2.1-py3-none-any.whl -
Subject digest:
98d0612edafd03a4e99872e5f323fcb9914385fdd13111c14dae1f785dc22a82 - Sigstore transparency entry: 974909000
- Sigstore integration time:
-
Permalink:
dperezcabrera/pico-client-auth@b1a094c6200988c16dc0feccd5e82f7ed7cd68da -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/dperezcabrera
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@b1a094c6200988c16dc0feccd5e82f7ed7cd68da -
Trigger Event:
release
-
Statement type: