skip to content

In a JWT, what is the difference between decoding a token and verifying it?

level: juniorimportance: must knowfreq 78%

answer

  1. One of the two needs no key
  2. Base64url is encoding, not protection
  3. Attacker can do the same operation
  4. Signature check must come first

basics

~20 s

Decoding only Base64url-unpacks a JWT's header and payload, which anyone holding the token can do. Verifying recomputes the signature with a trusted key and checks the claims; only verification tells you the token is authentic and still usable.

solid answer

~40 s

Decoding a JWT is pure parsing: split the compact string on its dots, Base64url-decode the first two parts, and you have the JOSE header and the claims JSON. It needs no key, so an attacker can decode a token too — and can equally well *re*-encode a modified payload. Verification is the security step: recompute or check the signature over the exact `header.payload` bytes using a key you trust, and only then evaluate the claims (`exp`, `nbf`, `iss`, `aud`). A common junior bug is reading `sub` or a role claim out of a decoded token and acting on it without ever verifying — that trusts input the caller fully controls. Rule of thumb: nothing inside a JWT means anything until the signature check has passed with a key from a trusted source.

code

json · 5 lines
json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "2024-11-a"
}

go deeper

for a junior

Be ready to state in one sentence that decoding needs no key and proves nothing, while verification checks a signature against a key you trust. Know that a JWT payload is readable by anyone.

for a middle

Explain the mechanics: the signature covers the exact transmitted header.payload bytes, so verification must run on the raw segments, and claim checks come after the signature passes.

for a senior

Show where this goes wrong in real code — debug decoders promoted into request handling, claims read before verification, frontend patterns copied to the backend — and how you'd catch it in review or with a lint rule.

for a principal

Own the platform position: one verification path shared by every service, no ad-hoc decoding in application code, and an explicit statement that bearer-token verification proves origin and integrity but never possession legitimacy.

## The two operations are not the same thing A JWT in compact serialization is three Base64url-encoded segments joined by dots: `header.payload.signature`. **Decoding** means taking that string apart — split on `.`, Base64url-decode segment one into the JOSE header JSON, segment two into the claims JSON. **Verification** means proving that those bytes were produced by a party holding a key you trust, and that the claims they carry are still applicable to you, right now. Decoding requires no secret, no public key, and no network call. Base64url is an encoding, not encryption. Any tool, any browser console, any attacker who has intercepted the token can decode it in a second. That means decoding proves exactly one thing: the string was well-formed. ## Why the distinction is a security issue, not a pedantic one Because the caller supplies the token, the caller also controls its bytes. An attacker can decode a token, change `"role": "user"` to `"role": "admin"` or `"sub": "123"` to someone else's identifier, re-encode the payload, and send it back. If your code reads claims from the decoded payload and acts on them, you have just accepted attacker-authored JSON as your authorization decision. The signature is the only thing standing between those two situations, and it only helps if you actually check it. The mistake shows up in recognisable shapes: - A debugging helper that decodes the payload to log the user id gets promoted into request handling. - Frontend code decodes the token to render the UI (legitimate — the UI is not a trust boundary) and someone copies that pattern into a backend handler (illegitimate — that *is* the trust boundary). - A library API offers both a `decode` and a `verify` entry point, and the shorter name wins in a hurry. ## What verification actually involves Verification has a cryptographic half and a semantic half, and both are mandatory. The cryptographic half operates on the **exact ASCII bytes** of `header.payload` as they arrived — not on a re-serialized version of the parsed JSON. JSON has many byte-level representations of the same object (key order, whitespace, escaping), so re-encoding before checking would produce a different signing input and either fail spuriously or, worse, verify something other than what was sent. For an HMAC algorithm the verifier recomputes the MAC over those bytes with the shared secret and compares; for a signature algorithm it runs the public-key verification routine. Which key to use, and which algorithm you are willing to accept, must be decided by the *verifier's* configuration. The semantic half evaluates the claims: is the token expired (`exp`), not yet usable (`nbf`), issued by the party you expect (`iss`), and addressed to this service (`aud`)? A cryptographically perfect signature on a token minted for a different service, or one that expired last week, is still a token you must reject. ## Order matters Check the signature before you trust anything from the header or payload. Claims from an unverified token are untrusted input, and code that branches on them — picking a tenant, picking a key, picking an algorithm — is making decisions on attacker-supplied data. The one thing you legitimately read before verification is the key hint (`kid`) in the header, and even that is treated as an opaque lookup key into a set of keys you already trust, never as an instruction. ## What a successful verification does and does not prove It proves: the token was signed by the holder of the trusted key, the payload has not been altered since, and the time and audience conditions you checked currently hold. It does not prove: that the person presenting the token is the person it was issued to. A JWT is a bearer credential — whoever holds it can use it. Verification says nothing about theft, and nothing about events that happened after minting (a password change, a disabled account, a revoked session), because a signed token is a frozen snapshot of what was true at issue time. ## Practical framing for an interview Say it in one line: *decoding reads, verifying trusts*. Then give the failure: any code path that reads a claim without a prior successful signature check is trusting user input. Interviewers ask this because it separates candidates who see a JWT as a data format from candidates who see it as a credential.

  • Debugging sites let you paste a JWT and read the claims. Does that mean JWTs are insecure?
    No — it means they are not confidential. A JWT is signed, not encrypted, so the payload is readable by anyone holding the token; that is by design. The security property is integrity: nobody can alter the claims without invalidating the signature. It only becomes a defect if you put data in the payload that must stay secret.
  • Is it acceptable for a browser frontend to decode a JWT without verifying it?
    Yes, for display purposes only — showing a username, or hiding a menu item the token's claims say the user cannot use. The browser is not a trust boundary, and the same user could edit the DOM anyway. The server must still verify the token and re-check authorization on every request; the frontend decode is a convenience, never an enforcement point.
  • Why must the signature be verified over the raw received bytes rather than a re-serialized payload?
    The signing input is the literal ASCII of `header.payload` as transmitted. JSON allows many byte encodings of one object — key order, whitespace, unicode escaping — so re-serializing after parsing usually produces different bytes and a mismatched signature. Libraries therefore keep the original segments around and verify those, then hand you the parsed claims.

saying these in an interview costs you the question

  • Thinks decoding a JWT requires the signing key
  • Reads claims from an unverified token in server code
  • Says the payload is encrypted because it looks random
  • Verifies the signature but never checks exp or aud
  • Believes a valid signature proves the sender is the subject

context