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 — for example, FAPI rejects RS256 signatures in favour of ES256/PS256, requires PKCE on every authorization, mandates sender-constrained tokens (DPoP or mTLS), and forces RPs to send their authorize requests through PAR + JAR instead of as plain query strings.
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
Source:
examples/03-fapi2/main.go
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 flags RequiredAnyOf=[DPoP, MTLS]; the constructor refuses to start unless one is enabled. |
| ES256 (or PS256) signing | RFC 7518 | Algorithm allow-list excludes RS256 from FAPI surface; none/HS* never present. |
redirect_uri exact match | FAPI 2.0 §5.3 | No wildcards. Byte-identical comparison. |
private_key_jwt or mTLS client auth | FAPI 2.0 §3.1.3 | Token endpoint auth-method list intersected with FAPI allow-list. |
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/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"
)
provider, err := op.New(
op.WithIssuer(demoIssuer),
op.WithStore(inmem.New()),
op.WithKeyset(opKeys.Keyset()),
op.WithCookieKey(opKeys.CookieKey),
op.WithProfile(profile.FAPI2Baseline), // <--- the profile switch
op.WithFeature(feature.DPoP), // pick the sender-constraint binding
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 (the embedder still picks the sender-constraint binding —feature.DPoPorfeature.MTLS— explicitly viaWithFeature). - Intersects
token_endpoint_auth_methods_supportedwith the FAPI 2.0 §3.1.3 allow-list (private_key_jwt,tls_client_auth,self_signed_tls_client_auth). - Locks the ID Token signing alg to
ES256/PS256and rejectsRS256for new tokens. - Forces
redirect_uriexact match (no wildcards anywhere).
mTLS instead of DPoP
The same profile leaves RequiredAnyOf=[DPoP, MTLS] — enable feature.MTLS instead of (or alongside) DPoP and configure op.WithMTLSProxy(...) for a TLS-terminating proxy. See examples/50-fapi-tls-jwks for FAPI-grade TLS helpers.
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", "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.