Skip to content

Use case — FAPI 2.0 Baseline ​

What is FAPI 2.0? ​

FAPI ("Financial-grade API") is a family of OAuth 2.0 security profiles maintained by the OpenID Foundation; OIDC applies when the client needs an authenticated user's identifier. The current FAPI 2.0 Security Profile requires PAR, PKCE with S256, sender-constrained tokens (DPoP or mTLS), confidential clients, and JWT algorithms from PS256, ES256, or EdDSA. It does not require JAR as part of the Security Profile or restrict ID Token signing to ES256. The authorize endpoint is /auth under the configured mount prefix, so the default /oidc mount serves it at /oidc/auth. This library's profile.FAPI2Baseline adds JAR, uses private_key_jwt for client authentication, and currently signs ID Tokens with ES256 only.

The bar exists because banking, healthcare, and government deployments need a profile that can be audited against a checklist instead of "did you remember to set every flag?". FAPI 2.0 supersedes FAPI 1.0 (which is still in use). The final FAPI 2.0 document is titled Security Profile; this library calls its preset profile.FAPI2Baseline. The library's separate Message Signing preset adds JARM, a DPoP nonce-source constraint, and response-signing behavior; those are library preset behaviors, not requirements of the final Security Profile.

This library exposes Baseline as a single profile switch (op.WithProfile(profile.FAPI2Baseline)) that enables its required checks and refuses to start in any combination that would silently violate the library profile.

A primer with each acronym (PAR, JAR, JARM, DPoP, mTLS, ES256) walked through is at FAPI 2.0 primer. This page covers the wiring.

Specs referenced on this page

Sources: examples/03-fapi2/main.go covers the profile flow. examples/50-fapi-tls-jwks shows op.FAPITLSConfig() for the TLS 1.2 FAPI 1.0 RW cipher allow-list and op.LoadPublicJWKS, which strips private JWK material before client registration. TLS 1.3 deployments need their own tls.Config, because Go does not expose a TLS 1.3 cipher-suite allow-list.

What the library's FAPI2Baseline enforces ​

RequirementRFCLibrary behaviour
Pushed Authorization RequestsRFC 9126feature.PAR auto-enabled by the profile. request_uri returned from /par is the only authorize entry.
Proof Key for Code ExchangeRFC 7636code_challenge_method=S256 required; plain rejected.
Sender-constrained tokens (DPoP or mTLS)RFC 9449 / RFC 8705profile.RequiredAnyOf returns {DPoP, MTLS} for this profile, so either one satisfies the requirement; if neither is configured, the constructor auto-selects DPoP as the no-infrastructure default.
JWT algorithmsFAPI 2.0 §5.4.1The profile permits PS256, ES256, and EdDSA. This library's id_token_signing_alg_values_supported remains ["ES256"]; RS256 / none / HS* are never advertised.
redirect_uri exact matchLibrary policyThe library requires a pre-registered URI and compares it byte-for-byte, without wildcards. The final FAPI 2.0 Security Profile instead requires redirect_uri in the authenticated PAR request; it does not mandate pre-registration.
Client authenticationFAPI 2.0 §5.3.2.1FAPI allows mTLS or private_key_jwt. This library dispatches only private_key_jwt for token endpoint client authentication; mTLS can satisfy sender constraint, but mTLS client-auth dispatch is not wired.

Architecture ​

The library's FAPI2Baseline path, end to end
With the library's FAPI2Baseline profile, the RP pushes its authorization request to the pushed-authorization endpoint and receives a request_uri; the browser only ever carries that reference. The token request is authenticated with private_key_jwt and carries a DPoP proof, and the returned access token is DPoP-bound while the library's ID Token is signed with ES256.RP / clientprivate_key_jwt + DPoPOPgo-oidc-provider1POST /parclient_assertion=<private_key_jwt> · code_challenge=S2562201 · request_uri=urn:…:<id> · expires_in3GET /authorize?request_uri=urn:…&client_id4validate against the profilelibrary ID Token ES256 · redirect_uri exact match5the interaction endpoints obtain an authorization decision6302 redirect_uri?code=…&state=…7POST /token · DPoP: <proof>code · code_verifier · client_assertion8200access_token (DPoP-bound) · id_token (ES256) · refresh_token
Steps 1 and 2 are what the library's profile adds up front. After them the browser carries a reference rather than the request itself, so nothing in the URL bar is worth tampering with.

Code (excerpts from examples/03-fapi2) ​

go
import (
  "github.com/libraz/go-oidc-provider/op"
  "github.com/libraz/go-oidc-provider/op/feature"
  "github.com/libraz/go-oidc-provider/op/grant"
  "github.com/libraz/go-oidc-provider/op/profile"
  "github.com/libraz/go-oidc-provider/op/storeadapter/inmem"
)

