Skip to content

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.

go
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

What the profile does and what you supply ​

Message Signing includes everything in the Baseline table and adds the rows below.

The profile turns onYou supply
JAR as mandatory: every request to /par and /auth carries a signed request objectA key set (JWKS or JWKSURI) on every client, and clients that sign their requests with PS256, ES256 or EdDSA
JARM: /auth responses are signed JWTsClients that can verify a JARM response against the OP's JWKS
Signed introspection: /introspect answers with an RFC 9701 JWTfeature.Introspect when resource servers call it, and resource servers that verify the signed response (Protect an API)
A DPoP nonce challenge whenever DPoP is activeA 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 (JWKS or JWKSURI) that the OP can verify request objects against. A private_key_jwt client already has one.
  • The client signs with PS256, ES256 or EdDSA; RS256 is 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 iat of the object may be up to 60 minutes old (profile.MaxRequestObjectAge), with nbf and exp still 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:

go
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.