Use case — FAPI 2.0 Message Signing
profile.FAPI2MessageSigning is FAPI 2.0 Baseline with three signed messages added: the authorization request, the authorization response and the introspection response. Everything the Baseline page lists still applies: PAR, PKCE, DPoP or mTLS, private_key_jwt or certificate client authentication, an issuer-only aud on client assertions, and refresh tokens that are not rotated. This page covers only what changes for you.
op.New(
/* the same options as for FAPI2Baseline */
op.WithProfile(profile.FAPI2MessageSigning),
op.WithDPoPNonceSource(nonces), // required while DPoP is active, see below
)Specs referenced on this page
- FAPI 2.0 Message Signing — Final
- RFC 9101 — JWT-Secured Authorization Request (JAR)
- JARM — JWT-Secured Authorization Response Mode
- RFC 9701 — JWT Response for OAuth Token Introspection
- RFC 9449 — DPoP
What the profile does and what you supply
Message Signing includes everything in the Baseline table and adds the rows below.
| The profile turns on | You supply |
|---|---|
JAR as mandatory: every request to /par and /auth carries a signed request object | A key set (JWKS or JWKSURI) on every client, and clients that sign their requests with PS256, ES256 or EdDSA |
JARM: /auth responses are signed JWTs | Clients that can verify a JARM response against the OP's JWKS |
Signed introspection: /introspect answers with an RFC 9701 JWT | feature.Introspect when resource servers call it, and resource servers that verify the signed response (Protect an API) |
| A DPoP nonce challenge whenever DPoP is active | A nonce source: op.NewInMemoryDPoPNonceSource for a single replica, a shared source such as Store.DPoPNonceSource from op/storeadapter/redis for several |
Both Baseline's list and this one assume stores shared by every replica and, behind a proxy, op.WithTrustedProxies (plus op.WithMTLSProxy and op.WithMTLSRootCAs for mTLS). The runnable version is examples/54-fapi2-message-signing.
Checklist
Signed request objects are mandatory
Every request to /par and /auth must carry a signed JAR request object. Baseline accepts one and also accepts plain parameters; under Message Signing a request without a signed object is refused. feature.JAR is enabled by the profile, so the only work is on the client side and in client registration:
- Register each client with a key set (
JWKSorJWKSURI) that the OP can verify request objects against. Aprivate_key_jwtclient already has one. - The client signs with
PS256,ES256orEdDSA;RS256is rejected. - Every authorization parameter goes inside the signed object, because a request object replaces the unsigned query or form parameters instead of being merged with them.
- The
iatof the object may be up to 60 minutes old (profile.MaxRequestObjectAge), withnbfandexpstill checked.
Authorization responses are JARM
Every response from /auth is wrapped in a JARM JWT signed with the OP's active signing key. feature.JARM is enabled by the profile and nothing else is configured. The relying party verifies the JWT against the OP's JWKS and then reads code, state and iss from its claims, so a client that cannot process a JARM response cannot use this profile.
Introspection responses are signed
When /introspect is mounted, every successful response uses the RFC 9701 JWT format, signed with the OP's active key, whatever Accept header or client metadata the caller sent. The profile does not mount the endpoint. Enable feature.Introspect when resource servers call it, and make sure those callers accept a signed JWT response and verify it against the JWKS.
A DPoP nonce source is required while DPoP is active
op.New fails with WithProfile(profile.FAPI2MessageSigning) requires a DPoP nonce source because DPoP is active when DPoP is on and no nonce source is configured. DPoP is on if you enabled feature.DPoP, and also when you enabled neither DPoP nor feature.MTLS, because the profile then selects DPoP itself. An OP that chooses feature.MTLS for sender constraint needs no nonce source.
This requirement belongs to this library's preset. The Final FAPI 2.0 Security Profile and Message Signing specifications make DPoP nonces optional, so a conformance claim does not depend on it. With the source configured, /token, /par and /userinfo answer a proof without an accepted nonce with use_dpop_nonce and a fresh DPoP-Nonce header (DPoP nonce flow). profile.FAPICIBA carries the same requirement.
Several replicas share one nonce source
op.NewInMemoryDPoPNonceSource is process-local, so a nonce issued by one replica fails on another. Use the Redis source of op/storeadapter/redis:
nonces, err := redisStore.DPoPNonceSource(time.Minute)
if err != nil {
log.Fatal(err)
}
op.WithDPoPNonceSource(nonces)Replicas that share the store's key prefix share the nonce state. Multi-instance deployment covers the failure behaviour.
What stays the same
Client authentication choices, the aud rule, non-rotating refresh tokens, the 10-minute access-token cap, the WithPARTTL ceiling, exact-match redirect_uri and the TLS decisions (proxy handling, RSA certificate for op.FAPITLSConfig) are identical to Baseline. Keep using the Baseline page for them.
Read next
- FAPI 2.0 Baseline — the profile this page builds on.
- DPoP nonce flow — the nonce handshake and its wiring.
- FAPI 2.0 primer — what each signed message is for.
- JARM — how the signed authorization response is built and verified.