Skip to content

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

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
ProfilesRead-Only / Read-WriteSecurity Profile / Message Signing
Authorize request transportOptional request JWTPAR mandatory
Sender constraintmTLS or holder-of-keyDPoP or mTLS mandatory
response_typeAllowed code id_token (hybrid)code only
JWT algorithmsProfile-specific choicesPS256, 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.

Each FAPI layer fixes more of the base specs’ options
The FAPI 2.0 profiles. The base specifications leave many options open; the Security Profile fixes a high-security baseline; Message Signing adds separately testable signed authorization requests, signed authorization responses, and signed introspection responses.FAPI 2.0 Message SigningSecurity Profile + JAR · JARM · signed introspectionop.WithProfile(profile.FAPI2MessageSigning)audit trailnon-repudiationFAPI 2.0 Security ProfilePAR · PKCE S256 · confidential clients · DPoP or mTLSop.WithProfile(profile.FAPI2Baseline)safety floorOAuth 2.0 + OpenID Connectmany options, some risky in combinationtransport
The Security Profile fixes the high-security choices in the underlying specifications. Message Signing adds application-level signatures for selected OAuth messages; it does not wrap every API request and response in a JWS.

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:

MethodSourceWhat the client presentsOP-side verification
private_key_jwtOIDC Core §9A short JWT signed by a registered private key, posted as client_assertion on /tokenVerify the signature against the client's registered JWKS
tls_client_authRFC 8705 §2.1.1An X.509 certificate presented during the TLS handshakeValidate the chain against a configured trust anchor and match against the registered subject DN / SAN
self_signed_tls_client_authRFC 8705 §2.2A self-signed X.509 certificateMatch the certificate's public key against the client's registered JWKS (no CA-chain walk)
  • private_key_jwt: The JWT carries iss, sub (its client_id), aud (the authorization server's issuer URL as a string under the Final FAPI profile), exp and jti.

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 / RFCFAPI 2.0 Security ProfileFAPI 2.0 Message SigningFAPI-CIBA
OAuth 2.0 (RFC 6749)requiredinheritedprofile-specific
OIDC Core 1.0conditional: needed when an authenticated user identifier is returnedinheritedprofile-specific
PKCE — S256 only (RFC 7636)required for authorization codeinheritedn/a (CIBA has no front-channel)
PAR (RFC 9126)requiredinheritedn/a (CIBA posts to /bc-authorize)
JAR (RFC 9101) — signed request objectoptionaloptional signed-authorization-request methodprofile-specific
JARM — signed authorization responseoptionaloptional signed-authorization-response methodn/a (CIBA has no front-channel)
Signed introspection response (RFC 9701)optionaloptional signed-introspection-response methodprofile-specific
DPoP (RFC 9449) OR mTLS (RFC 8705)one of the two requiredinheritedprofile-specific
Resource Indicators (RFC 8707)optionalinheritedprofile-specific
iss parameter on authorization response (RFC 9207)requiredinheritedn/a (CIBA has no front-channel)
OAuth 2.0 Security BCP (RFC 9700)followed by profileinheritedprofile-specific
Token-endpoint client authmTLS or private_key_jwtinheritedprofile-specific
client_secret_basic / _post / _jwtnot permittedinheritedprofile-specific
JWT signing algPS256, ES256, or EdDSA; none is forbiddeninheritedprofile-specific
redirect_uri in PARrequiredinheritedn/a (no redirect)
Refresh-token rotationprohibited except extraordinary circumstancesinheritedprofile-specific
Sender-constrained access tokensrequiredinheritedprofile-specific
op.WithProfile argumentprofile.FAPI2Baselineprofile.FAPI2MessageSigningprofile.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 ​

How go-oidc-provider makes it one switch ​

go
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:

  1. Auto-enables feature.PAR and feature.JAR as library settings. JAR is optional in the Final Security Profile.

  2. Intersects token_endpoint_auth_methods_supported with the library's FAPI allow-list: private_key_jwt, plus tls_client_auth and self_signed_tls_client_auth when feature.MTLS is enabled.

  3. Requires at least one sender-constraint feature: explicit feature.MTLS is preserved, otherwise feature.DPoP is enabled by default.

  4. Keeps OP-issued ID Tokens on ES256 by refusing op.WithSigningAlgorithm(op.AlgRS256) at op.New, since RS256 is outside the Final Security Profile's set (PS256, ES256, or EdDSA). The library issues no other algorithm, so the profile always signs with ES256.

  5. Forces exact-match redirect_uri comparison as a library policy.

  6. 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 with op.WithRefreshGracePeriod, which op.New rejects.

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:

SettingUnder FAPI 2.0 and FAPI-CIBA profiles
Token-endpoint client authenticationprivate_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 audthe issuer identifier as a single string under the two FAPI 2.0 profiles; profile.FAPICIBA keeps accepting the issuer and the endpoint URLs
Refresh tokensnot rotated under the two FAPI 2.0 profiles, and op.WithRefreshGracePeriod is rejected; profile.FAPICIBA keeps rotating
DPoP htucompared with the endpoint URL the OP advertises, not with a URL rebuilt from the request
op.WithAccessTokenTTLat most 10 minutes
op.WithPARTTLstrictly below 600 seconds
Request object at /par and /bc-authorize, and at /auth under a FAPI profilereplaces 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 strategyRevocationStrategyNone is refused
op.WithFirstPartyClients, op.WithOpenIDScopeOptionalrefused under the FAPI 2.0 profiles
DPoP nonce sourceop.WithDPoPNonceSource is required with DPoP under FAPI2MessageSigning and FAPICIBA. This is the library's preset, not a MUST of the Final specifications
profile.FAPICIBAgrant.CIBA must be enabled (it is not auto-enabled); CIBA default and maximum expiry at most 10 minutes
Responsesevery 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.