User Guide¶
This guide covers the core concepts and features of pico-client-auth in depth.
Contents¶
| Section | Description |
|---|---|
| SecurityContext | Accessing authenticated user claims and roles |
| Role Resolver | Customizing how roles are extracted from tokens |
Quick Reference¶
Decorators¶
from pico_client_auth import allow_anonymous, requires_role
@allow_anonymous # Skip auth entirely
async def public(): ...
@requires_role("admin") # Require a specific role
async def admin(): ...
@requires_role("editor", "admin") # Any of these roles
async def edit(): ...
SecurityContext¶
from pico_client_auth import SecurityContext
claims = SecurityContext.get() # TokenClaims | None
claims = SecurityContext.require() # TokenClaims (raises if None)
roles = SecurityContext.get_roles() # list[str]
ok = SecurityContext.has_role("x") # bool
SecurityContext.require_role("x", "y") # raises InsufficientPermissionsError
TokenClaims¶
from pico_client_auth import TokenClaims
# Frozen dataclass with these fields:
# sub: str - Subject (user ID)
# email: str - User email
# role: str - Primary role claim
# org_id: str - Organisation ID
# jti: str - JWT ID
Configuration¶
auth_client:
enabled: true # default
issuer: https://auth.example.com
audience: my-api
jwks_ttl_seconds: 300 # default
jwks_endpoint: "" # auto: {issuer}/api/v1/auth/jwks
Authentication Flow¶
Request arrives
|
v
Has @allow_anonymous?
/ \
Yes No
| |
v v
Skip auth Extract Bearer token
| |
v v
call_next TokenValidator.validate()
|
v
RoleResolver.resolve()
|
v
SecurityContext.set()
|
v
Has @requires_role?
/ \
Yes No
| |
v v
User has role? call_next
/ \
Yes No
| |
v v
call_next 403 Forbidden
Best Practices¶
1. Use @allow_anonymous Sparingly¶
Auth-by-default is a safety net. Only mark endpoints as anonymous when genuinely public:
# Good - health checks, public info
@get("/health")
@allow_anonymous
async def health(self): ...
# Avoid - accidentally exposing sensitive data
@get("/users")
@allow_anonymous # Are you sure?
async def list_users(self): ...
2. Access Claims via SecurityContext, Not Request¶
# Good - works everywhere (controllers, services, repositories)
claims = SecurityContext.require()
# Avoid - couples your code to FastAPI's Request object
claims = request.state.claims
3. Use @requires_role for Authorization¶
# Good - declarative, checked by middleware
@requires_role("admin")
async def admin_panel(self): ...
# Avoid - imperative checks in handler
async def admin_panel(self):
if not SecurityContext.has_role("admin"):
raise HTTPException(403)
4. Override RoleResolver for Complex Scenarios¶
If your tokens have a roles array instead of a single role string, provide a custom RoleResolver: