Thoryn
  • security
  • oauth2
  • fapi
  • compliance

FAPI compliance — what we actually implement

FAPI-compliant is on a lot of vendor sites. Here is what FAPI actually requires, the three protocols we ship — PAR, DPoP, mTLS — the test that proves it, and the parts we do not claim yet.

20 May 2026 · Mark Bakker

"FAPI-compliant" shows up on the security page of more or less every identity vendor. Three letters with weight behind them, and the gap between the marketing version and a working implementation is wide. We have the test that proves which side of the gap we're on: a system test runs every required FAPI check against a live authorization server on every CI build. Each profile control passes or the build is red.

This post is what FAPI compliance means in the Authorization Hub today. Three protocols: PAR (RFC 9126) for tamper-resistant authorization requests, DPoP (RFC 9449) for sender-constrained tokens, mTLS (RFC 8705) for certificate-bound client authentication. Plus the parts we don't claim yet — because the omission is as load-bearing as the rest.

What FAPI actually requires

The Financial-grade API security profile is a tightening of OAuth 2.0 / OIDC for high-stakes APIs — open banking, payments, healthcare, anywhere a leaked token has real consequences. Two profiles matter.

FAPI 1.0 Advanced. Pushed authorization requests, signed request objects, mandatory nonce, response_type=code only, access tokens ≤ 300 s.

FAPI 2.0 Security Profile. PAR is no longer optional — every authorization request is pushed. Sender-constrained tokens are no longer optional either — every token is bound to a DPoP key or an mTLS client certificate. PKCE is mandatory. client_secret_basic and client_secret_post are out at the token endpoint; you authenticate with private_key_jwt or mTLS. Bearer-as-bearer is dead.

If you take FAPI 2.0 seriously, three protocols stop being nice-to-haves and become floor requirements. Each gets its own section below.

Three pillars of FAPI compliance — PAR, DPoP, mTLS — each with a per-client policy and a system test mapped to it

PAR — pushed authorization requests

In a standard authorization-code flow, the client builds a long URL with response_type, client_id, redirect_uri, scope, state, nonce, code_challenge and sends the user's browser there. Every parameter rides through a chain you don't control — user agent, corporate proxies, browser history, referer headers. Anything attacker-adjacent that can read or modify the URL has a shot at it.

PAR inverts the flow. The client POSTs the parameters to /oauth2/par with backchannel client authentication; the server returns an opaque request_uri. The browser only sees ?client_id=...&request_uri=urn:par:abc123. The interesting parameters never traverse the user agent.

Implementation lives in our FAPI library, behind a pluggable PAR-store interface. Only the Redis-backed store is wired up in production — PAR state must be visible to every replica, and an in-process map gets you split-brain the moment the load balancer routes /oauth2/authorize to a different pod than the one that handled /oauth2/par. A 120-second TTL prunes abandoned requests. The FAPI 1.0 enforcement filter rejects any authorization request without a request_uri, missing nonce, or response_type=token — at the perimeter, before any authentication step.

DPoP — proof of possession

Bearer is bearer. Whoever has the token bytes is, as far as the resource server is concerned, the user. Lift a token from a log file, a memory dump, a misconfigured CDN — you can use it. The protocol gives you no way to ask "is the holder the one we issued this to?"

DPoP — Demonstrating Proof of Possession — answers that. The client generates a keypair, sends a per-request signed JWT (the DPoP proof) along with every token-protected call, and the issuer binds the access token to the public key's thumbprint via a cnf.jkt claim. Steal the token without the key, and the check cnf.jkt == sha256(presented_jwk) fails.

A per-client DPoP-required flag drives the policy. The proof is validated at the token endpoint — fresh iat (within 60 s), correct method (htm: POST), correct URI (htu: http://.../oauth2/token), signature verifying against the JWK in the proof header. Each is a separate test, each rejection a typed 400 with invalid_dpop_proof. The discovery document advertises dpop_signing_alg_values_supported.

mTLS — certificate-bound tokens

DPoP is the application-layer answer to bearer-as-bearer. mTLS is the transport-layer answer. The client presents an X.509 certificate at TLS handshake; the issuer binds the token to the SHA-256 thumbprint (cnf.x5t#S256); the resource server checks the cert on the API call matches the thumbprint on the token. Token without the corresponding private key — useless.

A per-client mTLS-required flag drives the policy. Two methods are supported: tls_client_auth (PKI-backed) and self_signed_tls_client_auth. The certificate path lives behind a configurable proxy header so TLS termination can sit at any reverse proxy without baking PKI into the JVM. The mTLS test suite covers cert presence and absence, thumbprint match, the invalid_client rejection when a required-mTLS client presents none, and DPoP-and-mTLS coexisting without server error. Discovery advertises tls_client_certificate_bound_access_tokens: true.

mTLS is operationally heavier than DPoP. Customers running their own PKI tend to want it; the rest pick DPoP. FAPI 2.0 accepts either — what it doesn't accept is neither.

The test that ties it together

The FAPI compliance system test runs against the live application context with Testcontainers and asserts each profile control:

  • FAPI 1.0 authorization without request_uri / nonce / response_type=codeinvalid_request
  • FAPI 1.0 access-token TTL ≤ 300 s
  • FAPI 2.0 client_secret_postinvalid_client
  • FAPI 2.0 token request with neither DPoP nor mTLS → invalid_request; with either → 200
  • private_key_jwt client assertion verified against the configured JWKS URI
  • JAR (signed request parameter) decoded, signature verified, params merged
  • non-FAPI clients unaffected by FAPI enforcement

If any assertion fails, the build is red. The other reason it's in CI is regression: FAPI enforcement is a per-client opt-in, and every change to the token endpoint, the auth filter, the customizer chain, or the discovery document risks weakening enforcement on FAPI clients. The test catches it.

What we don't claim

A vendor's compliance page is most informative in what it omits. Two omissions matter.

Full FAPI 2.0 message-signing isn't shipped. FAPI 2.0 has an Advanced profile on top of Security that mandates signed JWS responses for the token, userinfo, and introspection endpoints, plus signed authorization responses (JARM). We've shipped the request side — JAR, PAR, signed client assertions — but not the response side. It's on the roadmap; it isn't shipped today. Saying "FAPI 2.0 compliant" without that caveat would be marketing, not engineering.

We don't run the OpenID Foundation conformance suite in CI yet. The OIDF publishes a test suite that exercises FAPI implementations against an external harness. We pass our own assertions; we haven't run the OIDF suite end-to-end and we have no public certification on the OIDF directory. That's deliberate phasing — internal assertions cover the failure modes our current customers care about; OIDF conformance is the milestone before we accept Open Banking-style regulated workloads. Cited intent, not promise.

Both get glossed over at the vendor-comparison stage. We'd rather call them out than have a customer find out post-contract.

If you're evaluating vendors

Four questions, each answerable in under a minute:

  1. "Is PAR required on every FAPI authorization request, or optional?" Optional PAR isn't FAPI 2.0.
  2. "Do you bind every issued access token to a DPoP key or an mTLS cert?" Anything else is bearer.
  3. "Do you have a test that rejects client_secret_post for FAPI 2.0 clients?" If there isn't one, the rejection isn't being checked.
  4. "What part of FAPI 2.0 do you not yet implement?" Useful answers are specific. Useless answers are "none".

The protocol is unforgiving; the implementations vary; the marketing is uniform. Knowing where to look is most of the work.

If you want to see the test run, request access and we'll point you at the CI build.