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.gocovers the profile flow.examples/50-fapi-tls-jwksshowsop.FAPITLSConfig()for the TLS 1.2 FAPI 1.0 RW cipher allow-list andop.LoadPublicJWKS, which strips private JWK material before client registration. TLS 1.3 deployments need their owntls.Config, because Go does not expose a TLS 1.3 cipher-suite allow-list.
What the library's FAPI2Baseline enforces
| Requirement | RFC | Library behaviour |
|---|---|---|
| Pushed Authorization Requests | RFC 9126 | feature.PAR auto-enabled by the profile. request_uri returned from /par is the only authorize entry. |
| Proof Key for Code Exchange | RFC 7636 | code_challenge_method=S256 required; plain rejected. |
| Sender-constrained tokens (DPoP or mTLS) | RFC 9449 / RFC 8705 | profile.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 algorithms | FAPI 2.0 §5.4.1 | The 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 match | Library policy | The 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 authentication | FAPI 2.0 §5.3.2.1 | FAPI 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
Code (excerpts from examples/03-fapi2)
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:
- Enables
feature.PARandfeature.JARautomatically. - Intersects
token_endpoint_auth_methods_supportedwith the FAPI 2.0 §5.3.2.1 allow-list; configure clients withprivate_key_jwtfor the token endpoint. - Keeps
id_token_signing_alg_values_supported = ["ES256"](the OP only ever advertises and signsES256ID Tokens). This is the library's current keyset policy; the FAPI 2.0 Security Profile also permitsPS256andEdDSA. - Forces
redirect_uriexact match (no wildcards anywhere). - Satisfies the DPoP-or-mTLS sender-constraint requirement by preserving an explicit
feature.MTLSopt-in when present, or by addingfeature.DPoPwhen 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
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:
{
"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.