Skip to content

RFC 9101 — JWT-Secured Authorization Request (JAR) ​

Spec: datatracker.ietf.org/doc/html/rfc9101Status: Full

RFC 9101 (formerly OpenID Connect Core §6) lets a client pack the authorization request into a signed JWT — the Request Object — instead of sending parameters as plain query strings. This protects the request from tampering in transit and lets a client be authenticated at /authorize rather than only at /token.

Two delivery modes are supported:

  • request=<JWT> — the JWT is sent inline as a parameter (§6.1).
  • request_uri=<URL> — the JWT is hosted at a URL the OP fetches (§6.2).

WARNING

Earlier AuthHero versions decoded the request JWT without verifying its signature. Any caller could forge claims and have them merged into the authorization request. This page describes the post-fix behavior; that hole is now closed.

How verification works ​

request JWTs are signature-verified against the client's keys before any claim is applied to the authorization request. request_uri URLs are first fetched (with the SSRF guard described below) and the body runs through the same verifier.

Supported algorithms ​

FamilyAlgorithmsVerified against
AsymmetricRS256, RS384, RS512, ES256, ES384, ES512Client's registration_metadata.jwks (inline) or client_metadata.jwks_uri (fetched)
SymmetricHS256, HS384, HS512Client's stored client_secret

alg=none is rejected.

Claim validation ​

The verifier enforces:

  • client_id — required, and must equal the outer client_id parameter (§5).
  • iss — must equal client_id when present.
  • aud — must match the OP issuer when present.
  • exp — required, validated with 30 s leeway.
  • nbf — validated with 30 s leeway when present.

Sending both request and request_uri is rejected with HTTP 400 per the spec.

Parameter handling ​

When a Request Object is supplied, AuthHero uses only the parameters inside it (§5). The outer query string is read for three things: request or request_uri to find the object, and client_id to find the keys that verify it. Everything else in the query is ignored, including:

  • Duplicates. If state, nonce, scope, redirect_uri or any other parameter appears in both places with different values, the signed value wins. The request is not rejected.
  • Gaps. A parameter that is missing from the Request Object cannot be supplied through the query string. A missing response_type fails the request with an error. The error is returned to the signed redirect_uri.
  • AuthHero extensions. connection, screen_hint, login_ticket, realm, vendor_id, auth0Client and the other AuthHero-specific parameters follow the same rule. To use one with a Request Object, sign it into the object.
  • Stored login sessions. A plain /authorize request whose state matches an unfinished login session is filled in from that session's stored parameters. Signed requests never are.

Requests without a Request Object work exactly as before.

A Request Object without redirect_uri behaves like a plain /authorize request without one: the login flow falls back to the client's registered callback. Profiles such as FAPI require the request to fail instead. That per-client enforcement is not implemented yet (see below).

SSRF guard for request_uri ​

request_uri is a server-side fetch that the client controls, so it's exposed to SSRF. AuthHero applies these defaults:

  • https:// only — http:// URLs are rejected.
  • No localhost / 127.0.0.0/8 / link-local / private IPv4 ranges.
  • No private IPv6 ranges (loopback, link-local, ULA).
  • 5-second timeout on the fetch.
  • 64 KiB body cap — larger bodies are truncated and rejected.

For local development and tests, set ALLOW_PRIVATE_OUTBOUND_FETCH: true on the env binding to relax this guard (allows http://, localhost, and private IPs).

Discovery ​

/.well-known/openid-configuration advertises:

json
{
  "request_parameter_supported": true,
  "request_uri_parameter_supported": true,
  "request_object_signing_alg_values_supported": [
    "RS256", "RS384", "RS512",
    "ES256", "ES384", "ES512",
    "HS256", "HS384", "HS512"
  ]
}

Partial / not yet implemented ​

  • request_uri pre-registration — the spec allows an OP to require clients to pre-register their request_uri values; AuthHero currently accepts any URL that passes the SSRF guard.
  • Encrypted request objects (JWE) — only signed JWTs are accepted.
  • require_signed_request_object per-client flag — there's no client-level setting yet to require request objects on every authorize call.
  • FAPI-profile checks — redirect_uri, nonce, nbf and a bounded exp lifetime are not required inside the Request Object.

Test coverage ​

The parameter-handling rules are covered by regression tests in packages/authhero/test/routes/auth-api/request-object.spec.ts. They cover both inline request and fetched request_uri objects: query-only parameters are ignored, signed values win over differing duplicates, and gaps cannot be filled from the query. They also check that a missing or mismatched client_id is rejected and that plain query requests are unchanged.

The conformance runner exercises these OIDC plan modules:

  • oidcc-request-uri-unsigned
  • oidcc-unsigned-request-object-supported-correctly-or-rejected-as-unsupported
  • oidcc-ensure-request-object-with-redirect-uri

The two unsigned-object modules pass when an OP correctly rejects alg=none. Passing them does not show that JAR is implemented.

The upstream suite checks the signed-only rule itself in FAPI plans. Those plans are not wired into the runner yet:

Upstream moduleWhat it checks
fapi1-advanced-final-state-only-outside-request-object-not-useda query-only state is not echoed back
fapi1-advanced-final-ensure-different-nonce-inside-and-outside-request-objectthe signed nonce wins over a differing query value
fapi2-security-profile-final-ensure-different-state-inside-and-outside-request-objectthe signed state wins over a differing query value
fapi1-advanced-final-ensure-request-object-without-scope-failsa query scope cannot fill a gap in the Request Object
fapi1-advanced-final-ensure-request-object-without-redirect-uri-failsFAPI profile: redirect_uri is required in the Request Object
fapi1-advanced-final-ensure-request-object-without-nonce-failsFAPI profile: nonce is required in the Request Object

The first four match the behavior described above. The last two also depend on FAPI-profile requirements that AuthHero does not enforce yet.

Dual-licensed: AGPL-3.0-only or commercial license.