How JSON Web Tokens Work: Claims, Signatures and Common Mistakes

By the CodeBeautify team at Softaware Commerce Ltd · Published

A JSON Web Token (JWT) is three Base64URL-encoded strings joined by dots: a header that names the signing algorithm, a payload of claims, and a signature over the first two. Anyone holding the token can decode and read the header and payload; only the signature, checked with the right key, proves the token came from the issuer and was not altered. A server should accept a token only after it has verified that signature with an algorithm it chose itself and has checked the expiry, issuer and audience claims.

The three parts of a token

The format is defined in RFC 7519 (JWT) and RFC 7515 (JSON Web Signature, JWS). Here is a real HS256 token, generated with Node.js for this guide:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    .eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLTEyMzQiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlhdCI6MTc2NzIyNTYwMCwibmJmIjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjY1MDAsImp0aSI6IjVmMWMyYTllIn0
    .GDf2WqMmWcMi-OASUkdUs3sZiR74b7bvCDamGJcAzy0

It is split over three lines here for readability; the real token is one line with no spaces. Decoding the first two segments gives:

// header
    {"alg":"HS256","typ":"JWT"}

    // payload
    {"iss":"https://auth.example.com","sub":"user-1234","aud":"https://api.example.com",
     "iat":1767225600,"nbf":1767225600,"exp":1767226500,"jti":"5f1c2a9e"}

The third segment is the HMAC-SHA256 of the ASCII string header.payload (the two encoded segments and the dot between them), itself Base64URL-encoded. It was produced like this:

