A classic OAuth authorisation response is a handful of unsigned query parameters that travel through the user's browser, so the client has no cryptographic proof of who issued the code, who it was meant for, or whether anyone changed it on the way. JARM, the JWT Secured Authorization Response Mode, wraps that response in a signed (and optionally encrypted) JWT from the authorisation server. It is one of the two response protection options in FAPI 1.0 Advanced, which many open banking ecosystems built on, and it is the mechanism for signed responses in the FAPI 2.0 Message Signing profile. This guide explains how JARM works, how it differs from request signing, how to configure and validate it, and where it fits next to PKCE and the iss response parameter. It is for identity engineers building or integrating high-assurance OAuth clients and authorisation servers. For signing the request side, see the OAuth Pushed Authorization Requests and JAR Guide.
Key takeaways
- JARM protects the response, JAR protects the request. They solve different problems and high-assurance profiles often want both.
- Four response modes. query.jwt, fragment.jwt, form_post.jwt and the shortcut jwt, with the JWT carried in a single parameter named response.
- Three claims make it work. iss, aud and exp let the client detect mix-up, misdirected responses and old responses; the specification recommends a lifetime of no more than 10 minutes.
- Validation order matters. Check the issuer before fetching keys, reject the none algorithm, and do not touch the code or other response parameters until every check passes.
- It does not replace PKCE. JARM's own security considerations still call for PKCE, because a signed code can still leak.
What JARM is and the problem it solves
JARM is an OpenID Foundation specification produced by the FAPI working group, published as a final specification in November 2022. It defines new response modes that tell the authorisation server to return the authorisation response as a JWT instead of separate parameters.
In the standard authorisation code flow, the server redirects the browser to the client with code and state in the URL. Those values are not integrity protected. A client that talks to several authorisation servers cannot prove which one issued a code, which is the root of mix-up attacks, and an attacker who can inject or alter a response in the browser leaves no cryptographic trace. JARM changes that by putting every response parameter, including errors, inside a JWT signed by the authorisation server and addressed to the client.
| Mechanism | What it protects | Direction | Defined in |
|---|---|---|---|
| JAR (signed request object) | Integrity and origin of the authorisation request | Client to server | RFC 9101 |
| PAR | Moves the request to a back-channel POST, returns a request_uri | Client to server | RFC 9126 |
| JARM | Integrity, origin, audience and freshness of the authorisation response; optional confidentiality | Server to client | OpenID Foundation JARM |
| ID Token as detached signature | Hashes of code (c_hash) and state (s_hash) signed in an ID Token returned with the response | Server to client | FAPI 1.0 Part 2 with OpenID Connect |
| iss response parameter | Identifies the issuer, unsigned, to detect mix-up | Server to client | RFC 9207 |
| PKCE | Binds the code to the client instance that started the flow | Both, checked at token endpoint | RFC 7636 |
How JARM works
The client asks for a JWT response by setting response_mode in the authorisation request. The server builds a JWT containing the normal response parameters plus three claims, signs it, optionally encrypts it, and returns it in a parameter named response.
Response modes
| response_mode | How the JWT is delivered | Notes |
|---|---|---|
| query.jwt | In the redirect URI query string | Must not be used with token or id_token response types unless the JWT is encrypted |
| fragment.jwt | In the redirect URI fragment | Not sent to the server in the HTTP request; client-side code reads it |
| form_post.jwt | Auto-submitted HTTP POST to the redirect URI | Keeps the JWT out of URLs, browser history and referrer headers |
| jwt | The default encoding for the response type | query.jwt for code; fragment.jwt for token and OpenID Connect response types other than none |
JWT contents
- iss: the authorisation server's issuer identifier.
- aud: the client_id of the intended client.
- exp: expiry; the specification recommends a maximum lifetime of 10 minutes.
- Response parameters: code, state, error and error_description, and any extension parameters such as session_state, all inside the JWT rather than beside it.
The JWT is either signed, or signed then encrypted as a Nested JWT. The server picks the algorithms, normally from the client's registered metadata.
Because errors also travel inside the JWT, a client can trust an error response as much as a success response: an attacker cannot forge an access_denied or invalid_request to confuse the client's state. Encryption adds confidentiality on top of integrity, which matters when the front channel would otherwise expose personal data, but JARM Section 5.4 notes that if the encryption is ever broken the whole response is exposed, so encryption is a complement to PKCE, not a substitute.
Configuration: client and server metadata
| Metadata | Where | Meaning | Default or rule |
|---|---|---|---|
| authorization_signed_response_alg | Client registration | Signing algorithm for responses | RS256 if omitted; none is not allowed |
| authorization_encrypted_response_alg | Client registration | Key management algorithm for encryption | No encryption if omitted |
| authorization_encrypted_response_enc | Client registration | Content encryption algorithm | A128CBC-HS256 when an alg is set; requires authorization_encrypted_response_alg |
| response_modes_supported | Server metadata | Advertised response modes | Should include the four JWT modes |
| authorization_signing_alg_values_supported | Server metadata | Signing algorithms the server can use | Publish only those your profile allows |
| authorization_encryption_alg_values_supported and authorization_encryption_enc_values_supported | Server metadata | Encryption algorithms the server can use | Publish only if you encrypt responses |
Profiles override the defaults. FAPI 1.0 Part 2 Section 8.6 requires PS256 or ES256, says RSASSA-PKCS1-v1_5 algorithms such as RS256 should not be used, forbids none, and forbids RSA1_5 for JWE. A FAPI client should therefore register PS256 or ES256 explicitly rather than rely on the JARM default of RS256.
Validating a JARM response
JARM Section 2.4 sets out the client's checks. Treat them as a fixed sequence in a single, well-tested library function:
- Decrypt the JWT if the client registered for encryption, and reject an unencrypted response when encryption is expected.
- Check iss against the issuer the client sent this user to. Do this before fetching any keys; Section 5.1 warns that crafted JWTs can otherwise make the client fetch keys from attacker-chosen locations.
- Check aud equals the client's own client_id.
- Check exp has not passed, with only a small clock skew allowance.
- Verify the signature with the issuer's published keys, using only the expected algorithm. Reject none.
- Only then read code, state and error, check state against the browser session, and redeem the code with the PKCE verifier.
The specification leaves CSRF detection to the OAuth security best current practice, so state or PKCE still has to tie the response to the user's session. OWASP ASVS 5.0 V10.1.2 captures the same principle: accept values only from a flow started by the same user agent session and transaction. See the OAuth 2.0 Security Best Current Practice (RFC 9700) Guide.
Where profiles require JARM
- FAPI 1.0 Part 2 Advanced (Final, 12 March 2021): Section 5.2.2 requires the authorisation server to support either the
code id_tokenresponse type, where the ID Token acts as a detached signature with c_hash and s_hash, or thecoderesponse type withresponse_mode=jwt. With JARM and the openid scope, the ID Token comes from the token endpoint instead. Public clients are not supported. - FAPI 2.0 Message Signing: Section 5.4 uses JARM to sign authorisation responses for non-repudiation of front-channel responses. Where an implementation adopts response signing, the server shall support, require and issue signed responses, and the client shall set
response_mode=jwtand verify the responses. The profile notes that the RFC 9207 iss parameter should only appear inside the JWT, and Section 6.6 warns that signed codes and state values have limited non-repudiation value on their own. See the FAPI 2.0 Security Profile Implementation Guide. - Open banking ecosystems: many national schemes adopted FAPI 1.0 Advanced, so their clients meet JARM or the detached signature pattern. Check your ecosystem's own profile for which one it mandates. See the Open banking and open finance security guide.
Decision guidance
| Situation | Choice | Why |
|---|---|---|
| FAPI 1.0 Advanced client without OpenID Connect needs | code with response_mode=jwt | Plain OAuth works; no ID Token needed in the front channel |
| FAPI 1.0 Advanced client already processing ID Tokens | code id_token (detached signature) or JARM | Both are allowed; JARM keeps personal data out of the front channel by moving the ID Token to the token endpoint |
| FAPI 2.0 Security Profile only | PKCE, PAR and the iss parameter; JARM optional | FAPI 2.0 baseline does not need signed responses; add Message Signing only where non-repudiation is required |
| Client talks to many authorisation servers | JARM or at least RFC 9207 iss | Signed iss and aud give strong mix-up protection |
| Response carries sensitive data or uses query.jwt with tokens | Signed then encrypted JARM, prefer form_post.jwt | Encryption and POST delivery limit leakage in URLs and logs |
| General web or mobile app, single provider | PKCE and iss; JARM usually not needed | Lower complexity for limited added benefit |
Small teams should take JARM from their authorisation server and client library rather than build it. Large platforms serving many relying parties should publish JARM support in metadata so high-assurance clients can opt in without custom work.
Implementing JARM: a phased approach
- Confirm the requirement. Read your ecosystem profile to see whether JARM, the detached signature pattern or neither is required, and which algorithms are allowed.
- Prepare keys. Publish the server's signing keys in its JWKS with key IDs, and plan rotation with overlap so clients can fetch new keys before old ones are retired. For encryption, collect each client's encryption keys from its registered JWKS.
- Register clients. Set authorization_signed_response_alg to the profile's algorithm (for FAPI, PS256 or ES256) and, if needed, the encryption metadata.
- Build validation once. Implement the six-step check in one library function, with tests for wrong iss, wrong aud, expired exp, bad signature, none algorithm, unexpected algorithm and unencrypted responses when encryption is registered.
- Test against conformance tools. The OpenID Foundation runs conformance suites for FAPI profiles; use them in a test environment before production.
- Roll out and roll back. Enable response_mode=jwt per client. Rollback is removing the response_mode from the client's request, as long as the server still allows plain responses for that client during the transition.
Detection and measurement
Log each response validation failure with the reason (iss, aud, exp, signature, algorithm, decryption). A rise in iss or aud failures can indicate mix-up or injection attempts; signature failures after a key rotation usually mean a client has cached old keys.
| Metric | Target |
|---|---|
| High-assurance clients using response_mode=jwt or the profile's alternative | 100 percent |
| Clients registered with an algorithm outside the profile list | Zero |
| Validation failures by reason per week | Low and explained; spikes investigated |
| JARM responses with exp beyond 10 minutes | Zero |
| Days from server key rotation to zero signature failures | Within the planned overlap window |
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev. 5 | SC-8, SC-23 | Integrity of the response in transit and session authenticity |
| NIST SP 800-53 Rev. 5 | SC-12, SC-13 | Key management and approved algorithms for signing and encryption |
| NIST SP 800-53 Rev. 5 | SI-10 | Validating inputs (the response JWT) before use |
| NIST CSF 2.0 | PR.DS-02, PR.AA-03 | Data in transit protected; services authenticated |
| OWASP ASVS 5.0 | V10.1.2, V10.2.2 | Accept only values from the client's own flow; defend against mix-up |
| MITRE ATT&CK | T1557, T1528 | Interception or tampering of responses; theft of codes and tokens |
Common failure patterns
- Reading the code before validating. Some integrations parse the JWT payload, take the code and validate later or never, which removes all the protection.
- Accepting any algorithm. Libraries that trust the JWT header's alg value can be led to accept none or an unexpected algorithm. Pin the algorithm per client.
- Fetching keys before checking the issuer. Looking up keys for whatever issuer an unchecked JWT names, which Section 5.1 warns can be abused for denial of service. Resolve keys only for the issuer the client expects.
- Relying on the RS256 default. Registering no algorithm in a FAPI ecosystem leaves the JARM default of RS256, which FAPI 1.0 Part 2 says should not be used.
- Dropping PKCE. Treating a signed response as enough; JARM Section 5.4 notes a code can still leak, so PKCE stays.
- Tokens in query.jwt without encryption. Explicitly disallowed by the specification and a leakage risk through logs and referrers.
Practitioner checklist
- Confirm whether your profile needs JARM, the detached signature pattern or neither.
- Register an explicit signing algorithm per client; PS256 or ES256 for FAPI.
- Publish response_modes_supported and the JARM algorithm metadata on the server.
- Keep JARM lifetimes at 10 minutes or less.
- Validate in order: decrypt, iss, aud, exp, signature, then parameters.
- Reject none and any algorithm not registered for the client.
- Keep PKCE and state checks alongside JARM.
- Prefer form_post.jwt, and encrypt when responses carry sensitive data.
- Rotate signing keys with overlap and monitor signature failures.
- Test with the OpenID Foundation conformance suite before go-live.
Standards and references
- OpenID Foundation: JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)
- OpenID Foundation: Financial-grade API Security Profile 1.0 Part 2: Advanced
- OpenID Foundation: FAPI 2.0 Message Signing
- OWASP: Application Security Verification Standard 5.0
- NIST: SP 800-53 Rev. 5
- NIST: Cybersecurity Framework 2.0
- MITRE ATT&CK: Adversary-in-the-Middle (T1557)
Related NHI Mgmt Group resources: OAuth Pushed Authorization Requests and JAR Guide · FAPI 2.0 Security Profile Implementation Guide · OAuth 2.1, GNAP and FAPI Guide · Open banking and open finance security