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
| Parameter | Type | Default | Description |
|---|---|---|---|
signJwt | (header, claims) => Promise<string> | Required | JWT signing function |
keyId | string | — | Key ID (kid) for the JWT header |
algorithm | string | 'RS256' | Signing algorithm |
lifetime | number | 300 | Request 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 stringJARRequestOptions
| Parameter | Type | Description |
|---|---|---|
clientId | string | OAuth 2.0 client ID |
issuer | string | Authorization server issuer URL (used as aud) |
redirectUri | string | Redirect URI |
scope | string | Requested scopes |
state | string | CSRF protection state |
nonce | string | Replay protection nonce |
codeChallenge | string | PKCE code challenge |
codeChallengeMethod | string | Challenge method (always 'S256') |
responseType | string | Response type (default: 'code') |
prompt | string | Prompt behavior |
loginHint | string | Login hint |
acrValues | string | Authentication context |
extraClaims | Record<string, unknown> | Additional JWT claims |
Request Object JWT Claims
The generated JWT includes:
| Claim | Description |
|---|---|
iss | Client ID |
aud | Authorization server issuer URL |
iat | Issued at timestamp |
exp | Expiration timestamp |
jti | Unique identifier |
nbf | Not before timestamp |
response_type | OAuth response type |
client_id | Client identifier |
redirect_uri | Redirect URI |
scope | Requested scopes |
state | CSRF protection value |
nonce | Replay protection value |
code_challenge | PKCE code challenge |
code_challenge_method | PKCE 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=xyzWith 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 URLconst 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
| Parameter | Type | Default | Description |
|---|---|---|---|
expectedState | string | Required | Expected state value for CSRF validation |
clientId | string | Required | Client ID (must match aud claim) |
clockSkewSeconds | number | 60 | Allowed clock skew in seconds |
JARMValidationResult
| Property | Type | Description |
|---|---|---|
code | string | The authorization code |
state | string | The validated state parameter |
JARM Response Claims
The JWT returned by the server contains:
| Claim | Description |
|---|---|
iss | Authorization server issuer |
aud | Client ID |
exp | Expiration timestamp |
code | Authorization code (on success) |
state | State parameter |
error | Error code (on failure) |
error_description | Error description (on failure) |
Complete Example: JAR + JARM Flow
import { JARBuilder, JARMValidator, isJarRequired } from '@authrim/core';
// Setupconst jarBuilder = new JARBuilder({ signJwt: mySignFunction, algorithm: 'RS256',});
const jarmValidator = new JARMValidator({ verifyJwt: myVerifyFunction,});
// Step 1: Build authorization URL with JARconst 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 responseconst 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 Code | Description | Recovery |
|---|---|---|
jar_signing_error | Failed to sign the Request Object | Check signing key and configuration |
jar_required | Server requires JAR but it wasn’t used | Set useJar: true |
jarm_validation_error | JARM response validation failed | Check server configuration |
jarm_signature_invalid | JARM JWT signature verification failed | Verify server keys |
References
- RFC 9101 — The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)
- Financial-grade API Security Profile 2.0
Next Steps
- DPoP — Bind tokens to cryptographic keys
- PAR — Push authorization parameters via back-channel
- Authorization Code Flow — Standard login flow