skip to content

What checks must a verifier run before accepting a JWT, and in what order?

level: middleimportance: must knowfreq 82%

answer

  1. Signature before any claim
  2. The verifier picks the algorithm, not the token
  3. Two time claims, one small tolerance
  4. Two identity claims: who issued, who for
  5. Valid is not the same as still active

basics

~20 s

Verify the signature first, using a key from a trusted source and an algorithm the verifier chose, not the token. Then check the time claims exp and nbf with a small leeway, the issuer, and the audience. Reject on any failure.

solid answer

~50 s

Validation is an ordered pipeline, and the order is part of the security. First, parse the compact form and read the header only as a hint. Second, decide the acceptable algorithm from **your** configuration and resolve the key from **your** trusted key material, using `kid` purely as a lookup. Third, verify the signature over the exact received `header.payload` bytes. Only after that do the claims mean anything, so fourth: check `exp` (expired?), `nbf` (not yet valid?), each compared against current time with a small leeway of at most a minute or so for clock drift; check `iss` equals the issuer you trust; and check `aud` contains this service's identifier. Anything else the application needs — scopes, tenant, token type — is checked last. Every failure is a rejection, not a warning, and the response should not explain which check failed.

code

json · 9 lines
json
{
  "iss": "https://auth.example.com/",
  "aud": ["orders-api", "billing-api"],
  "sub": "user-1042",
  "iat": 1764496400,
  "nbf": 1764496400,
  "exp": 1764497300,
  "jti": "9f1c2b7e-3c1a-4f0d-9b2e-77a1c0a5e001"
}

go deeper

for a junior

Be able to name the checks: signature, expiry, not-before, issuer, audience. Know that a failure means rejecting the request, not logging a warning and continuing.

for a middle

Explain why the order matters and where each value comes from — the algorithm and key from your configuration, the time claims compared against a clock with a small leeway, the issuer matched exactly.

for a senior

Demonstrate the operational side: fail closed when keys are unreachable, generic errors outward with detailed logging inward, never log the raw token, and know that validity says nothing about an ended session.

for a principal

Own consistency across services — one shared verification implementation with pinned algorithms and mandatory audience, so no team can quietly ship a decoder that skips a step.

## Why the checklist exists A JWT is a bearer credential that arrives entirely under the caller's control. Acceptance therefore has to be a deliberate sequence of checks, each of which can only reject. Skipping one does not make the token slightly less trustworthy — it usually makes it forgeable or replayable somewhere it should not work. Interviewers ask for the list because a candidate who can only say "check the signature" has half a control. ## Step 0 — parse defensively Split the compact serialization on `.` and require exactly the expected number of segments. Reject unusual shapes early: a missing signature segment, extra segments, non-Base64url characters. The header is now readable, but it is *input*, not configuration. ## Step 1 — decide the algorithm yourself The verifier must know, before it looks at the token, which algorithms it will accept for this issuer — for example "RS256 only". The `alg` header is then compared against that expectation and the token is rejected if it does not match. The failure mode of letting the token choose is the whole family of algorithm-substitution attacks, and it is entirely avoided by treating `alg` as an assertion to check rather than an instruction to follow. ## Step 2 — resolve the key from trusted material For a symmetric algorithm the key is a secret both sides configured. For an asymmetric one the verifier holds the issuer's public keys, typically fetched from that issuer's published key set. The `kid` header selects among keys you already trust: it is an opaque lookup token, never a path, never a URL, never a query fragment. If `kid` is absent, a verifier holding several keys may try each candidate key of the right type, but the cleanest deployments require `kid`. If no trusted key matches, reject. ## Step 3 — verify the signature over the transmitted bytes The signing input is the ASCII string `base64url(header) + "." + base64url(payload)` exactly as received. Verify against that, not against a re-serialization of the parsed JSON, because JSON key order and whitespace are not canonical. A failed signature ends processing immediately; there is no partial credit and no reason to look at claims. ## Step 4 — time claims `exp` (expiration time) and `nbf` (not before) are NumericDate values: seconds since the Unix epoch, UTC. Reject when current time is at or past `exp`, and when current time is before `nbf`. Both comparisons get a small **leeway** because independent machines never agree perfectly on the clock; a few seconds to a minute is normal, and the specification's own guidance is that any such leeway should be small — minutes, not hours. Large leeway is quietly dangerous: it extends the lifetime of every token, including one you were counting on to expire quickly. `iat` (issued at) is not an expiry check, but a verifier may reject implausibly old tokens, and some designs compare `iat` against a per-user cutoff to invalidate sessions. ## Step 5 — issuer and audience `iss` must equal the issuer string you configured for this trust relationship — an exact string comparison, not a prefix or a substring match. `aud` must contain this service's own identifier; it is either a string or an array of strings, and code must handle both. The audience check is what stops a token minted for a low-value internal service being replayed against a high-value one, in a system where several services trust the same issuer. ## Step 6 — application-level checks Only now do scopes, roles, tenant identifiers, or a token-type marker in `typ` mean anything. Authorization is a separate decision that consumes the verified claims; a valid token is permission to *ask*, not permission to *do*. ## Failure behaviour and observability Fail closed. If the key set cannot be reached and no cached key matches, reject rather than skipping verification. Return a generic authentication failure to the caller — telling an attacker whether the signature or the audience failed is free information. Internally, log the failure reason, the `kid`, the `iss`, and the token identifier if present, but never the raw token: logs are a common place for credentials to leak. ## What the checklist deliberately does not cover Even a fully valid token can belong to a session that should be over — a logged-out user, a disabled account, a changed password. Nothing in this pipeline detects that, because a signed token is a frozen statement about the past. That gap is a design problem answered separately, with short lifetimes and some form of revocation lookup. ## Interview framing Recite it as a pipeline with a reason attached to each stage — algorithm pinned by the verifier, key from trusted material, signature over raw bytes, time with small leeway, issuer exact, audience contains me — and finish with the honest limit: validity is not liveness.

  • How much clock leeway is reasonable, and what does a large value cost you?
    Seconds to about a minute. Leeway exists for NTP drift between independent machines, not for convenience. Every second of leeway extends the effective lifetime of every token you issue, so a five-minute allowance on a five-minute token doubles the window an attacker has with a stolen credential. Fix the clocks instead of widening the tolerance.
  • The aud claim can be a single string or an array. Why does that matter in practice?
    Because naive code that assumes a string throws or silently skips the check when it meets an array, and code that assumes an array does the reverse. The correct check is membership: the verifier's own identifier must be present among the audience values. A skipped audience check is invisible in testing and only shows up when a token is replayed at another service.
  • If the key set endpoint is unreachable and you have no cached key for the token's kid, what should the verifier do?
    Reject and return an authentication failure. Failing open — accepting the token unverified because the key could not be fetched — turns a dependency outage into an authentication bypass. Mitigate availability with cached keys and a short retry, not by relaxing verification.
  • Should the error response tell the caller which check failed?
    No. Give the client a generic authentication failure and keep the detail in your own logs with the kid, issuer and token identifier. Distinguishing "bad signature" from "wrong audience" tells an attacker which knob to turn next, and the legitimate client's remedy is the same either way: obtain a fresh token.

saying these in an interview costs you the question

  • Reads the alg header and uses whatever it says
  • Checks claims first and the signature afterwards
  • Sets clock leeway to several minutes for convenience
  • Matches iss with a prefix or contains check
  • Skips aud because there is only one service today

context