const (
  demoIssuer      = "https://op.example.com"
  demoClientID    = "fapi2-example-client"
  demoRedirectURI = "https://rp.example.com/callback"
)

st := inmem.New()
loginFlow := op.LoginFlow{Primary: op.PrimaryPassword{Store: st.UserPasswords()}}

provider, err := op.New(
  op.WithIssuer(demoIssuer),
  op.WithStore(st),
  op.WithKeyset(opKeys.Keyset()),
  op.WithCookieKeys(opKeys.CookieKey),
  op.WithLoginFlow(loginFlow),
  op.WithGrants(grant.AuthorizationCode, grant.RefreshToken),
  op.WithProfile(profile.FAPI2Baseline), // <--- the profile switch
  op.WithFeature(feature.DPoP),
  op.WithStaticClients(op.PrivateKeyJWTClient{
    ID:            demoClientID,
    JWKS:          clientJWKs, // public JWK Set as JSON bytes
    RedirectURIs:  []string{demoRedirectURI},
    Scopes:        []string{"openid", "profile", "email"},
    GrantTypes:    []string{"authorization_code", "refresh_token"},
    ResponseTypes: []string{"code"},
  }),
)

PrivateKeyJWTClient is the typed seed for FAPI clients — it forces token_endpoint_auth_method=private_key_jwt automatically, so the embedder never has to spell that field out. The companion typed seeds are op.PublicClient and op.ConfidentialClient; all three implement op.ClientSeed and feed WithStaticClients(seeds ...ClientSeed).

The library's WithProfile call:

  1. Enables feature.PAR and feature.JAR automatically.
  2. Intersects token_endpoint_auth_methods_supported with the FAPI 2.0 §5.3.2.1 allow-list; configure clients with private_key_jwt for the token endpoint.
  3. Keeps id_token_signing_alg_values_supported = ["ES256"] (the OP only ever advertises and signs ES256 ID Tokens). This is the library's current keyset policy; the FAPI 2.0 Security Profile also permits PS256 and EdDSA.
  4. Forces redirect_uri exact match (no wildcards anywhere).
  5. Satisfies the DPoP-or-mTLS sender-constraint requirement by preserving an explicit feature.MTLS opt-in when present, or by adding feature.DPoP when neither binding was selected.

For JAR, profile.MaxRequestObjectAge(profile.FAPI2Baseline) returns 60 * time.Minute. op.New passes that value to the request-object verifier, which applies it to the signed request object's iat age while still checking its nbf and exp claims.

mTLS instead of DPoP

The profile's default sender binding is DPoP because it needs no TLS client-certificate plumbing. If your deployment standardizes on mTLS, enable feature.MTLS explicitly and configure op.WithMTLSProxy(...) for a TLS-terminating proxy; that explicit choice suppresses the DPoP default.

This is mTLS sender constraint, not token endpoint mTLS client authentication. Keep the client registered with private_key_jwt and use the forwarded certificate only to bind issued access tokens.

Verifying the surface ​

sh
curl -s http://localhost:8080/.well-known/openid-configuration | jq '{
  pushed_authorization_request_endpoint,
  request_parameter_supported,
  dpop_signing_alg_values_supported,
  token_endpoint_auth_methods_supported,
  id_token_signing_alg_values_supported
}'

Expected:

json
{
  "pushed_authorization_request_endpoint": "http://localhost:8080/oidc/par",
  "request_parameter_supported": true,
  "dpop_signing_alg_values_supported": ["ES256", "EdDSA", "PS256"],
  "token_endpoint_auth_methods_supported": ["private_key_jwt"],
  "id_token_signing_alg_values_supported": ["ES256"]
}

The library publishes ["ES256"] for id_token_signing_alg_values_supported regardless of profile (every issued ID Token is signed ES256). The live FAPI 2.0 Security Profile allows PS256, ES256, and EdDSA for JWT algorithms (§5.4.1); the library's ES256-only ID Token policy is stricter. dpop_signing_alg_values_supported covers DPoP proof acceptance and is ["ES256", "EdDSA", "PS256"].

Conformance ​

The library's profile.FAPI2Baseline is a policy preset, not a claim that every deployment conforms to every requirement in the current final Security Profile. The core refresh-token flow rotates refresh tokens, while FAPI 2.0 Security Profile §5.3.2.1(9) prohibits routine rotation except under extraordinary circumstances. The client-assertion verifier also accepts a token-endpoint URL or array aud, whereas §5.3.2.1(8) requires the issuer identifier as a string. Review these differences before making a conformance claim.

The OFCS fapi2-security-profile-id2-test-plan exercises this exact wiring: 48 PASSED / 9 REVIEW (manual reviewer) / 1 SKIPPED (RSA-key negative test that needs an additional client key) / 0 FAILED in the latest baseline.

For the full OFCS picture and the REVIEW / SKIPPED breakdown, see OFCS conformance status.