FAPI 2.0 Security Profile
The FAPI 2.0 Security Profile is a set of mandatory security constraints designed for high-risk APIs — Open Banking (PSD2), healthcare data exchange, government identity, and any scenario where token theft or parameter tampering would have severe consequences.
Granit implements every FAPI 2.0 requirement across its OpenIddict server, BFF proxy, and resource server validation layers. Enable the full profile with a single flag or configure each mechanism individually.
Why FAPI 2.0 matters
Section titled “Why FAPI 2.0 matters”Standard OAuth 2.0 flows are vulnerable to several classes of attacks that FAPI 2.0 explicitly addresses:
| Attack | Impact | FAPI 2.0 mitigation |
|---|---|---|
| Authorization code interception | Attacker exchanges stolen code for tokens | PKCE with S256 (RFC 7636) |
| Authorization parameter tampering | Attacker modifies scopes, redirect URI in browser URL | PAR (RFC 9126) — parameters sent via back-channel |
| Token replay / theft | Stolen access token used from another device | DPoP (RFC 9449) — tokens bound to client key |
| Client credential leakage | Shared secret logged by proxy or WAF | private_key_jwt (RFC 7523) — no secret on the wire |
| IdP mix-up attack | Client tricked into sending code to wrong issuer | Issuer verification (RFC 9207) |
| Authorization code replay | Stolen code reused after a long window | Short-lived authorization codes (60 seconds) |
Requirement mapping
Section titled “Requirement mapping”The following table maps each FAPI 2.0 requirement to its Granit implementation:
| FAPI 2.0 requirement | RFC | Granit feature | Layer |
|---|---|---|---|
| PKCE with S256 | RFC 7636 | RequireProofKeyForCodeExchange() (default) | OpenIddict server |
| Pushed Authorization Requests | RFC 9126 | RequirePar: true | OpenIddict server |
| Sender-constrained access tokens | RFC 9449 | UseDPoP: true on BFF frontend | BFF + OpenIddict server |
| DPoP enforcement on resource servers | RFC 9449 | RequireDPoP: true | Resource server |
private_key_jwt or mTLS client auth | RFC 7523 | ClientAuthenticationMethod: PrivateKeyJwt | BFF frontend |
| Authorization response issuer verification | RFC 9207 | RequireIssuerValidation: true (default) | BFF |
| Short-lived authorization codes | FAPI 2.0 SS5.3.2.1 | 60-second lifetime (auto-applied) | OpenIddict server |
Quick start
Section titled “Quick start”Enable the full FAPI 2.0 profile on the OpenIddict server with a single option:
{ "OpenIddict": { "EnableFapi2Profile": true }}This single flag enforces all server-side FAPI 2.0 constraints:
RequireParis set totrue(PAR mandatory for all authorization code flows)- Authorization code lifetime is reduced to 60 seconds
- PKCE with S256 remains enforced (already the default)
Full configuration example
Section titled “Full configuration example”A complete FAPI 2.0-compliant deployment requires configuration across three layers:
{ "OpenIddict": { "Issuer": "https://auth.example.com", "EnableFapi2Profile": true,
"Seeding": { "Applications": [ { "ClientId": "my-bff", "ClientSecret": null, "DisplayName": "My BFF (FAPI 2.0)", "SigningKeyJwk": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\"}", "Permissions": [ "ept:token", "ept:authorization", "ept:pushed_authorization", "ept:logout", "gt:authorization_code", "gt:refresh_token", "rst:code", "scp:openid", "scp:profile", "scp:email", "scp:roles", "scp:offline_access" ], "RedirectUris": ["https://app.example.com/bff/callback"], "PostLogoutRedirectUris": ["https://app.example.com/"] } ] } }}{ "Bff": { "Authority": "https://auth.example.com", "RequireIssuerValidation": true,
"Frontends": [ { "Name": "admin", "ClientId": "my-bff", "ClientAuthenticationMethod": "PrivateKeyJwt", "ClientSigningKeyJwk": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\",\"d\":\"...\"}", "UsePushedAuthorizationRequests": true, "UseDPoP": true, "Scopes": ["openid", "profile", "email", "roles", "offline_access"] } ] }}{ "Authentication": { "OpenIddict": { "Issuer": "https://auth.example.com", "Audience": "my-api", "RequireDPoP": true } }}Compliance checklist
Section titled “Compliance checklist”| Requirement | RFC | Status | Configuration |
|---|---|---|---|
| PKCE with S256 | RFC 7636 | Enforced by default | RequireProofKeyForCodeExchange() — always on |
| Pushed Authorization Requests | RFC 9126 | Opt-in | OpenIddict.EnableFapi2Profile: true or OpenIddict.RequirePar: true |
| DPoP sender-constrained tokens | RFC 9449 | Opt-in | Bff.Frontends[].UseDPoP: true |
| DPoP enforcement (resource server) | RFC 9449 | Opt-in | Authentication.OpenIddict.RequireDPoP: true |
private_key_jwt client auth | RFC 7523 | Opt-in | Bff.Frontends[].ClientAuthenticationMethod: PrivateKeyJwt |
| Issuer verification | RFC 9207 | Enabled by default | Bff.RequireIssuerValidation: true (default) |
| Short-lived authorization codes | FAPI 2.0 SS5.3.2.1 | Auto-applied | 60-second lifetime when EnableFapi2Profile: true |
| Confidential client only | FAPI 2.0 SS5.3.1 | Enforced by BFF | Public clients (SPAs) are never exposed — tokens stay server-side |
How the mechanisms work together
Section titled “How the mechanisms work together”sequenceDiagram
participant Browser
participant BFF as BFF Server
participant IdP as OpenIddict Server
participant API as Resource Server
Browser->>BFF: GET /bff/login
Note over BFF: Generate PKCE (S256)<br/>+ DPoP EC P-256 key pair
BFF->>IdP: POST /connect/par (PAR)<br/>client_assertion (private_key_jwt)<br/>+ code_challenge + redirect_uri
IdP-->>BFF: { request_uri: "urn:..." }
BFF-->>Browser: 302 /connect/authorize?request_uri=urn:...
Browser->>IdP: GET /connect/authorize?request_uri=urn:...
IdP-->>Browser: Login form → consent → 302 callback
Browser->>BFF: GET /bff/callback?code=xxx&iss=https://auth.example.com
Note over BFF: Verify iss parameter (RFC 9207)
BFF->>IdP: POST /connect/token<br/>client_assertion (private_key_jwt)<br/>+ code_verifier (PKCE)<br/>+ DPoP proof
IdP-->>BFF: { access_token (cnf-bound), token_type: "DPoP" }
BFF->>API: GET /api/resource<br/>Authorization: DPoP <token><br/>DPoP: <fresh proof>
Note over API: Validate DPoP proof<br/>Reject plain Bearer
API-->>BFF: 200 OK
BFF-->>Browser: Response
See also
Section titled “See also”- Pushed Authorization Requests (PAR) — move authorization parameters to back-channel
- DPoP (Proof-of-Possession) — bind tokens to cryptographic keys
- Private Key JWT Authentication — eliminate shared secrets
- OIDC Server Configuration — endpoints and flows overview
- BFF Configuration — global and frontend options reference
- DPoP Resource Server Validation — validate DPoP proofs on any IdP (Keycloak, Entra ID, Auth0)
- Authentication — DPoP enforcement on resource servers