Security audit workflow - vulnerability scan β verification
npx skills add dperezcabrera/pico-skills --skill "add-auth"
Install specific skill from multi-skill repository
# Description
Add JWT authentication to pico-fastapi controllers. Use when protecting endpoints, adding role-based access control, implementing custom role resolvers, or configuring SecurityContext.
# SKILL.md
name: add-auth
description: Add JWT authentication to pico-fastapi controllers. Use when protecting endpoints, adding role-based access control, implementing custom role resolvers, or configuring SecurityContext.
argument-hint: [endpoint or role resolver name]
allowed-tools: Read Grep Glob Write Edit
Add Authentication
Add JWT authentication for: $ARGUMENTS
Read the codebase to understand existing controllers and patterns, then add authentication following these templates.
Protect All Routes (Default)
pico-client-auth protects all routes by default. Just add the dependency and configure:
# application.yaml
auth_client:
issuer: https://auth.example.com
audience: my-api
from pico_boot import init
from pico_ioc import configuration, YamlTreeSource
from fastapi import FastAPI
config = configuration(YamlTreeSource("application.yaml"))
container = init(modules=["myapp"], config=config)
app = container.get(FastAPI)
# All routes are now protected β pico-client-auth is auto-discovered
Public Endpoint
from pico_fastapi import controller, get
from pico_client_auth import allow_anonymous
@controller(prefix="/api")
class HealthController:
@get("/health")
@allow_anonymous
async def health(self):
return {"status": "ok"}
Role-Protected Endpoint
from pico_fastapi import controller, get, post
from pico_client_auth import SecurityContext, requires_role
@controller(prefix="/api/admin", tags=["admin"])
class AdminController:
def __init__(self, service: AdminService):
self.service = service
@get("/users")
@requires_role("admin")
async def list_users(self):
return await self.service.list_users()
@post("/users/{user_id}/ban")
@requires_role("admin", "moderator")
async def ban_user(self, user_id: str):
return await self.service.ban(user_id)
Group-Protected Endpoint
from pico_fastapi import controller, get
from pico_client_auth import requires_group, SecurityContext
@controller(prefix="/api/projects", tags=["projects"])
class ProjectController:
def __init__(self, service: ProjectService):
self.service = service
@get("/{project_id}")
@requires_group("project-alpha", "project-beta")
async def get_project(self, project_id: str):
return await self.service.get(project_id)
Access Claims in Services
SecurityContext works anywhere within a request β controllers, services, repositories:
from pico_ioc import component
from pico_client_auth import SecurityContext
@component
class AuditService:
async def log_action(self, action: str):
claims = SecurityContext.require()
print(f"[{claims.sub}] {action}")
async def get_current_org(self) -> str:
return SecurityContext.require().org_id
async def check_group_access(self, group_id: str) -> bool:
return SecurityContext.has_group(group_id)
async def get_user_groups(self) -> tuple[str, ...]:
return SecurityContext.get_groups()
Custom Role Resolver (Roles Array)
Override when tokens use a roles array instead of a single role string:
from pico_ioc import component
from pico_client_auth import RoleResolver, TokenClaims
@component
class ArrayRoleResolver:
async def resolve(self, claims: TokenClaims, raw_claims: dict) -> list[str]:
return raw_claims.get("roles", [])
Database Role Resolver with TTL Cache
When roles are stored in a database:
import time
from pico_ioc import component
from pico_client_auth import RoleResolver, TokenClaims
@component
class DatabaseRoleResolver:
def __init__(self, role_repository: RoleRepository):
self._repo = role_repository
self._cache: dict[str, tuple[float, list[str]]] = {}
self._ttl = 300 # 5 minutes
async def resolve(self, claims: TokenClaims, raw_claims: dict) -> list[str]:
cached = self._cache.get(claims.sub)
if cached and (time.monotonic() - cached[0]) < self._ttl:
return cached[1]
roles = await self._repo.find_roles_by_user(claims.sub)
self._cache[claims.sub] = (time.monotonic(), roles)
return roles
Keycloak Realm Roles
@component
class KeycloakRoleResolver:
async def resolve(self, claims: TokenClaims, raw_claims: dict) -> list[str]:
realm_access = raw_claims.get("realm_access", {})
return realm_access.get("roles", [])
Disable Auth for Development
auth_client:
enabled: false
Post-Quantum (ML-DSA) Authentication
Enable ML-DSA-65 / ML-DSA-87 post-quantum JWT verification:
pip install pico-client-auth[pqc]
# application.yaml
auth_client:
issuer: https://auth.example.com
audience: my-api
accepted_algorithms:
- RS256
- ML-DSA-65
ML-DSA tokens use the AKP JWK key type with base64url-encoded raw public keys (per draft-ietf-cose-dilithium). The TokenValidator automatically dispatches ML-DSA tokens to pqc_jwt (liboqs) and RS256 tokens to python-jose.
JWKS response with ML-DSA key:
{
"keys": [
{"kty": "AKP", "kid": "pqc-key-1", "alg": "ML-DSA-65", "pub": "<base64url>"}
]
}
Supported algorithms: ML-DSA-65 (NIST Level 3), ML-DSA-87 (NIST Level 5).
When liboqs-python is not installed, ML-DSA tokens are rejected with AuthConfigurationError. RS256 continues to work without liboqs.
Testing PQC
PQC tests skip gracefully without liboqs via pytest.importorskip("oqs"):
def test_pqc_token(mldsa65_keypair, make_pqc_token):
oqs = pytest.importorskip("oqs")
public_key, secret_key = mldsa65_keypair
token = make_pqc_token(secret_key, algorithm="ML-DSA-65")
# ... test with token
Run PQC tests in Docker: make pqc-test
Checklist
- [ ]
auth_client.issuerandauth_client.audienceconfigured - [ ] Public endpoints marked with
@allow_anonymous - [ ] Admin endpoints protected with
@requires_role - [ ] Group-restricted endpoints protected with
@requires_group - [ ]
SecurityContext.require()used in services (not just controllers) - [ ] Custom
RoleResolverregistered if token structure differs from default - [ ] Tests use RSA keypair fixture and
make_tokenfactory - [ ] If PQC:
accepted_algorithmsincludes ML-DSA variant,pqcextra installed
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.