FAPI 2.0 — a primer
FAPI stands for Financial-grade API. It is a family of high-security profiles maintained by the OpenID Foundation and based on OAuth 2.0. The FAPI 2.0 Security Profile uses OpenID Connect when a deployment needs to return an authenticated user's identifier, but it does not require OIDC for every use case.
The original target was open banking. Open banking and open finance schemes in the UK, Australia, Brazil, and elsewhere have used FAPI or related regional profiles between banks and third-party fintechs. The same profile is increasingly adopted in healthcare, government identity, and any setting where "we follow the RFC" isn't a sufficient audit answer.
Open Banking — what is it?
"Open Banking" is the regulatory framework that lets a customer authorize a third-party app to read account data or initiate payments on their behalf, instead of handing the third party their bank password. The UK's PSD2-driven Open Banking (2018), Brazil's Open Finance (2020), and Australia's Consumer Data Right (2020) are the largest examples. Their security standards have used FAPI and related profiles in different versions and combinations. If you're not in this regulated space you don't need FAPI for compliance reasons — but the same profile is a sensible default any time you'd find a CVE-class vulnerability hard to live with.
Specs referenced on this page
- FAPI 2.0 Security Profile — Final (the former Baseline specification)
- FAPI 2.0 Message Signing — Final
- FAPI 2.0 Attacker Model — Final
- RFC 9126 — Pushed Authorization Requests (PAR)
- RFC 9101 — JWT-Secured Authorization Request (JAR)
- JARM — JWT-Secured Authorization Response Mode
- RFC 7636 — PKCE
- RFC 9449 — DPoP
- RFC 8705 — Mutual-TLS Client Authentication
- RFC 7518 — JOSE algorithms
Why a profile, not just "OIDC plus best practices"
OAuth 2.0 and related specifications leave many deployment choices open:
- The authorize request can travel as a plain query string.
- Implementations may expose more signing algorithms than a high-security deployment needs.
- Client authentication can use
client_secret_basic(a long-lived shared secret).
Those choices have contributed to serious implementation vulnerabilities when they are combined incorrectly.
A profile is a contract that fixes the security-critical choices for a deployment. FAPI 2.0 fixes those choices and leaves explicit options where the underlying specifications allow them. Compliance becomes a checklist instead of an argument.
FAPI 1.0 vs FAPI 2.0
You may still see references to FAPI 1.0 (split into "Read-Only" and "Read-Write"). FAPI 2.0 is its successor with a formally analysed attacker model and a single general-purpose security profile. Message Signing is a separate FAPI 2.0 profile that adds non-repudiation methods:
| FAPI 1.0 (still in use) | FAPI 2.0 | |
|---|---|---|
| Profiles | Read-Only / Read-Write | Security Profile / Message Signing |
| Authorize request transport | Optional request JWT | PAR mandatory |
| Sender constraint | mTLS or holder-of-key | DPoP or mTLS mandatory |
response_type | Allowed code id_token (hybrid) | code only |
| JWT algorithms | Profile-specific choices | PS256, ES256, or EdDSA |
FAPI 1.0 Read-Only / Read-Write — what's the difference?
FAPI 1.0 had two profiles. Read-Only was for use cases that only retrieved data (for example, an app reading an account balance). Read-Write added requirements for use cases that initiated state changes (for example, an app instructing a payment). FAPI 2.0 uses the Security Profile as its general security baseline and defines Message Signing separately for deployments that need non-repudiation. Message Signing does not replace the Security Profile; it extends it.
go-oidc-provider targets FAPI 2.0; FAPI 1.0 is not supported.
Security Profile and Message Signing
FAPI 2.0 has a general Security Profile and a separate Message Signing profile. The Go library keeps the historical profile.FAPI2Baseline identifier for the former.
FAPI is not a separate protocol. It fixes how OAuth 2.0 and its related specifications are used for a high-security deployment, which is why the diagram places the profiles on top of the base specifications.
Security Profile — the required baseline
The FAPI 2.0 Security Profile requires confidential clients, sender-constrained access tokens using DPoP or mTLS, PAR, PKCE with S256, response_type=code, an iss authorization-response parameter, and secure TLS endpoints. Token-endpoint client authentication uses mTLS or private_key_jwt. Its JWT algorithm set is PS256, ES256, or EdDSA; it does not require JAR or an ES256-only implementation.
Message Signing — non-repudiation
Message Signing inherits the Security Profile and defines three independently testable methods: JAR-signed pushed authorization request objects, JARM-signed authorization responses, and signed introspection responses using RFC 9701. It does not require DPoP nonces and does not sign every resource request or response. The library's profile.FAPI2MessageSigning auto-enables PAR, JAR, and JARM; when feature.Introspect is also enabled, it forces that endpoint to emit signed introspection responses. With DPoP active, the library validator also requires WithDPoPNonceSource; that is a library constraint, not a Message Signing requirement. The profile does not mount the introspection endpoint itself.
Regional open banking and open insurance schemes may add message-signing requirements; check the scheme's current security standard separately from the FAPI specification.
Message signing — what does that buy you?
"Message signing" in FAPI 2.0 covers selected OAuth messages: a JAR-signed pushed authorization request object sent to PAR, the JARM-signed authorization response, and the signed introspection response. With PAR, the browser's front-channel request carries only request_uri and client_id; it is not a separately signed authorization request for non-repudiation. The receiver verifies the signature against the sender's published public key. The profile does not define a JWS wrapper for every resource API request and response.
JWS — what is that?
JWS = "JSON Web Signature" (RFC 7515). It is a signed representation whose payload can be arbitrary bytes. A signed JWT uses the JWS compact serialization with a JSON claims payload; a JWT can also be encrypted as a JWE. When FAPI talks about a signed authorization request or response, it means the corresponding protocol object is carried in a signed JWT or JWS as required by that method.
What each acronym actually does
PAR — Pushed Authorization Request (RFC 9126)
The classical OAuth flow has the RP build an /auth?... URL with all parameters in the query string and tell the browser to navigate there. Anyone in the middle (browser extensions, intermediary proxies, referrer leaks, server logs) sees the parameters.
PAR has the RP first POST the parameters to the OP's /par endpoint directly, server-to-server with client authentication. The OP returns a short-lived request_uri such as urn:ietf:params:oauth:request_uri:abc.... The browser is then redirected to /auth?request_uri=urn:...&client_id=... and the OP looks up the original parameters internally.
Result: sensitive parameters never travel through the browser.
request_uri — what is it?
A reference to a server-side stash of the authorize parameters. PAR returns a request_uri whose URN-form value points to a one-time row the OP keeps for ~60 seconds. The browser replays only the URN and the client_id to /auth; the OP joins them back to the stashed parameters. The request_uri mechanism predates PAR (RFC 9101 also uses it for inline request JWTs hosted at an HTTPS URL) — PAR specialises it to "the OP itself hosts the URI" and locks the lifetime to a single redemption.
JAR — JWT-Secured Authorization Request (RFC 9101)
PAR ensures the parameters reach the OP confidentially. JAR ensures the OP can prove they came from the legitimate RP: the parameters are wrapped in a JWT signed with the RP's private key. The OP verifies the signature against the RP's registered public key.
PAR + JAR together give server-to-server delivery and cryptographic origin authentication of the authorize request.
JARM — JWT-Secured Authorization Response Mode
JARM carries the authorization response (code, state, iss, and errors) in a JWT signed by the OP. The RP verifies the signature and claims before processing the response. FAPI 2.0 Message Signing defines this as the signed-authorization-response method; the library's profile.FAPI2MessageSigning requires it.
DPoP / mTLS — sender constraint
Both bind the issued access token to a key the legitimate client holds, so a leaked token alone is useless. See Sender constraint for the mechanics.
FAPI JWT algorithms — why not RS256?
The FAPI 2.0 Security Profile requires PS256, ES256, or EdDSA (Ed25519) for JWT processing and forbids none; RS256 is therefore outside the profile's set. The library issues JWTs with one algorithm per OP, ES256 by default or RS256 through op.WithSigningAlgorithm(op.AlgRS256), and accepts PS256, ES256, and EdDSA for client-signed objects. Because RS256 is outside the profile's set, op.New refuses it together with profile.FAPI2Baseline, profile.FAPI2MessageSigning and profile.FAPICIBA; under those profiles the OP signs with ES256. Limiting issuance to ES256 is not a requirement of the Final Security Profile.
Algorithm allow-list — why a list and not "negotiate"?
The FAPI 2.0 Security Profile fixes the JWT algorithm set in advance: PS256, ES256, or EdDSA, and no none. Discovery can advertise the algorithms a deployment accepts, but the server must enforce its allow-list when it verifies a JWT. The library applies its own closed allow-lists at each JOSE surface; its single issuing algorithm per OP (ES256 or RS256) is an implementation choice.
private_key_jwt — FAPI client authentication
Confidential clients have to prove their identity to the token endpoint. The FAPI 2.0 Security Profile permits mTLS client authentication or private_key_jwt; mTLS has the tls_client_auth and self_signed_tls_client_auth variants defined by RFC 8705:
| Method | Source | What the client presents | OP-side verification |
|---|---|---|---|
private_key_jwt | OIDC Core §9 | A short JWT signed by a registered private key, posted as client_assertion on /token | Verify the signature against the client's registered JWKS |
tls_client_auth | RFC 8705 §2.1.1 | An X.509 certificate presented during the TLS handshake | Validate the chain against a configured trust anchor and match against the registered subject DN / SAN |
self_signed_tls_client_auth | RFC 8705 §2.2 | A self-signed X.509 certificate | Match the certificate's public key against the client's registered JWKS (no CA-chain walk) |
private_key_jwt: The JWT carriesiss,sub(itsclient_id),aud(the authorization server's issuer URL as a string under the Final FAPI profile),expandjti.
Both classes avoid the client_secret_basic failure mode (a long-lived shared secret leaks once and remains compromised until it is replaced).
Implementation note: under its FAPI profiles this library advertises and dispatches private_key_jwt, and, when feature.MTLS is enabled, tls_client_auth and self_signed_tls_client_auth too. The seed types are op.PrivateKeyJWTClient, op.TLSClientAuthClient and op.SelfSignedTLSClientAuthClient; FAPI 2.0 Baseline lists them.
The Final Security Profile requires the aud claim to be the issuer identifier as a string. Under profile.FAPI2Baseline and profile.FAPI2MessageSigning the verifier accepts exactly that and rejects an array or an endpoint URL with invalid_client. Other profiles keep accepting the issuer or the token endpoint URL, as RFC 7523 allows.
FAPI 2.0 × RFC requirement matrix
FAPI is a profile — it does not reinvent OAuth or OIDC. It tightens how the existing specifications are used. The table separates the Final Security Profile from the independently testable Message Signing methods and from library-specific settings.
| Spec / RFC | FAPI 2.0 Security Profile | FAPI 2.0 Message Signing | FAPI-CIBA |
|---|---|---|---|
| OAuth 2.0 (RFC 6749) | required | inherited | profile-specific |
| OIDC Core 1.0 | conditional: needed when an authenticated user identifier is returned | inherited | profile-specific |
PKCE — S256 only (RFC 7636) | required for authorization code | inherited | n/a (CIBA has no front-channel) |
| PAR (RFC 9126) | required | inherited | n/a (CIBA posts to /bc-authorize) |
| JAR (RFC 9101) — signed request object | optional | optional signed-authorization-request method | profile-specific |
| JARM — signed authorization response | optional | optional signed-authorization-response method | n/a (CIBA has no front-channel) |
| Signed introspection response (RFC 9701) | optional | optional signed-introspection-response method | profile-specific |
| DPoP (RFC 9449) OR mTLS (RFC 8705) | one of the two required | inherited | profile-specific |
| Resource Indicators (RFC 8707) | optional | inherited | profile-specific |
iss parameter on authorization response (RFC 9207) | required | inherited | n/a (CIBA has no front-channel) |
| OAuth 2.0 Security BCP (RFC 9700) | followed by profile | inherited | profile-specific |
| Token-endpoint client auth | mTLS or private_key_jwt | inherited | profile-specific |
client_secret_basic / _post / _jwt | not permitted | inherited | profile-specific |
| JWT signing alg | PS256, ES256, or EdDSA; none is forbidden | inherited | profile-specific |
redirect_uri in PAR | required | inherited | n/a (no redirect) |
| Refresh-token rotation | prohibited except extraordinary circumstances | inherited | profile-specific |
| Sender-constrained access tokens | required | inherited | profile-specific |
op.WithProfile argument | profile.FAPI2Baseline | profile.FAPI2MessageSigning | profile.FAPICIBA |
A few rows deserve a short prose note.
Why DPoP or mTLS. Both bind an access token to a key the legitimate client holds, so a leaked token alone is useless. The choice is operational: select the method that fits the existing PKI or proxy topology. See Sender constraint for the mechanics. The Final Security Profile requires one method; when neither is configured explicitly, this library selects DPoP as its default.
Why JAR and JARM are separate methods. Message Signing defines signed authorization requests, signed authorization responses, and signed introspection responses as distinct methods. An implementation can select the methods its deployment needs. profile.FAPI2MessageSigning is a library setting that auto-enables PAR, JAR, and JARM. If the separately enabled feature.Introspect mounts /introspect, the profile forces that endpoint's successful responses into the signed RFC 9701 format. When DPoP is active, the library profile also requires a configured DPoP nonce source; the Final Message Signing specification does not.
What differs in this library. profile.FAPI2Baseline auto-enables PAR and JAR, uses ES256 for OP-issued JWTs, and enforces exact redirect-URI matching. These are implementation choices or stricter settings; they must not be presented as additional Final Security Profile requirements. The two FAPI 2.0 profiles follow the Final text on two further points: client assertions must carry the issuer identifier as a string aud, and refresh tokens are not rotated. A passing op.New is still not a conformance claim, so review the running configuration, including the TLS setup, before making one.
See also
- RFC compliance matrix — per-RFC implementation status across the whole library, not only the FAPI subset.
- Use case — FAPI 2.0 Security Profile — full wiring with a
private_key_jwtclient and DPoP. - Sender constraint (DPoP / mTLS) — what the binding actually does, in detail.
- CIBA — the back-channel flow FAPI-CIBA constrains.
How go-oidc-provider makes it one switch
import (
"github.com/libraz/go-oidc-provider/op"
"github.com/libraz/go-oidc-provider/op/profile"
)
op.New(
/* required options */
op.WithProfile(profile.FAPI2Baseline),
)The profile call:
Auto-enables
feature.PARandfeature.JARas library settings. JAR is optional in the Final Security Profile.Intersects
token_endpoint_auth_methods_supportedwith the library's FAPI allow-list:private_key_jwt, plustls_client_authandself_signed_tls_client_authwhenfeature.MTLSis enabled.Requires at least one sender-constraint feature: explicit
feature.MTLSis preserved, otherwisefeature.DPoPis enabled by default.Keeps OP-issued ID Tokens on
ES256by refusingop.WithSigningAlgorithm(op.AlgRS256)atop.New, sinceRS256is outside the Final Security Profile's set (PS256,ES256, orEdDSA). The library issues no other algorithm, so the profile always signs withES256.Forces exact-match
redirect_uricomparison as a library policy.Does not rotate refresh tokens, as the Final Security Profile requires: a refresh response carries a new access token and no
refresh_token. The profile is therefore incompatible withop.WithRefreshGracePeriod, whichop.Newrejects.
Options that conflict with the profile cause op.New to refuse to start; the constructor catches those detected conflicts before the OP is created. The main constraints, by what they act on:
| Setting | Under FAPI 2.0 and FAPI-CIBA profiles |
|---|---|
| Token-endpoint client authentication | private_key_jwt, plus tls_client_auth and self_signed_tls_client_auth when feature.MTLS is enabled; dynamic registration refuses any other token_endpoint_auth_method with invalid_client_metadata |
| Algorithms on client-signed input (assertions, request objects) | PS256, ES256, EdDSA; RS256 is rejected at runtime, in discovery and at registration |
Client assertion aud | the issuer identifier as a single string under the two FAPI 2.0 profiles; profile.FAPICIBA keeps accepting the issuer and the endpoint URLs |
| Refresh tokens | not rotated under the two FAPI 2.0 profiles, and op.WithRefreshGracePeriod is rejected; profile.FAPICIBA keeps rotating |
DPoP htu | compared with the endpoint URL the OP advertises, not with a URL rebuilt from the request |
op.WithAccessTokenTTL | at most 10 minutes |
op.WithPARTTL | strictly below 600 seconds |
Request object at /par and /bc-authorize, and at /auth under a FAPI profile | replaces the unsigned query or form parameters instead of being merged with them, so every authorization parameter must be inside the signed object |
| Access-token revocation strategy | RevocationStrategyNone is refused |
op.WithFirstPartyClients, op.WithOpenIDScopeOptional | refused under the FAPI 2.0 profiles |
| DPoP nonce source | op.WithDPoPNonceSource is required with DPoP under FAPI2MessageSigning and FAPICIBA. This is the library's preset, not a MUST of the Final specifications |
profile.FAPICIBA | grant.CIBA must be enabled (it is not auto-enabled); CIBA default and maximum expiry at most 10 minutes |
| Responses | every response carries an x-fapi-interaction-id: the request's value, or a generated one |
The two behaviours behind the client-assertion aud and refresh-token rows are exposed as profile.RequiresIssuerAssertionAudience and profile.ForbidsRefreshTokenRotation, both true for the two FAPI 2.0 profiles and false for profile.FAPICIBA. The startup.profile audit record reports the resolved result in its refresh_rotation field, which is false when the profile keeps one refresh token for its lifetime.
To select the library's Message Signing profile, use profile.FAPI2MessageSigning instead of profile.FAPI2Baseline; when DPoP is active, also configure WithDPoPNonceSource. FAPI 2.0 Message Signing lists what changes for the embedder. The conformance suite covers both settings; see OFCS status.
Read next
- Sender constraint (DPoP / mTLS) — what the binding actually does, in detail.
- Use case: FAPI 2.0 Security Profile — full wiring with a
private_key_jwtclient and DPoP. - Authorization Code + PKCE — the flow FAPI 2.0 builds on.