Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

Response validation and failures

A JWT has arrived at the printer's callback. It contains a code, and the printer would like to exchange it. JARM sets a firm rule about that: the client must not process the code, or any other parameter of the response, until every check on the JWT has passed.

Checks before the code is used

The printer works through the response in this order:

  1. Decrypt it, if it was encrypted, with the printer's own private key.
  2. Check the issuer. Read iss and compare it with the issuer this browser session's pending attempt was sent to, https://auth.photos.example. Stop if it differs.
  3. Check the audience. aud must be the client ID the printer used in the request, photo-printer.
  4. Check the expiry. exp must not have passed, allowing at most a small tolerance for clock differences.
  5. Verify the signature with a key from the photo service's key set, using an algorithm the printer expects. none is never accepted.
  6. Only now use the contents. Match state to the pending attempt in this session, then exchange the code with the PKCE verifier and client authentication, as in any code flow.

The order of steps 2 and 5 may look backwards. Why read the issuer before the signature is verified? Because the issuer decides which keys to verify with, and the printer must not let an unverified JWT choose them. The printer looks up the issuer it expected in its own configuration and uses the key set configured for that issuer. If it fetched keys from wherever an incoming JWT said its issuer lived, an attacker could make the printer fetch from an address of the attacker's choosing, a slow or enormous one, or someone else's server it should not be calling.

In code, the order looks like this:

const pending = session.pendingAttempt();       // saved when the request was sent
const jwt = callbackParams.get('response');
const claims = decodeWithoutTrusting(jwt);      // readable, not yet trusted

if (claims.iss !== pending.issuer) return reject('unexpected_issuer');
if (claims.aud !== pending.clientId) return reject('wrong_audience');
if (claims.exp <= nowInSeconds() - CLOCK_TOLERANCE) return reject('expired_response');

const keys = await keySetFor(pending.issuer);   // from configuration, never from the JWT
if (!verifySignature(jwt, keys, ['ES256'])) return reject('bad_signature');

// Every check passed. Now state and code may be used.
if (claims.state !== pending.state) return reject('state_mismatch');
await exchangeCode(claims.code, pending.codeVerifier);

The helper names are illustrative. What matters is that no branch reaches exchangeCode without passing every check above it.

Errors inside the JWT

All values in these examples are fictional. When you decline the connection, the error arrives in the same form as a success. Its decoded claims read:

{
  "iss": "https://auth.photos.example",
  "aud": "photo-printer",
  "exp": 1790845500,
  "error": "access_denied",
  "state": "demo-attempt-7"
}

The printer validates it exactly like a success, through step 5, before believing it. A verified error is reliable information: the photo service really did refuse, for this attempt. An error that fails validation is not evidence that you or the photo service refused anything, and the printer should not record your connection as declined because of it.

A plain error=access_denied in the query string deserves the same caution. The printer asked for a signed response, so an unsigned one at its callback could have been written by anyone. The safest reading is that the attempt failed for an unknown reason, with an honest message and a fresh attempt on offer. Some failures never reach the callback at all. If the return address itself is invalid, the photo service shows its own error page and does not redirect, signed response or not.

The iss parameter and the iss claim

Without JARM, the printer learns which server answered from the plain iss response parameter. JARM puts the same value inside the signed JWT. Validated the same way, the claim gives the same protection against authorization server mix-up as the parameter, and it is covered by the signature as well. A separate iss parameter outside the JWT is therefore unnecessary, and the FAPI 2.0 Message Signing profile advises servers to send iss only inside the JWT.

A server might still send both. Then the response contains two issuer identifiers, and they must match exactly. If they differ, the printer rejects the response rather than choosing whichever one suits it.

When a check fails

Most failures have mundane causes, and a few deserve a closer look:

FailureLikely causeWhat the printer does
Unknown kidThe photo service has started signing with its next key, photos-2027-01, and the printer's cached key set is old.Fetch the key set again from its configured address, at most once in a short period, and verify again. Reject if the key is still unknown.
Expired expThe response was delayed or replayed, or the printer's clock is wrong.Reject and offer a fresh attempt. Check the printer's own clock if this happens often.
Wrong audA response issued to another client reached this callback, for example through a misconfigured test deployment or a deliberate replay.Reject.
Unexpected issAnother server answered, or someone is attempting a mix-up.Reject, and never send the code anywhere.
Bad signature or unexpected algThe JWT was altered or forged.Reject.

Every row ends the same way. The printer makes no token request, keeps the code out of its logs, and tells you it could not connect your photo account, with an option to try again. Its own log records the stage, the failed check, and the attempt's correlation ID, never the JWT itself, which still contains a code.

The issuer checks in this lesson follow the same comparison rules as the plain iss parameter. The next group, Authorization response issuer identification, sets out those rules and what the issuer check can and cannot protect.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2Why does the printer check iss before verifying the JARM signature?

QUESTION 2 OF 2A JARM response contains error=access_denied, but its signature does not verify. What should the printer conclude?

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

Learn identity