Skip to main content

FAPI 2.0 Security Profile

FAPI 2.0 is a security profile on top of OAuth 2.0 designed for financial APIs, open banking, healthcare, and any application handling high-value or highly sensitive transactions. Use it when regulatory requirements or threat models demand stronger client authentication and tokens that are bound to a specific client key or certificate.

FAPI 2.0 — Financial-grade API Security Profile from the OpenID Foundation. A hardening profile of OAuth that mandates PAR (Pushed Authorization Requests), sender-constrained tokens (DPoP or mTLS), PKCE, and tighter client authentication.

When to Use FAPI 2.0

Use FAPI 2.0 for financial-grade APIs, open banking integrations (PSD2), healthcare, and any application requiring the strongest OAuth security.

What FAPI 2.0 Adds on Top of OAuth 2.0

Attack TypeFAPI Protection
Token theft — attacker steals an access tokenDPoP binds tokens to a client-held key; a stolen token alone cannot be used from another host
Request tampering — modifying authorization parameters in the URLPAR sends the request through a back-channel, returning a short reference used in the front channel
Replay attacks — reusing old authentication dataNonces and short-lived codes (60 seconds) prevent replay
Code interception — capturing the authorization codePKCE S256 ensures only the original client can redeem codes

Key Features

FeatureWhat It Does
PAR (RFC 9126 — Pushed Authorization Requests)The client POSTs the authorization parameters to a back-channel endpoint first, receiving a request_uri that it then uses in the front-channel redirect. Avoids URL-length limits and tampering.
DPoP (RFC 9449 — Demonstrating Proof-of-Possession)Binds an access token to a client-held key. Each request includes a short-lived proof JWT signed with that key; a stolen token alone is useless from another host.
mTLS (RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens)Uses client certificates for client auth and to bind tokens.
private_key_jwtThe client signs a JWT assertion with a private key instead of sending a shared client secret.
Short auth codesAuthorization codes expire in 60 seconds (vs. 10 minutes in standard OAuth).

How to Enable FAPI 2.0

Configure FAPI 2.0 in the Organization Portal:

  1. Navigate to OAuth Clients and select your application.
  2. Click the FAPI 2.0 tab.
  3. Choose a Security Profile:
ProfileSecurity LevelUse Case
DisabledStandard OAuthRegular web/mobile apps
FAPI 2.0 BaselineHighOpen Banking, PSD2
FAPI 2.0 AdvancedMaximumHigh-value financial services

PAR: Pushed Authorization Requests

Instead of putting all authorization parameters in the URL (where they could be seen or modified), PAR lets the client push the request first and get back a short reference to use.

POST /orgs/{orgId}/api/v1/oauth/par

Step 1: Push the Authorization Request

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/par \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "response_type=code" \
-d "redirect_uri=https://myapp.com/callback" \
-d "scope=openid profile email" \
-d "code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" \
-d "code_challenge_method=S256"

Response

{
"request_uri": "urn:ietf:params:oauth:request_uri:abc123xyz...",
"expires_in": 600
}

Step 2: Exchange the Code with a DPoP Proof

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-H "DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Li4ufX0..." \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "grant_type=authorization_code" \
-d "code=YOUR_AUTH_CODE" \
-d "code_verifier=YOUR_CODE_VERIFIER"

Response with DPoP Token

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6...",
"token_type": "DPoP",
"expires_in": 3600,
"scope": "openid profile email"
}
token_type is DPoP, not Bearer

When using FAPI with DPoP, the token response returns token_type: "DPoP" instead of "Bearer". Use the DPoP proof mechanism when presenting the token.

DPoP Proof Structure

The DPoP proof is a JWT with these required fields:

FieldLocationDescription
typHeaderMust be dpop+jwt
algHeaderAlgorithm: ES256, RS256, or PS256
jwkHeaderYour public key (never include the private key)
htmPayloadHTTP method (POST, GET)
htuPayloadFull URL of the request
iatPayloadCurrent timestamp (must be within 60 seconds)
jtiPayloadUnique identifier (at least 16 characters)
athPayloadAccess token hash (only for resource requests)

Client Authentication Methods

FAPI 2.0 recommends stronger client authentication than shared secrets. Configure your preferred method in the FAPI 2.0 tab.

MethodSecurityHow It Works
client_secret_basicLowPassword in HTTP Authorization header
client_secret_postLowPassword in POST body
private_key_jwtHighSign a JWT with your private key
tls_client_authHighTLS certificate authentication (mTLS)

private_key_jwt Example

Instead of sending a client_secret, the client creates a signed JWT assertion:

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-d "grant_type=authorization_code" \
-d "code=YOUR_AUTH_CODE" \
-d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
-d "client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

The client assertion must contain:

ClaimValue
issYour client_id
subYour client_id (same as iss)
audAuthorization server issuer URL
expExpiration (short — 60 seconds recommended)
jtiUnique identifier

Discovery Document

FAPI 2.0 capabilities are advertised in the OpenID Connect discovery document:

GET /orgs/{orgId}/api/v1/.well-known/openid-configuration

{
"pushed_authorization_request_endpoint": "https://.../oauth/par",
"require_pushed_authorization_requests": false,
"dpop_signing_alg_values_supported": ["ES256", "RS256", "PS256"],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"private_key_jwt",
"tls_client_auth"
],
"authorization_response_iss_parameter_supported": true,
"tls_client_certificate_bound_access_tokens": true
}

Error Responses

ErrorCauseSolution
invalid_request + "PAR required"Client has PAR enabled but didn't use itPush request to /oauth/par first
invalid_dpop_proofDPoP proof is malformed or expiredCheck JWT format, timestamps, and signing
use_dpop_nonceServer requires a nonceInclude the nonce from the DPoP-Nonce header
invalid_clientprivate_key_jwt signature failedVerify JWKS is published and keys match

Glossary

TermMeaning
PARPushed Authorization Request — secure way to initiate OAuth flows
DPoPDemonstrating Proof-of-Possession — binds tokens to client keys
mTLSMutual TLS — two-way certificate authentication
JWKJSON Web Key — format for a cryptographic key
JWKSJSON Web Key Set — collection of JWKs
JKTJWK Thumbprint — hash of a public key used for identification
PKCEProof Key for Code Exchange — protects authorization codes

Reference Specifications