Use case — FAPI 2.0 Baseline
What is FAPI 2.0?
FAPI ("Financial-grade API") is a profile of OAuth 2.0 + OIDC maintained by the OpenID Foundation. It picks a strict subset of the underlying specs and forbids the optional flexibility that attackers historically abused. FAPI rejects RS256 signatures in favour of ES256 / PS256, requires PKCE on every authorization, mandates sender-constrained tokens (DPoP or mTLS), and makes RPs send their authorize requests through PAR + JAR rather than as plain query strings. The authorize endpoint is /auth under the configured mount prefix, so the default /oidc mount serves it at /oidc/auth. This library signs id_tokens with ES256 only, so the anti-RS256 clause is satisfied by construction.
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). FAPI 2.0 Baseline is the entry-level profile; FAPI 2.0 Message Signing adds JARM + DPoP nonce + RS-side proof signing.
This library exposes Baseline as a single profile switch (op.WithProfile(profile.FAPI2Baseline)) that flips every required flag and refuses to start in any combination that would silently violate the 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 FAPI 2.0 Baseline mandates
| 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. |
| ES256 signing | RFC 7518 | id_token_signing_alg_values_supported is ["ES256"] unconditionally; RS256 / none / HS* never advertised. |
redirect_uri exact match | FAPI 2.0 §5.3 | No wildcards. Byte-identical comparison. |
private_key_jwt client auth | FAPI 2.0 §3.1.3 | Use private_key_jwt for the token endpoint auth path. 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 WithProfile call:
- Enables
feature.PARandfeature.JARautomatically. - Intersects
token_endpoint_auth_methods_supportedwith the FAPI 2.0 §3.1.3 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; FAPI 2.0's anti-RS256clause is satisfied by construction). - 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 FAPI 2.0 §6.2.1 mandate against RS256 is satisfied because RS256 never appears on the OP's supported set in the first place. dpop_signing_alg_values_supported covers DPoP proof acceptance and is ["ES256", "EdDSA", "PS256"].
Conformance
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.