Introduction
There is a world where a single compromised API means money leaves a bank account. That world is open banking, open data, and open finance. The OpenID Foundation FAPI (Financial-grade API) working group was born from the realization that "general OAuth 2.0 best practices" are not enough in such an environment, and its flagship deliverable is the [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-2_0-security-profile.html).
As of 2026, FAPI is no longer a finance-only story. The FAPI 2.0 Security Profile and Attacker Model have been finalized as Final specifications, and nation-scale ecosystems — Brazil Open Finance, UK Open Banking, Australia CDR, Saudi Arabia Open Banking — run on FAPI. Domains that need high assurance, such as healthcare and government digital services, are adopting it too. And with [Keycloak 26.6](https://www.keycloak.org/docs/latest/release_notes/index.html) officially supporting the Final versions of the FAPI 2.0 Security Profile and Message Signing, the environment for building financial-grade profiles on an open-source IdP is now complete.
This article covers the background of the evolution from FAPI 1.0 to 2.0, the core requirements, how to choose between DPoP and mTLS, Keycloak configuration, and the lessons general-purpose services can take from FAPI.
The Problem FAPI Solves — the Attacker Model
The starting point of FAPI 2.0 is a separate document called the [Attacker Model](https://openid.net/specs/fapi-2_0-attacker-model.html). It assumes a far stronger attacker than plain OAuth implicitly does.
- An attacker on the network: can read and tamper with authorization requests and responses
- A malicious resource server: a fake RS to which a client might mistakenly send tokens
- Authorization server mix-up: response confusion when a client talks to multiple AS instances
- Leaked authorization requests/responses: exposure via browser history, logs, and referrers
The goal of FAPI 2.0 is that token misuse and session hijacking remain impossible even under this attacker model — and its security properties have actually been proven through formal analysis. The biggest differentiator of FAPI 2.0 is that it is not a "collection of best practices" but a "proven security profile."
From FAPI 1.0 to 2.0 — A History of Simplification
FAPI 1.0 was split into Baseline and Advanced profiles, and Advanced demanded implementation-heavy mechanisms: the hybrid flow, JARM, signed request objects. Interpretations diverged across implementations, and taking months to pass conformance testing was common.
FAPI 2.0 was redesigned with the direction of "keep the security level, lower the complexity."
| Aspect | FAPI 1.0 Advanced | FAPI 2.0 Security Profile |
|--------|-------------------|---------------------------|
| Profile structure | Split into Baseline + Advanced | Single Security Profile + optional Message Signing |
| Authorization request protection | Signed request objects (JAR) or hybrid flow | Unified on PAR |
| Authorization response protection | JARM or hybrid-flow ID token | authorization code + PKCE suffices |
| Response types | Mixed, including code id_token | code only |
| Token binding | mTLS-centric | Choice of DPoP or mTLS |
| Formal verification | Partial | Entire profile formally verified |
The core insight: if you pre-register the authorization request over the back channel (PAR) instead of sending it through the front channel (browser redirect), you no longer need elaborate signing machinery to defend against request tampering. Likewise, PKCE plus issuer identification (the iss response parameter) blocks code injection and mix-up, so the complexity of the hybrid flow could be removed as well.
Core Requirements of the FAPI 2.0 Security Profile
Compressed, what FAPI 2.0 demands of authorization servers and clients is:
1. PAR (Pushed Authorization Requests, RFC 9126) mandatory — every authorization request is pre-registered over the back channel
2. PKCE (S256) mandatory — no exception even for confidential clients
3. response_type is code only
4. Sender-constrained access tokens mandatory — DPoP or mTLS certificate binding
5. Client authentication via private_key_jwt or mTLS (tls_client_auth) — the client_secret family is forbidden
6. The iss response parameter (RFC 9207) to defend against mix-up attacks
7. Refresh tokens are sender-constrained too
8. RFC 9700-grade fundamentals: single-use authorization codes, exact redirect_uri matching, and so on
The full flow looks like this.
+----------+ +---------------+
| Client | | Auth Server |
+----+-----+ +-------+-------+
| (1) POST /par |
| private_key_jwt + PKCE + all request params |
| ---------------------------------------------> |
| (2) request_uri (single-use reference) |
| <--------------------------------------------- |
| |
| (3) browser redirect: client_id+request_uri |
| ---------------------------------------------> |
| (4) after user auth/consent: code + iss |
| <--------------------------------------------- |
| |
| (5) POST /token |
| code + code_verifier + private_key_jwt |
| (+ DPoP proof or mTLS) |
| ---------------------------------------------> |
| (6) sender-constrained access token |
| <--------------------------------------------- |
| |
| (7) API call: token + DPoP proof/mTLS cert |
v v
PAR (RFC 9126) — Authorization Requests over the Back Channel
A traditional authorization request loads every parameter into a query string and sends it through the browser. Parameters can be tampered with, they end up in browser history and server logs, and URL length limits bite. [PAR](https://datatracker.ietf.org/doc/html/rfc9126) registers the authorization request body in advance over an authenticated back channel and sends only a reference (request_uri) through the browser.
POST /realms/fin/protocol/openid-connect/ext/par/request HTTP/1.1
Host: idp.bank.example
Content-Type: application/x-www-form-urlencoded
client_id=tpp-client
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJQUzI1NiIs...
&response_type=code
&redirect_uri=https%3A%2F%2Ftpp.example%2Fcallback
&scope=accounts%20payments
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=af0ifjsldkj
The response looks like this.
{
"request_uri": "urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c",
"expires_in": 90
}
The subsequent browser redirect becomes radically simple.
GET /authorize?client_id=tpp-client
&request_uri=urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c
Because the authorization server already authenticated the client and pinned the parameters at registration time, tampering in the browser segment is blocked at the root. The request_uri is single-use and short-lived (typically around 90 seconds).
RAR (RFC 9396) — Fine-Grained Authorization at Transaction Level
Scope strings struggle to express transaction-level authorization like "transfer 150,000 KRW from account A to B." [RAR (Rich Authorization Requests)](https://datatracker.ietf.org/doc/html/rfc9396) solves this with a JSON structure named authorization_details.
{
"authorization_details": [
{
"type": "payment_initiation",
"instructedAmount": {
"currency": "KRW",
"amount": "150000"
},
"creditorAccount": {
"iban": "KR123456789012345678"
},
"remittanceInformation": "2026-06 rent"
}
]
}
This structure is included in the PAR request, displayed verbatim on the user consent screen, and baked into the issued access token as authorization_details. The resource server verifies that the transaction details inside the token match the actual API request. "Only what was consented to, only as much as was consented to" becomes cryptographically enforced. RAR is not mandatory in FAPI 2.0, but in payment scenarios it is the de facto standard companion.
Sender-Constrained Tokens — DPoP vs mTLS
The most important requirement of FAPI 2.0 is the ban on plain bearer tokens — sender-constrained tokens are mandatory. Even if a token leaks, only the legitimate holder must be able to use it. There are two options.
mTLS certificate binding (RFC 8705)
[RFC 8705](https://datatracker.ietf.org/doc/html/rfc8705) binds the thumbprint of the TLS client certificate to the token. The certificate hash goes into the token's cnf claim.
{
"sub": "user-1234",
"aud": "https://api.bank.example",
"cnf": {
"x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
}
}
The resource server compares the hash of the client certificate presented in the TLS handshake against the cnf value in the token. Since enforcement happens automatically at the TLS layer, application code changes are minimal, but the certificate issuance/rotation infrastructure (PKI) and management of TLS termination points are demanding. In particular, when a load balancer terminates TLS, you need extra configuration to forward certificate information to the backend.
DPoP (RFC 9449)
[DPoP](https://datatracker.ietf.org/doc/html/rfc9449) attaches a proof JWT to every request at the application layer. The client generates a public/private key pair and sends a proof signed with that key in the DPoP header on each token request and API call. The token binds the public key thumbprint as jkt inside cnf.
{
"sub": "user-1234",
"aud": "https://api.bank.example",
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}
Comparison of selection criteria
| Criterion | mTLS (RFC 8705) | DPoP (RFC 9449) |
|-----------|-----------------|-----------------|
| Operating layer | TLS (transport) | HTTP (application) |
| Infrastructure demands | PKI, mTLS-capable LB/gateway | None (client generates keys) |
| Public clients (SPA/mobile) | Practically impossible | Well suited |
| Doubles as client authentication | Yes (tls_client_auth) | No (token binding only) |
| Traversing proxies/CDNs | Needs config at each TLS termination | Passes through transparently |
| Implementation complexity | Concentrated on the infra side | Concentrated on client code |
| Typical adopters | Bank-to-bank B2B, closed networks | Fintech apps, public clients |
Roughly summarized: mTLS is natural for server-to-server traffic in environments with existing PKI, while DPoP fits environments involving mobile apps and SPAs. Many ecosystems mix the two (for instance private_key_jwt for client authentication and DPoP for token binding).
FAPI 2.0 Message Signing — When You Need Non-Repudiation
If the Security Profile covers "protection in transit," [FAPI 2.0 Message Signing](https://openid.net/specs/fapi-2_0-message-signing.html) adds non-repudiation. It exists for regulated environments where, in a dispute, you must prove to a third party that "this request/response was exchanged with exactly this content."
- Signing authorization requests: signed request objects via JAR (RFC 9101)
- Signing authorization responses: receiving the response as a signed JWT via JARM
- Signing resource requests/responses: leveraging HTTP Message Signatures (RFC 9421)
Message Signing can be applied selectively, only to the message types that need it. It is mainly adopted in markets where regulation requires it, such as Brazil Open Finance.
Configuring FAPI 2.0 in Keycloak 26.6
Keycloak enforces FAPI requirements through its client policies mechanism. 26.6 ships built-in profiles corresponding to the Final specifications of the FAPI 2.0 Security Profile and Message Signing. The built-in global profile names are fapi-2-security-profile and fapi-2-message-signing.
A client policy is a combination of "conditions (which clients)" + "profiles (which requirements)." For example, a policy that enforces FAPI 2.0 on all clients holding a particular client role can be defined in JSON as follows.
{
"policies": [
{
"name": "fapi2-for-openbanking-clients",
"description": "Enforce FAPI 2.0 Security Profile on all open banking clients",
"enabled": true,
"conditions": [
{
"condition": "client-roles",
"configuration": {
"roles": ["openbanking-tpp"]
}
}
],
"profiles": ["fapi-2-security-profile"]
}
]
}
Apply it with the Admin CLI.
inspect current policies
kcadm.sh get client-policies/policies -r fin
update policies (assuming the JSON above is saved as policies.json)
kcadm.sh update client-policies/policies -r fin -f policies.json
If a client under this policy violates FAPI requirements — say, it makes an authorization request without PAR, authenticates with a client_secret, or omits PKCE — Keycloak rejects the request outright. Executors also run at client registration time, blocking non-compliant configuration (for example a disallowed signing algorithm).
Realm-level settings worth checking alongside:
PAR request lifetime (default 60s; keep within FAPI-recommended range)
kcadm.sh update realms/fin -s 'attributes."parRequestUriLifespan"="90"'
verify DPoP enablement (supported in 26.x)
kcadm.sh get realms/fin --fields attributes
enforce mTLS token binding on a client
kcadm.sh update clients/CLIENT-UUID -r fin \
-s 'attributes."tls.client.certificate.bound.access.tokens"="true"'
26.6 also added EdDSA signing support, widening future algorithm policy options. Note, however, that the algorithms FAPI 2.0 permits center on the PS256 and ES256 families, so check your ecosystem's requirements first.
Conformance Testing — the Conformance Suite
A major advantage of FAPI is that "did we comply" can be verified mechanically. The [conformance suite](https://www.certification.openid.net/) provided by the OpenID Foundation automatically runs hundreds of scenarios against authorization servers and clients — happy paths, tampered requests, bad signatures, expired tokens, and more.
Operational tips:
1. Run the suite locally before pursuing certification. The suite is open source and runs in Docker.
2. The environment under test must have the same TLS/proxy configuration as production. Many failures come from an LB rewriting headers.
3. Failure messages cite spec clause numbers, so reading them with the spec document open beside you is fastest.
4. Keycloak is a FAPI-certified implementation, but your deployment configuration (reverse proxies, custom SPIs) can break conformance. Re-validating per deployment is the safe approach.
A Korean Financial-Sector Perspective — MyData APIs and FAPI
Korea's MyData (personal credit information management) ecosystem uses a standard API specification led by the Financial Security Institute. The current specification did not adopt FAPI wholesale, but it shares many structural elements — mutual authentication between institutions, transport encryption, access-token-based authorization, and fine-grained consent for data provision.
When designing or improving MyData-style systems from a FAPI 2.0 perspective, the takeaways are:
- Binding consent into the token: a design that structurally embeds the consent scope into the token, like RAR's authorization_details, technically blocks "queries beyond consent" incidents.
- Moving off bearer tokens: consider mTLS binding for institution-to-institution APIs and DPoP for segments involving user devices.
- International alignment: if interoperability with overseas open banking or global fintech expansion matters, layering a FAPI 2.0-compatible profile on top of the domestic specification is an effective strategy.
- A conformance-testing culture: moving from spec documents plus manual checks toward conformance-suite-style automated interoperability validation reduces costs across the whole ecosystem.
What General-Purpose Services Can Learn from FAPI
Even if your service is not financial, FAPI 2.0 is the reference for "how far can you go when you use OAuth properly." Ordered by cost-effectiveness:
1. PKCE everywhere — mandatory in OAuth 2.1 anyway. Turn it on now.
2. Exact redirect_uri matching and single-use codes — achievable with configuration changes alone.
3. Adopt PAR — requires client changes, but structurally eliminates authorization-request tampering and log leakage.
4. Switch client authentication to private_key_jwt — frees you from shared-secret leakage and rotation problems.
5. DPoP for high-risk APIs only — if full adoption is too heavy, start with payment and personal-data APIs.
Conversely, blanket adoption of Message Signing or mTLS may be over-investment if no regulation demands it. Remember that FAPI 2.0 itself is a profile simplified under the philosophy of "only as much as needed."
Global Ecosystem Landscape and a Phased Adoption Roadmap
Comparing the major ecosystems that have adopted or are transitioning to FAPI 2.0:
| Ecosystem | Base profile | Token binding | Characteristics |
|-----------|--------------|---------------|-----------------|
| UK Open Banking | Transitioning from FAPI 1.0 Advanced to 2.0 | mTLS-centric | The oldest large-scale operation |
| Brazil Open Finance | FAPI 2.0 + Message Signing | mTLS | Signing adopted for non-repudiation demands |
| Australia CDR | FAPI 1.0 based, 2.0 on the roadmap | mTLS | Expanding beyond banking into energy and other sectors |
| Saudi Arabia | FAPI 2.0 | mTLS/DPoP | Designed on 2.0 from the start |
A phased roadmap proposal for organizations adopting fresh:
Phase 0 Assess the current state
- inventory clients, auth methods, PKCE adoption rate
Phase 1 Build the foundation (no compatibility impact)
- PKCE S256 on all clients
- exact redirect_uri matching, single-use codes
- add RFC 9207 iss parameter validation
Phase 2 Migrate client authentication
- client_secret -> private_key_jwt migration
- operate a JWKS endpoint, establish key rotation
Phase 3 Protect the request
- adopt PAR, enforce require_pushed_authorization_requests
Phase 4 Bind the tokens
- apply DPoP or mTLS to pilot clients
- deploy cnf validation on resource servers
Phase 5 Policy enforcement and verification
- enforce realm-wide via Keycloak client policies
- build a conformance suite regression pipeline
Each phase is valuable on its own, so the security level rises incrementally even before full completion.
Implementing cnf validation on the resource server
The last mile of FAPI is the resource server. An example of validating the cnf of an mTLS-bound token on a Spring-based resource server:
public class CertificateBoundTokenValidator
implements OAuth2TokenValidator<Jwt> {
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
Map<String, Object> cnf = jwt.getClaim("cnf");
if (cnf == null || cnf.get("x5t#S256") == null) {
return OAuth2TokenValidatorResult.failure(
new OAuth2Error("invalid_token",
"cnf claim missing - sender-constrained token required",
null));
}
String boundThumbprint = (String) cnf.get("x5t#S256");
// compute the thumbprint from the client cert forwarded by the proxy
String presentedThumbprint = CertificateThumbprintHolder.current();
if (!boundThumbprint.equals(presentedThumbprint)) {
return OAuth2TokenValidatorResult.failure(
new OAuth2Error("invalid_token",
"certificate thumbprint mismatch", null));
}
return OAuth2TokenValidatorResult.success();
}
}
The essential rule is "no cnf, no entry." In an environment that mandates sender-constrained tokens, letting cnf-less tokens through opens a downgrade attack path.
Troubleshooting and Anti-Patterns
Problems frequently encountered when applying the FAPI profile:
Symptom Cause and remedy
----------------------------------------------------------------
invalid_request: PAR required Client went straight to
/authorize without request_uri.
Check the client SDK's PAR support.
invalid_client (at PAR step) aud/exp errors in private_key_jwt,
JWKS not registered, or old key
used after rotation.
invalid_dpop_proof htm/htu mismatch in the proof
(proxy rewriting URLs), iat clock
skew, jti reuse.
401 from cnf mismatch LB terminated TLS and did not
forward the certificate. Configure
certificate header forwarding.
PKCE verification failure code_verifier storage issues
(non-sticky sessions in multi-
instance environments).
Anti-patterns to call out:
- Applying the FAPI policy to only some clients while leaving the same API open to non-FAPI clients — the security level converges to the weakest path.
- Extending the request_uri lifetime — single-use and short life are part of PAR's security rationale.
- Changing configuration after passing conformance once, without re-validation — regression tests are needed whenever proxies, algorithms, or SPIs change.
Closing Thoughts
FAPI 2.0 is a milestone for the OAuth ecosystem: a security profile that is formally verified under a strong attacker model yet remains implementable. To summarize:
- The complexity of FAPI 1.0 (mandatory hybrid flow, JARM) was simplified into PAR + PKCE + the code flow.
- Sender-constrained tokens are the core. Choose mTLS or DPoP according to your infrastructure maturity.
- Keycloak 26.6's built-in client policies let you enforce FAPI 2.0 Final declaratively.
- Verify conformance mechanically with the conformance suite, and re-run it whenever deployment configuration changes.
- Even outside finance, PKCE, PAR, and private_key_jwt are universal wins.
Wherever high-assurance authorization is needed, FAPI 2.0 offers a starting point that has already been validated. Before reinventing the wheel, read this profile first.
References
- [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-2_0-security-profile.html)
- [FAPI 2.0 Attacker Model](https://openid.net/specs/fapi-2_0-attacker-model.html)
- [FAPI 2.0 Message Signing](https://openid.net/specs/fapi-2_0-message-signing.html)
- [RFC 9126 — OAuth 2.0 Pushed Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9126)
- [RFC 9396 — OAuth 2.0 Rich Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9396)
- [RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://datatracker.ietf.org/doc/html/rfc9449)
- [RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://datatracker.ietf.org/doc/html/rfc8705)
- [RFC 9207 — OAuth 2.0 Authorization Server Issuer Identification](https://datatracker.ietf.org/doc/html/rfc9207)
- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)
- [RFC 7636 — Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636)
- [OpenID Foundation Conformance Suite](https://www.certification.openid.net/)
- [Keycloak Documentation](https://www.keycloak.org/documentation)
- [Keycloak Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
현재 단락 (1/267)
There is a world where a single compromised API means money leaves a bank account. That world is ope...