const crypto = require('crypto');
    const b64u = (s) => Buffer.from(s).toString('base64url');

    const header  = b64u(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
    const payload = b64u(JSON.stringify({
      iss: 'https://auth.example.com', sub: 'user-1234', aud: 'https://api.example.com',
      iat: 1767225600, nbf: 1767225600, exp: 1767226500, jti: '5f1c2a9e'
    }));
    const secret = 'correct-horse-battery-staple-7f3a91c2e4b8d605'; // demo only
    const signature = crypto.createHmac('sha256', secret)
      .update(header + '.' + payload)
      .digest('base64url');

    console.log(header + '.' + payload + '.' + signature);

Base64URL, not plain Base64

Each segment uses the URL-safe Base64 alphabet from RFC 4648 section 5: - replaces +, _ replaces /, and the trailing = padding is dropped. The same two bytes 0xFB 0xFF encode as +/8= in standard Base64 and -_8 in Base64URL. That is why a token can go into a URL or a header without further escaping, and why a standard Base64 decoder may reject a segment until you swap the characters back and re-pad it. The Base64 guide covers the encoding in more detail, and the Base64 Encoder / Decoder handles both alphabets.

Header fields: alg, typ and kid

FieldMeaning
algThe algorithm used to sign the token, for example HS256, RS256 or ES256. Required in JWS.
typThe media type of the whole token; JWT is the conventional value. Optional.
kidKey ID: a hint telling the verifier which of several keys was used, which makes key rotation possible. Optional, and its value is chosen by the issuer.

The header is just as untrusted as the payload until the signature has been checked. Treat kid as a lookup key into a set of keys you already hold, never as a file path or database query to run as-is.

Registered claims

RFC 7519 registers seven claim names. All are optional at the format level; your application decides which it requires.

ClaimNameWhat to check
issIssuerEquals the issuer you expect, compared exactly.
subSubjectThe user or entity the token is about, unique within the issuer.
audAudienceA string or an array of strings; your service must be one of them, otherwise reject.
expExpiration timeReject the token on or after this time.
nbfNot beforeReject the token before this time.
iatIssued atWhen the token was created; useful for enforcing a maximum age.
jtiJWT IDA unique identifier, useful for detecting replays or for a deny-list.

exp, nbf and iat are NumericDate values: the number of seconds since 1970-01-01T00:00:00Z, ignoring leap seconds. In the example, iat 1767225600 is 2026-01-01T00:00:00Z and exp 1767226500 is fifteen minutes later. The RFC allows a small leeway, usually no more than a few minutes, to allow for clock skew between servers. The Unix Timestamp Converter turns these numbers into dates.

HS256 versus RS256 and ES256

AlgorithmTypeSigns withVerifies with
HS256HMAC with SHA-256Shared secretThe same shared secret
RS256RSASSA-PKCS1-v1_5 with SHA-256RSA private keyRSA public key
ES256ECDSA, P-256 curve, SHA-256EC private keyEC public key

With HS256, every service that can verify a token can also mint one, because verification and signing use the same secret. That is fine when one service both issues and checks tokens. When several services or third parties need to verify tokens, an asymmetric algorithm is the better fit: only the issuer holds the private key, and verifiers fetch the public key, often from a JSON Web Key Set (JWKS) endpoint, selecting the right key by kid. The algorithms themselves are specified in RFC 7518.

How verification works

  1. Split the token on dots; a signed JWT has exactly three segments.
  2. Decode the header and confirm alg is the algorithm you configured for this issuer. Do not let the token choose.
  3. Recompute the signature over the original encoded header.payload string (not re-serialised JSON) using the key you hold, and compare it with the third segment, ideally in constant time.
  4. Only then parse the payload and check exp, nbf, iss and aud, plus anything your application requires.

Run against the example token with the demo secret and a clock set to 2026-01-01T00:06:40Z, a verifier written this way accepts it. Changing sub to admin without re-signing, using a different secret, or moving the clock past exp all cause rejection. In production, use a maintained library rather than hand-written checks, and configure its allowed algorithms, issuer and audience explicitly.

What a decoder can and cannot tell you

Decoding is not verifying. A decoder reads the header and payload, which requires no key at all. It cannot tell you whether the token is genuine, whether it has been revoked, or whether the server that receives it will accept it.

The JWT Decoder on this site works like this, all in your browser:

  • Decode splits the token, Base64URL-decodes the header and payload and shows them as indented JSON under // HEADER and // PAYLOAD. Anything other than exactly three dot-separated parts is reported as an invalid JWT, with the number of parts found.
  • Every numeric exp, iat or nbf value in the payload gets a comment with the date in ISO 8601 UTC form, for example // 2026-01-01T00:15:00.000Z. It shows the date only; it does not say whether the token has expired, so compare it with the current time yourself.
  • Verify Signature recomputes the HMAC with the Web Crypto API for HS256, HS384 and HS512 only. For any other alg, including RS256, ES256 and none, it says verification is not supported. The secret you type is used as UTF-8 text, so if your server's key is a Base64-encoded byte string that it decodes before use, pasting the encoded text will not match.

Paste the example token, enter the demo secret and the tool reports the signature as valid, even though the token expired on 1 January 2026. That is the point of this section: a valid signature is necessary, not sufficient.

Common mistakes

Trusting alg from the token

Early libraries read alg from the header and verified accordingly. An attacker could then send "alg":"none" with an empty signature, or, against a server expecting RS256, switch to HS256 and sign with the server's public key as the HMAC secret. Always pin the expected algorithm, or list of algorithms, on the server. RFC 8725, the JWT Best Current Practices document, covers both attacks.

Putting secrets or personal data in the payload

A signed JWT is encoded, not encrypted. Passwords, API keys and personal data placed in claims can be read by anyone who sees the token, including browser extensions, proxies and log files. Keep claims to identifiers and permissions.

Weak HMAC secrets

An HS256 token plus a guessable secret can be brute-forced offline, because the attacker can test candidate secrets without contacting your server. RFC 7518 requires a key at least as long as the hash output, so 256 bits for HS256. Generate one randomly, for example with crypto.randomBytes(32), rather than choosing a phrase.

Not checking exp, aud or iss

A correctly signed token from your identity provider for a different application will pass a signature-only check. Validate aud and iss every time, and require exp.

Milliseconds instead of seconds

JavaScript's Date.now() returns milliseconds. Writing it straight into exp creates a token that expires tens of thousands of years from now: the decoder shows 1767226500000 as +057971-03-07T10:00:00.000Z. Use Math.floor(Date.now() / 1000).

Long-lived tokens with no way to revoke them

A JWT stays valid until it expires, even after the user logs out or is disabled, unless the server keeps some state. Common approaches are short access-token lifetimes (minutes) paired with refresh tokens that the server can revoke, or a deny-list keyed on jti.

JWS versus JWE

Almost every token called a JWT is a JWS: signed, readable by anyone. JSON Web Encryption (RFC 7516) encrypts the content instead, and its compact form has five dot-separated parts rather than three. Use JWE when the claims must be confidential from the client or intermediaries. The decoder here handles only three-part signed tokens.

Frequently asked questions

Can I read a JWT without the secret?

Yes. The header and payload are only Base64URL-encoded, so any decoder can show them. The secret or public key is needed only to check the signature.

Is it safe to paste a production token into an online decoder?

A live token is a credential for as long as it is valid. The decoder on this site runs in your browser and does not send the token anywhere, but it is still good practice to use expired or test tokens when debugging.

How long should a JWT last?

There is no single rule. Access tokens are commonly kept short, from a few minutes to an hour, because they cannot easily be revoked; longer sessions are handled with refresh tokens the server can invalidate.

Why does my token fail with a valid signature?

Usually a claim check: exp in the past, nbf in the future because of clock skew, or an aud or iss value that does not match what the verifying service expects.

Tools for this guide