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.
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 Type | FAPI Protection |
|---|---|
| Token theft — attacker steals an access token | DPoP binds tokens to a client-held key; a stolen token alone cannot be used from another host |
| Request tampering — modifying authorization parameters in the URL | PAR sends the request through a back-channel, returning a short reference used in the front channel |
| Replay attacks — reusing old authentication data | Nonces and short-lived codes (60 seconds) prevent replay |
| Code interception — capturing the authorization code | PKCE S256 ensures only the original client can redeem codes |
Key Features
| Feature | What 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_jwt | The client signs a JWT assertion with a private key instead of sending a shared client secret. |
| Short auth codes | Authorization 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:
- Navigate to OAuth Clients and select your application.
- Click the FAPI 2.0 tab.
- Choose a Security Profile:
| Profile | Security Level | Use Case |
|---|---|---|
Disabled | Standard OAuth | Regular web/mobile apps |
FAPI 2.0 Baseline | High | Open Banking, PSD2 |
FAPI 2.0 Advanced | Maximum | High-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 BearerWhen 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:
| Field | Location | Description |
|---|---|---|
typ | Header | Must be dpop+jwt |
alg | Header | Algorithm: ES256, RS256, or PS256 |
jwk | Header | Your public key (never include the private key) |
htm | Payload | HTTP method (POST, GET) |
htu | Payload | Full URL of the request |
iat | Payload | Current timestamp (must be within 60 seconds) |
jti | Payload | Unique identifier (at least 16 characters) |
ath | Payload | Access 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.
| Method | Security | How It Works |
|---|---|---|
client_secret_basic | Low | Password in HTTP Authorization header |
client_secret_post | Low | Password in POST body |
private_key_jwt | High | Sign a JWT with your private key |
tls_client_auth | High | TLS 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:
| Claim | Value |
|---|---|
iss | Your client_id |
sub | Your client_id (same as iss) |
aud | Authorization server issuer URL |
exp | Expiration (short — 60 seconds recommended) |
jti | Unique 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
| Error | Cause | Solution |
|---|---|---|
invalid_request + "PAR required" | Client has PAR enabled but didn't use it | Push request to /oauth/par first |
invalid_dpop_proof | DPoP proof is malformed or expired | Check JWT format, timestamps, and signing |
use_dpop_nonce | Server requires a nonce | Include the nonce from the DPoP-Nonce header |
invalid_client | private_key_jwt signature failed | Verify JWKS is published and keys match |
Glossary
| Term | Meaning |
|---|---|
| PAR | Pushed Authorization Request — secure way to initiate OAuth flows |
| DPoP | Demonstrating Proof-of-Possession — binds tokens to client keys |
| mTLS | Mutual TLS — two-way certificate authentication |
| JWK | JSON Web Key — format for a cryptographic key |
| JWKS | JSON Web Key Set — collection of JWKs |
| JKT | JWK Thumbprint — hash of a public key used for identification |
| PKCE | Proof Key for Code Exchange — protects authorization codes |
Reference Specifications
- FAPI 2.0 Security Profile
- RFC 9126: Pushed Authorization Requests
- RFC 9449: DPoP
- RFC 8705: Mutual-TLS Client Authentication and Certificate-Bound Tokens
- RFC 7636: PKCE