Skip to main content

ADR-056: FAPI 2.0 Security Architecture

FieldValue
StatusAccepted
Date2026-03-06
TicketCAB-1733
AuthorChristophe Aboulicam
ReviewersCouncil 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)​

ThreatCategoryMitigation
Token theft + replaySpoofingDPoP binding (cnf.jkt) β€” token unusable without private key
Man-in-the-middleTamperingmTLS binding (cnf.x5t#S256) β€” cert pinned to token
Authorization code injectionTamperingPAR β€” auth params sent server-side, not in URL
Client impersonationSpoofingprivate_key_jwt β€” no shared secrets, asymmetric proof
Token exfiltrationInfo DisclosureSender constraints β€” stolen token requires matching DPoP/cert
Scope escalationElevationOPA policy enforcement + RAR (Phase 2)

Dual-Mode Transition​

During transition, gateway supports two client security profiles simultaneously:

ProfileAuth MethodClientsTimeline
Standard (current)Public PKCE + auth_method: noneClaude.ai, MCP clientsNow β†’ Phase 2 end
FAPI 2.0Confidential + private_key_jwt + DPoP/mTLSFinancial/regulated clientsPhase 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​

ConcernGateway (Rust)CP API (Python)
PAR endpoint proxyPOST /oauth/par β†’ KCN/A
Client assertion validationJWT signature verifyN/A
Sender constraint enforcementDPoP + mTLS middlewareN/A
Token exchangeN/A (future Phase 2)POST /v1/consumers/.../token-exchange
Client lifecycleDCR proxy + PKCE patchcreate_consumer_client() with FAPI config
FAPI client profile assignmentN/AKC admin API β€” set client profile

Implementation Phases​

Phase 1 β€” FAPI 2.0 Foundation βœ… (completed PR #1526)​

  1. βœ… PAR proxy (POST /oauth/par) β€” new endpoint, forward to KC, return request_uri
  2. βœ… Enable KC FAPI profile β€” fapi-2-security-profile on new clients
  3. βœ… OTel activation β€” runtime kill-switch (OTEL_ENABLED=true/false), not compile-time only
  4. βœ… ADR + docs β€” this document
  5. βœ… Unify KC versions β€” quickstart/E2E to 26.5.3

Phase 2 β€” Confidential Clients + Governance (3-6 months)​

  1. βœ… private_key_jwt β€” client assertion parsing, JWKS validation, discovery metadata update (PR #1531)
  2. Token exchange in gateway β€” RFC 8693 proxy for agent delegation chains
  3. RAR β€” rich authorization requests for fine-grained API access
  4. Agent delegation model β€” on_behalf_of claim, intent tracking, consent

Phase 3 β€” Certification (6-12 months)​

  1. FAPI 2.0 conformance test suite β€” OpenID Foundation tests
  2. Formal certification β€” OpenID Foundation listing
  3. STRIDE threat model β€” formal security review

Phase 4 β€” Innovation (12-18 months)​

  1. eBPF sidecar mode β€” kernel-level policy enforcement
  2. eIDAS 2.0 β€” EU Digital Identity Wallet integration
  3. 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)

References​