ADR-056: FAPI 2.0 Security Architecture
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-03-06 |
| Ticket | CAB-1733 |
| Author | Christophe Aboulicam |
| Reviewers | Council 8.13/10 |
Contextβ
STOA Gateway (65K LOC Rust) already implements sender-constrained tokens via DPoP (RFC 9449) and mTLS (RFC 8705) β the two hardest FAPI 2.0 requirements. However, full FAPI 2.0 Security Profile conformance requires additional capabilities (PAR, private_key_jwt, confidential clients) that are currently missing.
Production Keycloak 26.5.3 supports FAPI 2.0 client profiles natively (fapi-2-security-profile, fapi-2-dpop-security-profile). No Keycloak upgrade is needed β only configuration activation and gateway-side implementation of missing flows.
Current State (Spike Results)β
Implemented (FAPI 2.0 compliant):
- DPoP proof validation β full RFC 9449 Section 4.3, 843 LOC, replay prevention via moka cache
- mTLS certificate binding β full RFC 8705, 1,122 LOC, cnf.x5t#S256 verification
- Unified sender-constraint middleware β DPoP + mTLS in single pipeline, 621 LOC
- PKCE S256 enforcement β via DCR client patching
- OAuth discovery β RFC 9728 + RFC 8414 metadata
- Token exchange β RFC 8693 in Control Plane API (10 test cases)
Missing (required for FAPI 2.0):
- PAR (Pushed Authorization Requests, RFC 9126) β no code exists
- private_key_jwt client authentication (RFC 7523) β only client_secret supported
- Confidential client enforcement β current MCP OAuth uses public clients (PKCE without secret)
- Token revocation proxy β Keycloak handles it, gateway doesn't proxy
Missing (recommended/optional):
- JARM (JWT-Secured Authorization Response Mode)
- RAR (Rich Authorization Requests, RFC 9396)
- CIBA (Client-Initiated Backchannel Authentication)
Decisionβ
Adopt a phased approach to FAPI 2.0 conformance with a dual-mode transition period:
Architectureβ
FAPI 2.0 Security Profile
========================
Client STOA Gateway Keycloak 26.5.3
| | |
|--- PAR Request -------->| POST /oauth/par |
| |--- Forward PAR -------------> |
|<-- request_uri ---------|<-- request_uri + expiry ------|
| | |
|--- Auth Request ------->| (validate request_uri) |
| |--- Forward auth ------------> |
|<-- auth code -----------|<-- auth code -----------------|
| | |
|--- Token Request ------>| POST /oauth/token |
| + client_assertion |--- Validate JWT assertion --->|
| + code_verifier |--- Forward token req -------->|
| + DPoP proof |<-- access_token + cnf.jkt ----|
|<-- access_token --------| |
| | |
|--- API Request -------->| Verify: |
| + DPoP proof | 1. JWT signature |
| + mTLS cert | 2. DPoP binding (cnf.jkt) |
| | 3. mTLS binding (cnf.x5t) |
| | 4. OPA policy |
|<-- Response ------------| |
Threat Model (STRIDE)β
| Threat | Category | Mitigation |
|---|---|---|
| Token theft + replay | Spoofing | DPoP binding (cnf.jkt) β token unusable without private key |
| Man-in-the-middle | Tampering | mTLS binding (cnf.x5t#S256) β cert pinned to token |
| Authorization code injection | Tampering | PAR β auth params sent server-side, not in URL |
| Client impersonation | Spoofing | private_key_jwt β no shared secrets, asymmetric proof |
| Token exfiltration | Info Disclosure | Sender constraints β stolen token requires matching DPoP/cert |
| Scope escalation | Elevation | OPA policy enforcement + RAR (Phase 2) |
Dual-Mode Transitionβ
During transition, gateway supports two client security profiles simultaneously:
| Profile | Auth Method | Clients | Timeline |
|---|---|---|---|
| Standard (current) | Public PKCE + auth_method: none | Claude.ai, MCP clients | Now β Phase 2 end |
| FAPI 2.0 | Confidential + private_key_jwt + DPoP/mTLS | Financial/regulated clients | Phase 1 β permanent |
Per-client profile is determined by Keycloak client configuration. Existing MCP clients continue working unchanged. New FAPI clients get the full security profile.
Gateway β Control Plane API Boundaryβ
| Concern | Gateway (Rust) | CP API (Python) |
|---|---|---|
| PAR endpoint proxy | POST /oauth/par β KC | N/A |
| Client assertion validation | JWT signature verify | N/A |
| Sender constraint enforcement | DPoP + mTLS middleware | N/A |
| Token exchange | N/A (future Phase 2) | POST /v1/consumers/.../token-exchange |
| Client lifecycle | DCR proxy + PKCE patch | create_consumer_client() with FAPI config |
| FAPI client profile assignment | N/A | KC admin API β set client profile |
Implementation Phasesβ
Phase 1 β FAPI 2.0 Foundation β (completed PR #1526)β
- β
PAR proxy (
POST /oauth/par) β new endpoint, forward to KC, returnrequest_uri - β
Enable KC FAPI profile β
fapi-2-security-profileon new clients - β
OTel activation β runtime kill-switch (
OTEL_ENABLED=true/false), not compile-time only - β ADR + docs β this document
- β Unify KC versions β quickstart/E2E to 26.5.3
Phase 2 β Confidential Clients + Governance (3-6 months)β
- β private_key_jwt β client assertion parsing, JWKS validation, discovery metadata update (PR #1531)
- Token exchange in gateway β RFC 8693 proxy for agent delegation chains
- RAR β rich authorization requests for fine-grained API access
- Agent delegation model β
on_behalf_ofclaim, intent tracking, consent
Phase 3 β Certification (6-12 months)β
- FAPI 2.0 conformance test suite β OpenID Foundation tests
- Formal certification β OpenID Foundation listing
- STRIDE threat model β formal security review
Phase 4 β Innovation (12-18 months)β
- eBPF sidecar mode β kernel-level policy enforcement
- eIDAS 2.0 β EU Digital Identity Wallet integration
- FAPI 2.0 Message Signing β request/response non-repudiation
Consequencesβ
Positiveβ
- STOA becomes one of the first open-source API gateways with FAPI 2.0 conformance
- Unlocks regulated market segments (finance, health, e-government)
- Existing DPoP + mTLS investment (2,586 LOC) is directly reused
- Keycloak 26.5.3 already deployed β no infrastructure upgrade needed
- Dual-mode prevents breaking existing MCP integrations
Negativeβ
- PAR adds latency (extra round-trip to KC before authorization)
- private_key_jwt requires client key management (JWKS endpoints per client)
- Confidential client requirement breaks current Claude.ai MCP flow (mitigated by dual-mode)
- FAPI certification has ongoing maintenance cost (annual re-certification)
Risksβ
- Claude.ai may not support private_key_jwt for MCP OAuth (mitigation: keep standard profile)
- Keycloak FAPI 2.0 profiles may have edge cases not covered by KC tests (mitigation: own conformance suite)
- OTel runtime toggle may have performance impact even when disabled (mitigation: benchmark)