FAPI 2.0 for Open Banking Developers: PAR, PKCE and Token Binding
By OpenFinanceTools · Updated
If your OAuth integration works in a basic demo but fails in a financial API sandbox, the missing piece may be the security profile around the flow. FAPI gives the participants a stricter contract. It is useful to draw that contract before debugging individual JWTs.
This guide explains the authorization-code path, gives you a small executable PKCE exercise, and turns common implementation mistakes into a test plan. It does not implement an authorization server or certify an application. A particular bank or national ecosystem can require additional metadata, registration, consent and signing behavior.
1. Start with the right scope
The February 2025 final FAPI 2.0 Security Profile targets confidential clients. For the authorization-code path, it requires authenticated PAR and PKCE with S256. Access tokens are sender-constrained with mTLS or DPoP; client authentication uses mTLS or private_key_jwt. These are separate decisions.
Other details worth checking against the final profile include an authorization-response issuer check, codes valid for at most 60 seconds, PAR request references lasting less than 600 seconds, and JWT algorithms restricted to PS256, ES256 or EdDSA with Ed25519. With private_key_jwt, the assertion audience is the authorization server's issuer identifier, expressed as a string. Treat this summary as a map to the normative text, not a complete checklist.
“FAPI 2.0” is not a synonym for every open banking API. Before implementation, write down the exact security-profile version, ecosystem specification and server configuration you are targeting. A successful call to one institution does not establish compatibility with another.
2. Follow one authorization request end to end
Imagine a server-side application that needs permission to read account data. Keep the browser, application backend, authorization server and resource server distinct in your diagram. The browser should not receive the application's private signing key.
- Discover and configure. Start from a trusted issuer and retrieve its metadata. Record its endpoints and supported methods in your integration configuration.
- Create transaction state. Generate a fresh PKCE verifier. Keep it on the backend with the authorization transaction and its expiry.
- Push the authorization request. The backend authenticates to the PAR endpoint and sends the request parameters, including the challenge and redirect URI.
- Redirect the browser. Use the returned request reference to start the user's authorization interaction.
- Handle the callback. Associate the callback with the transaction, validate the expected issuer and process the returned code.
- Exchange the code. Authenticate to the token endpoint and submit the verifier. Establish token binding using the configured mechanism.
- Call the API. Present the access token with its required proof or client certificate. Apply the API's permission and consent rules.
PAR moves the detailed authorization request from a browser-carried URL into a direct request from the client to the authorization server. The response supplies a request_uri and expiry; the browser uses that reference. This reduces the amount of request content traveling through the browser. Source: RFC 9126.
The following is an illustrative parameter set, not a complete HTTP request. It omits client authentication and any ecosystem-specific consent parameters. Do not paste it into a sandbox and expect it to work unchanged.
response_type=code
client_id=YOUR_REGISTERED_CLIENT_ID
redirect_uri=https://client.example/callback
scope=accounts
code_challenge=VALUE_DERIVED_FROM_THIS_TRANSACTION_VERIFIER
code_challenge_method=S256
A useful debugging question at this stage is: “Did the server reject my client identity, my requested parameters, or the request reference later used by the browser?” These are different failure points. Log a correlation ID and the stage, rather than a complete request containing secrets.
3. Make PKCE concrete with a local exercise
PKCE connects the authorization request to the later code exchange. The client retains a random verifier and sends a derived challenge first; the verifier is presented when redeeming the code. With S256, the challenge is the base64url-encoded SHA-256 digest of the verifier. Base64url encoding is not interchangeable with ordinary padded Base64. Source: RFC 7636.
Download pkce-lab.mjs and run it with Node.js 22.19.0. It uses only built-in modules and makes no network calls:
node pkce-lab.mjs
// Local PKCE mechanics exercise; not a FAPI server or conformance test.
import assert from 'node:assert/strict';
import { createHash, randomBytes } from 'node:crypto';
export function challengeFor(verifier) {
if (!/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) {
throw new Error('Verifier must be 43–128 unreserved ASCII characters');
}
return createHash('sha256').update(verifier, 'ascii').digest('base64url');
}
const verifier = randomBytes(32).toString('base64url');
const challenge = challengeFor(verifier);
assert.equal(verifier.length, 43);
assert.equal(challenge.length, 43);
assert.match(challenge, /^[A-Za-z0-9_-]+$/);
assert.equal(challengeFor(verifier), challenge);
assert.notEqual(challengeFor(randomBytes(32).toString('base64url')), challenge);
assert.throws(() => challengeFor('too-short'));
console.log('PASS verifier length, challenge length, base64url, matching verifier, wrong verifier, malformed verifier');
console.log('6 checks passed');
Expected output:
PASS verifier length, challenge length, base64url, matching verifier, wrong verifier, malformed verifier
6 checks passed
The assertions catch a few easy mistakes: a verifier that is too short, a challenge using the wrong alphabet, or a different verifier at redemption. They do not demonstrate code storage, replay protection, client authentication or a server's enforcement. Those belong in integration tests against the actual authorization server.
In an application, store each verifier with a single transaction. Do not reuse a global verifier across users. Delete completed or expired transaction state, and make the callback handling tolerate an expected error without silently starting an unrelated authorization. These choices make failures understandable when a user opens two tabs or abandons a flow.
4. Client authentication and token binding solve different problems
Client authentication asks: which application is contacting the token service? Sender constraint asks: can the party presenting this access token demonstrate possession of its associated key? A signed client assertion does not automatically bind the resulting access token.
| Mechanism | Where to investigate | A common mistake |
|---|---|---|
| mTLS client authentication | Client certificate and the server's registered client identity | Offering a valid certificate that is not registered for this client. |
private_key_jwt | Signed assertion and client public-key registration | Using the wrong audience or a key from another environment. |
| Certificate-bound access token | The certificate associated with the token and the certificate used at the API | Switching certificates after obtaining the token. |
| DPoP-bound access token | Proof key, proof contents and resource-server validation | Treating a DPoP token like an ordinary bearer token. |
With certificate-bound tokens, the resource server checks that the presented certificate corresponds to the token's binding. An infrastructure change that replaces or terminates the client TLS connection therefore deserves explicit testing. Client authentication and certificate-bound tokens are distinct uses of mTLS in RFC 8705.
DPoP uses a signed proof associated with the request. At a protected resource, its checks include the HTTP method and URI, token association, timing and replay handling. Nonces can require a fresh proof on retry. A copied token is not sufficient without the associated private key, but stealing both defeats that protection. Source: RFC 9449.
The practical consequence is to design separate key inventories. Track client-authentication keys, DPoP keys and transport certificates by purpose, environment and owner. During rollover, test existing sessions as well as new authorizations; a successful new login can conceal a broken token-bound session.
5. Build an integration test matrix
The table below is a proposed test plan, not a report of tests run against a bank. Capture the actual server response and the specification clause for each scenario. Exact error descriptions can vary by implementation; your acceptance criterion should be the security behavior.
| Scenario | What to establish |
|---|---|
| Valid end-to-end flow | The authorized API operation succeeds with the correct proof and permissions. |
| Wrong PKCE verifier | The code cannot be redeemed. |
| Missing client authentication at PAR | The pushed request is rejected. |
| Reused or expired authorization code | The server does not issue a new usable token. |
| Unexpected callback issuer | The client stops before exchanging the code. |
| Token presented with another binding key | The resource request is denied. |
| DPoP nonce challenge, if applicable | The client handles it with a new proof rather than replaying a stale one. |
| Rotated certificate or signing key | Both the intended overlap and the retirement behavior are exercised. |
| Revoked consent | The integration follows the ecosystem's consent enforcement rules. |
A test plan should also cover operational failures: unavailable metadata, a failed key refresh, interrupted code redemption and clock drift. Use bounded retries and make it possible to distinguish an upstream outage from rejected credentials. Neither case should result in bypassing verification.
6. Plan a migration in small, observable steps
Begin with a configuration inventory, then compare one complete transaction in the old and new environments. Record issuer identifiers, redirect URIs, client authentication, token binding, algorithms and endpoint aliases. Avoid combining a protocol migration, key rotation and infrastructure migration into one untraceable change.
Run the relevant OpenID Foundation conformance tests alongside your ecosystem's onboarding requirements. Keep the distinction between a product's certification and the configuration you deploy. The six local PKCE checks above are useful regression coverage, but are not a substitute for that process.
Use the tools for inspection, with clear limits
The JWT Decoder & Validator helps inspect test assertions; the PEM Decoder helps read certificate details; and the JSON Formatter helps inspect metadata. These tools do not certify a FAPI deployment. Continue with the JWT verification failure lab for signature and claims errors, or the DCR overview for registration terminology.
Common questions
Does a browser-only application become confidential by adding PKCE?
No. PKCE does not make a browser-held secret confidential. Choose an architecture appropriate to the profile, such as a backend that can protect the client's credentials.
Must every ecosystem choose both mTLS and DPoP?
Do not assume that. Check the supported and required combinations in the specific ecosystem and server metadata.
Can a JWT decoder tell me whether the whole flow is secure?
No. A decoded assertion says little about redirect handling, replay controls, consent enforcement or the resource server's token-binding checks.
Method: AI-assisted drafting checked against the linked specifications. The downloadable PKCE exercise was executed locally on Node.js 22.19.0. The integration matrix is guidance for future testing, not a completed certification or bank-sandbox test.