← Payment Reference GuidesAPI Security Reference

FAPI: the Financial-grade API Security Profiles

FAPI is the OpenID Foundation's hardened profile of OAuth 2.0 and OpenID Connect for APIs where a stolen token means a stolen payment. This guide covers FAPI 1.0 Baseline and Advanced, the FAPI 2.0 Security Profile and its attacker model, Message Signing and CIBA — what each one requires, the authorization-code flow message by message, which open-banking ecosystems mandate which profile, and a checklist for shipping it.

The FAPI 2.0 authorization code flow, message by message

A PISP asking a bank to authorise a €120.00 payment. The client authenticates with private_key_jwt and binds its tokens with DPoP; swap in mTLS and the shape is identical, minus the DPoP headers. Values are abbreviated.

1

Pushed authorization request (back channel)

Everything the authorization server needs is posted server-to-server, authenticated, before the user is involved. RAR carries the payment details that scope cannot.

POST /par HTTP/1.1
Host: auth.bank.example
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr...   ← optional: binds the code to this key (dpop_jkt)

client_id=tpp-7f3a
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJQUzI1NiIsImtpZCI6InRwcC0yMDI2In0...   ← private_key_jwt, aud = "https://auth.bank.example"
&response_type=code
&redirect_uri=https://tpp.example/cb
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=af0ifjsldkj
&authorization_details=[{"type":"payment_initiation","instructedAmount":{"currency":"EUR","amount":"120.00"},
   "creditorName":"Shop Ltd","creditorAccount":{"iban":"DE02…"},"remittanceInformationUnstructured":"Order 4711"}]

HTTP/1.1 201 Created
{"request_uri":"urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c","expires_in":90}
2

Redirect to the authorization endpoint (front channel)

The browser sees only two parameters. There is nothing here for attacker A3a to learn or tamper with — the amount and payee never pass through the user agent.

HTTP/1.1 303 See Other
Location: https://auth.bank.example/authorize
  ?client_id=tpp-7f3a
  &request_uri=urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c
3

Authentication and consent at the bank

The bank authenticates the user — under PSD2 that is SCA with dynamic linking, so the screen shows exactly €120.00 to Shop Ltd — and asks for consent, naming the client. FAPI does not prescribe how the user authenticates; that is the ecosystem's (and the regulator's) call.

4

Authorization response: a code and an issuer

Single-use, valid at most 60 seconds, and useless without the PKCE verifier and the client's key. The client must compare iss with the issuer in the metadata it discovered — the mix-up defence.

HTTP/1.1 303 See Other
Location: https://tpp.example/cb
  ?code=SplxlOBeZQQYbYS6WxSbIA
  &state=af0ifjsldkj
  &iss=https%3A%2F%2Fauth.bank.example
5

Token request (back channel)

Client authentication again, the PKCE verifier, and a DPoP proof whose public key becomes the token's binding. With mTLS the proof is replaced by the client certificate on the TLS connection.

POST /token HTTP/1.1
Host: auth.bank.example
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr...   ← {htm:"POST", htu:"https://auth.bank.example/token", jti, iat}

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://tpp.example/cb
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_id=tpp-7f3a
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJQUzI1NiIsImtpZCI6InRwcC0yMDI2In0...

HTTP/1.1 200 OK
{"access_token":"Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxU",
 "token_type":"DPoP", "expires_in":300,
 "refresh_token":"Q..Zkm29lexi8VnWg2zPW1x-tgGad0Ibc3s3EwM_Ni4-g",
 "authorization_details":[{"type":"payment_initiation", …}]}

Inside the access token (or its introspection response) the binding is visible as a confirmation claim: "cnf":{"jkt":"0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"} for DPoP, or "cnf":{"x5t#S256":"…"} for mTLS.

6

Calling the API

The resource server checks the token and the proof: the DPoP JWT must be signed by the key in cnf.jkt, name this method and URL, be fresh, and carry ath (the hash of the access token). The x-fapi-interaction-id correlates the call in both parties' logs.

POST /open-banking/v4/pisp/domestic-payments HTTP/1.1
Host: api.bank.example
Authorization: DPoP Kz~8mXK1EalYznwH-LC-1fBAo.4Ljp~zsPE_NeO.gxU
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwiandr...   ← {htm, htu, jti, iat, ath, nonce?}
x-fapi-interaction-id: 93bac548-d2de-4546-b106-880a5018460d
Content-Type: application/json

{ "Data": { "ConsentId": "…", "Initiation": { "InstructedAmount": { "Amount": "120.00", "Currency": "EUR" }, … } } }

Animated walkthrough — redirect and decoupled (CIBA) flows

The FAPI 2.0 Security Profile redirect flow: the client pushes the authorization request server-to-server (PAR), the browser carries only a request_uri, the response is a single-use code plus the issuer, and the access token is bound to the client's key (DPoP) or certificate (mTLS) — so nothing an attacker can read in the front channel is enough to get or use a token.

1 / 11
🧑UserBrowser / app
⚙️ClientTPP · confidential
🏛️Auth ServerBank / IdP
🗄️Resource APIAccounts / payments
1
User Starts the Flow
2
Pushed Authorization Request
3
request_uri Issued
4
Redirect with Two Parameters
5
Authenticate & Consent
6
Code + iss (303)
7
Callback Checked
8
Token Request
9
Sender-Constrained Tokens
10
API Call with Proof
11
Protected Resource
Initiation

User Starts the Flow

Clicks "connect my bank" or "pay by bank" in the client.

FAPI requires the client to start an authorization only with the user's consent and to protect that start against CSRF — the user must know the context in which the flow began. The client discovers the bank's endpoints from its metadata document (RFC 8414 / OpenID Discovery) and nowhere else.

Initiation
Authorization
SCA Challenge
Response
Completion

Decoupled: FAPI-CIBA in one paragraph

When the user is not in a browser the client can start the flow from the back channel: it posts a signed backchannel authentication request (a login hint identifying the user, the scope or authorization_details, and a binding_message such as a short code) and receives an auth_req_id. The bank pushes an approval to the user's banking app, showing the binding message and the transaction; the user authenticates there. The client then either polls the token endpoint with grant_type=urn:openid:params:grant-type:ciba or waits for a ping to its notification endpoint and then collects the tokens. FAPI-CIBA forbids push mode (tokens delivered to the client's callback), and the tokens it issues are sender-constrained like any other FAPI token. This is the protocol behind the "decoupled" SCA approach in the open-banking consent flows.

Put it into practice

Next steps

Keep reading

Related articles