Skip to content

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.

Standard OAuth 2.0 flows are vulnerable to several classes of attacks that FAPI 2.0 explicitly addresses:

AttackImpactFAPI 2.0 mitigation
Authorization code interceptionAttacker exchanges stolen code for tokensPKCE with S256 (RFC 7636)
Authorization parameter tamperingAttacker modifies scopes, redirect URI in browser URLPAR (RFC 9126) — parameters sent via back-channel
Token replay / theftStolen access token used from another deviceDPoP (RFC 9449) — tokens bound to client key
Client credential leakageShared secret logged by proxy or WAFprivate_key_jwt (RFC 7523) — no secret on the wire
IdP mix-up attackClient tricked into sending code to wrong issuerIssuer verification (RFC 9207)
Authorization code replayStolen code reused after a long windowShort-lived authorization codes (60 seconds)

The following table maps each FAPI 2.0 requirement to its Granit implementation:

FAPI 2.0 requirementRFCGranit featureLayer
PKCE with S256RFC 7636RequireProofKeyForCodeExchange() (default)OpenIddict server
Pushed Authorization RequestsRFC 9126RequirePar: trueOpenIddict server
Sender-constrained access tokensRFC 9449UseDPoP: true on BFF frontendBFF + OpenIddict server
DPoP enforcement on resource serversRFC 9449RequireDPoP: trueResource server
private_key_jwt or mTLS client authRFC 7523ClientAuthenticationMethod: PrivateKeyJwtBFF frontend
Authorization response issuer verificationRFC 9207RequireIssuerValidation: true (default)BFF
Short-lived authorization codesFAPI 2.0 SS5.3.2.160-second lifetime (auto-applied)OpenIddict server

Enable the full FAPI 2.0 profile on the OpenIddict server with a single option:

appsettings.json
{
"OpenIddict": {
"EnableFapi2Profile": true
}
}

This single flag enforces all server-side FAPI 2.0 constraints:

  • RequirePar is set to true (PAR mandatory for all authorization code flows)
  • Authorization code lifetime is reduced to 60 seconds
  • PKCE with S256 remains enforced (already the default)

A complete FAPI 2.0-compliant deployment requires configuration across three layers:

appsettings.json (identity server)
{
"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/"]
}
]
}
}
}
RequirementRFCStatusConfiguration
PKCE with S256RFC 7636Enforced by defaultRequireProofKeyForCodeExchange() — always on
Pushed Authorization RequestsRFC 9126Opt-inOpenIddict.EnableFapi2Profile: true or OpenIddict.RequirePar: true
DPoP sender-constrained tokensRFC 9449Opt-inBff.Frontends[].UseDPoP: true
DPoP enforcement (resource server)RFC 9449Opt-inAuthentication.OpenIddict.RequireDPoP: true
private_key_jwt client authRFC 7523Opt-inBff.Frontends[].ClientAuthenticationMethod: PrivateKeyJwt
Issuer verificationRFC 9207Enabled by defaultBff.RequireIssuerValidation: true (default)
Short-lived authorization codesFAPI 2.0 SS5.3.2.1Auto-applied60-second lifetime when EnableFapi2Profile: true
Confidential client onlyFAPI 2.0 SS5.3.1Enforced by BFFPublic clients (SPAs) are never exposed — tokens stay server-side
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