Skip to content

JAR & JARM

Overview

JAR (JWT Secured Authorization Request, RFC 9101) and JARM (JWT Secured Authorization Response Mode) provide end-to-end integrity protection for the authorization flow:

  • JAR — Authorization parameters are sent as a signed JWT (Request Object), preventing parameter tampering
  • JARM — The authorization response is returned as a signed JWT, preventing response tampering

Together, they ensure that neither the authorization request nor the response can be modified by an attacker.

JAR (JWT Secured Authorization Request)

How JAR Works

Instead of passing authorization parameters as query string values, the client creates a signed JWT containing all parameters and sends it in the request parameter:

Standard: /authorize?client_id=app&scope=openid&redirect_uri=...
With JAR: /authorize?client_id=app&request=eyJhbGciOiJSUzI1NiJ9...

The JWT is signed with the client’s private key, ensuring the authorization server can verify that the parameters have not been modified.

JARBuilder

Create Request Objects using JARBuilder:

import { JARBuilder } from '@authrim/core';
const jarBuilder = new JARBuilder({
signJwt: async (header, claims) => {
// Sign the JWT with your private key
return signedJwtString;
},
keyId: 'my-key-id',
algorithm: 'RS256',
lifetime: 300, // seconds
});

JARBuilderConfig

ParameterTypeDefaultDescription
signJwt(header, claims) => Promise<string>RequiredJWT signing function
keyIdstringKey ID (kid) for the JWT header
algorithmstring'RS256'Signing algorithm
lifetimenumber300Request Object lifetime in seconds

Building a Request Object

const requestObject = await jarBuilder.buildRequestObject({
clientId: 'my-app',
issuer: 'https://auth.example.com',
redirectUri: 'https://myapp.com/callback',
scope: 'openid profile email',
state: generatedState,
nonce: generatedNonce,
codeChallenge: generatedChallenge,
codeChallengeMethod: 'S256',
});
// requestObject is a signed JWT string

JARRequestOptions

ParameterTypeDescription
clientIdstringOAuth 2.0 client ID
issuerstringAuthorization server issuer URL (used as aud)
redirectUristringRedirect URI
scopestringRequested scopes
statestringCSRF protection state
noncestringReplay protection nonce
codeChallengestringPKCE code challenge
codeChallengeMethodstringChallenge method (always 'S256')
responseTypestringResponse type (default: 'code')
promptstringPrompt behavior
loginHintstringLogin hint
acrValuesstringAuthentication context
extraClaimsRecord<string, unknown>Additional JWT claims

Request Object JWT Claims

The generated JWT includes:

ClaimDescription
issClient ID
audAuthorization server issuer URL
iatIssued at timestamp
expExpiration timestamp
jtiUnique identifier
nbfNot before timestamp
response_typeOAuth response type
client_idClient identifier
redirect_uriRedirect URI
scopeRequested scopes
stateCSRF protection value
nonceReplay protection value
code_challengePKCE code challenge
code_challenge_methodPKCE method

Using JAR with buildAuthorizationUrl

The simplest way to use JAR is through buildAuthorizationUrl():

const { url } = await client.buildAuthorizationUrl({
redirectUri: 'https://myapp.com/callback',
useJar: true,
});

Checking If JAR Is Required

import { isJarRequired } from '@authrim/core';
const discovery = await client.discover();
if (isJarRequired(discovery)) {
// Server requires signed request objects
const { url } = await client.buildAuthorizationUrl({
redirectUri: 'https://myapp.com/callback',
useJar: true,
});
}

The isJarRequired() function checks the require_signed_request_object field in the discovery document.


JARM (JWT Secured Authorization Response Mode)

How JARM Works

Instead of returning authorization parameters as query string values, the authorization server returns a signed JWT in the response parameter:

Standard: /callback?code=abc&state=xyz
With JARM: /callback?response=eyJhbGciOiJSUzI1NiJ9...

The JWT is signed by the authorization server, ensuring the client can verify that the response has not been tampered with.

JARMValidator

Validate JARM responses using JARMValidator:

import { JARMValidator } from '@authrim/core';
const jarmValidator = new JARMValidator({
verifyJwt: async (jwt, issuer) => {
// Verify the JWT signature using the server's public key
return decodedClaims;
},
});

Validating a JARM Response

// Extract the response parameter from the callback URL
const responseJwt = JARMValidator.extractResponseFromCallback(callbackUrl);
if (responseJwt) {
const result = await jarmValidator.validateResponse(discovery, responseJwt, {
expectedState: storedState,
clientId: 'my-app',
clockSkewSeconds: 60,
});
console.log(result.code); // Authorization code
console.log(result.state); // Validated state
}

JARMValidationOptions

ParameterTypeDefaultDescription
expectedStatestringRequiredExpected state value for CSRF validation
clientIdstringRequiredClient ID (must match aud claim)
clockSkewSecondsnumber60Allowed clock skew in seconds

JARMValidationResult

PropertyTypeDescription
codestringThe authorization code
statestringThe validated state parameter

JARM Response Claims

The JWT returned by the server contains:

ClaimDescription
issAuthorization server issuer
audClient ID
expExpiration timestamp
codeAuthorization code (on success)
stateState parameter
errorError code (on failure)
error_descriptionError description (on failure)

Complete Example: JAR + JARM Flow

import { JARBuilder, JARMValidator, isJarRequired } from '@authrim/core';
// Setup
const jarBuilder = new JARBuilder({
signJwt: mySignFunction,
algorithm: 'RS256',
});
const jarmValidator = new JARMValidator({
verifyJwt: myVerifyFunction,
});
// Step 1: Build authorization URL with JAR
const requestObject = await jarBuilder.buildRequestObject({
clientId: 'my-app',
issuer: 'https://auth.example.com',
redirectUri: 'https://myapp.com/callback',
scope: 'openid profile',
state: generatedState,
nonce: generatedNonce,
codeChallenge: generatedChallenge,
codeChallengeMethod: 'S256',
});
const authUrl = `${discovery.authorization_endpoint}?client_id=my-app&request=${requestObject}`;
// Step 2: After redirect, validate JARM response
const responseJwt = JARMValidator.extractResponseFromCallback(callbackUrl);
if (responseJwt) {
const { code, state } = await jarmValidator.validateResponse(
discovery,
responseJwt,
{ expectedState: storedState, clientId: 'my-app' },
);
// Step 3: Exchange code for tokens
const tokens = await exchangeCode(code);
}

Error Handling

Error CodeDescriptionRecovery
jar_signing_errorFailed to sign the Request ObjectCheck signing key and configuration
jar_requiredServer requires JAR but it wasn’t usedSet useJar: true
jarm_validation_errorJARM response validation failedCheck server configuration
jarm_signature_invalidJARM JWT signature verification failedVerify server keys

References

Next Steps