How JWT Works: Structure, Signing, and Verification

A JSON Web Token (JWT) is a compact, URL-safe string made of three base64url-encoded parts separated by dots: a header, a payload, and a signature. The signature proves the token has not been tampered with — but the payload is not encrypted and can be read by anyone who holds the token.

Want to inspect a JWT right now? Paste any token into Jsonic's JWT Decoder to see the decoded header, payload claims, and expiration time instantly.

Decode a JWT

JWT anatomy: the 3-part structure

Every JWT looks like this — three base64url strings joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Split on the dots and you get three segments:

PartContentExample (decoded)
HeaderAlgorithm and token type{"alg":"HS256","typ":"JWT"}
PayloadClaims (user data + metadata){"sub":"1234567890","exp":1516242622}
SignatureHMAC or RSA digestBinary bytes, base64url-encoded

The formula is: base64url(header) + "." + base64url(payload) + "." + base64url(signature). Neither the header nor the payload is encrypted — base64url is just an encoding, not encryption. The signature is what makes the token trustworthy.

Header and algorithm

The header is a small JSON object. It tells the verifier which algorithm was used to create the signature. The alg field is critical — the verifier uses it to pick the right key and hash function.

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

Common alg values are HS256, RS256, and ES256. The typ field is always "JWT" for a standard JSON Web Token. Some libraries also include a kid (key ID) field when the server rotates signing keys, so verifiers know which key to use.

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "2024-key-1"
}

Payload and standard claims

The payload is a JSON object containing claims — statements about the subject (usually a user) plus metadata about the token itself. The IANA JWT registry defines a set of standard (registered) claim names:

ClaimFull nameMeaning
issIssuerWho created and signed the token (e.g. "https://auth.example.com")
subSubjectWho the token is about — typically a user ID
audAudienceIntended recipient(s) — the verifier must match this value
expExpirationUnix timestamp after which the token must be rejected
iatIssued atUnix timestamp when the token was created
jtiJWT IDUnique identifier — used to prevent replay attacks

A typical payload looks like this. Custom claims (like role and name) are perfectly valid alongside registered claims:

{
  "sub": "1234567890",
  "name": "John Doe",
  "role": "admin",
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com",
  "iat": 1516239022,
  "exp": 1516242622
}

The exp and iat values are seconds since the Unix epoch (not milliseconds). A one-hour token has exp = iat + 3600.

Signature: how it works

The signature is computed from the encoded header and encoded payload using a cryptographic function and a secret or private key. For HS256 (HMAC with SHA-256):

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

The result is a 256-bit (32-byte) digest, which is then base64url-encoded and appended as the third part of the token.

For RS256 (RSA with SHA-256), the process is similar but asymmetric:

// Signing (server only, uses private key)
signature = RSA_SHA256_sign(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)

// Verifying (any party with the public key)
valid = RSA_SHA256_verify(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  signature,
  publicKey
)

Because the signature covers both the header and payload, any change to either part — even a single character — produces a completely different signature. A verifier immediately detects tampering by recomputing the expected signature and comparing it to the received one.

HS256 vs RS256 vs ES256

The three most common JWT signing algorithms differ in the type of key they use and the scenarios they fit best:

AlgorithmTypeKeyUse when
HS256Symmetric HMAC-SHA256Single shared secretSingle service or server-to-server where the signer and verifier are the same party
RS256Asymmetric RSA-SHA256Private key (sign) + Public key (verify)Multiple services need to verify tokens — share only the public key
ES256Asymmetric ECDSA-SHA256EC private key (sign) + EC public key (verify)Same as RS256 but produces a smaller signature — better for mobile and bandwidth-constrained clients

HS256 is simple to set up but every service that verifies tokens must hold the secret. A compromised verifier exposes the signing key. RS256 and ES256 eliminate that risk: the private key never leaves the auth server, and public keys can be distributed freely via a JWKS (JSON Web Key Set) endpoint.

How JWT authentication works

JWTs are most commonly used in the Authorization header to authenticate API requests. Here is the complete flow:

// 1. Client logs in with credentials
POST /auth/login
{ "email": "user@example.com", "password": "hunter2" }

// 2. Server verifies credentials, issues a JWT
HTTP 200 OK
{ "token": "eyJhbGci..." }

// 3. Client stores the token (memory or HttpOnly cookie)

// 4. Client sends the token with every protected request
GET /api/profile
Authorization: Bearer eyJhbGci...

// 5. Server verifies the token without a database lookup:
//    a) Re-compute signature from header + payload + secret
//    b) Compare computed signature to received signature
//    c) Check exp > now
//    d) Check iss and aud match expected values
//    e) If all pass → request is authenticated

The key advantage of JWTs is statelessness: the server does not need to store session data. All the information needed to authenticate a request is self-contained in the token. This makes JWTs horizontally scalable — any server instance can verify any token without a shared session store.

Step-by-step token verification

When your server receives a JWT, the verification process follows these exact steps:

// Node.js example using the jsonwebtoken library
import jwt from 'jsonwebtoken'

function verifyToken(token: string, secret: string) {
  // Steps 1–5 happen inside jwt.verify():
  // 1. Split token into header, payload, signature
  // 2. base64url-decode header → read "alg"
  // 3. Re-compute signature: HMACSHA256(header + "." + payload, secret)
  // 4. Compare computed signature to received signature (timing-safe)
  // 5. Parse payload, check exp, iss, aud
  try {
    const decoded = jwt.verify(token, secret, {
      algorithms: ['HS256'],        // whitelist algorithms
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com',
    })
    return decoded  // { sub, name, role, iat, exp, ... }
  } catch (err) {
    // TokenExpiredError, JsonWebTokenError, NotBeforeError
    throw new Error('Invalid or expired token')
  }
}

Always whitelist the algorithms option. Without it, a JWT with "alg": "none" in the header can bypass signature verification on vulnerable libraries.

Common JWT mistakes

MistakeWhy it mattersFix
Storing sensitive data in the payloadPayload is base64url, not encrypted — anyone can read itStore only non-sensitive identifiers (user ID, role). Never store passwords or PII.
Not checking expExpired tokens remain accepted indefinitelyAlways verify exp — use a library that does this automatically
Using a weak or hardcoded secretBrute-force or leaked secrets allow token forgeryUse at least 32 random bytes for HS256 secrets; rotate secrets periodically
Not whitelisting alg"alg":"none" attack bypasses signature checkAlways pass an explicit algorithms list to your JWT library
Storing JWT in localStorageAccessible to any script — vulnerable to XSSUse memory or an HttpOnly cookie with SameSite=Strict
Very long token TTL without refreshStolen tokens remain valid for hours or daysUse short-lived access tokens (15 min) with refresh tokens for re-issue

Frequently asked questions

Is the JWT payload encrypted?

No. The payload is base64url encoded, not encrypted. Base64url is a reversible encoding — anyone who holds the token can decode and read the claims. Never put passwords, credit card numbers, or other sensitive data in the payload. If you need a confidential payload, use JWE (JSON Web Encryption) instead of a plain JWT.

How does a server verify a JWT without storing it?

The server recomputes the signature from the received header and payload using its own secret (HS256) or public key (RS256). If the recomputed signature matches the one in the token, the token is authentic. The server then checks exp, iss, and aud. No database lookup is needed — all the information is self-contained in the token.

What is the difference between HS256 and RS256?

HS256 is symmetric: a single shared secret both signs and verifies tokens. Every service that verifies tokens must hold that secret. RS256 is asymmetric: a private key signs the token and a public key verifies it. The private key never leaves the auth server, and public keys can be published freely. RS256 is the better choice when multiple independent services need to verify tokens.

How do I check if a JWT has expired?

Decode the payload by base64url-decoding the middle segment, then read the exp claim. It is a Unix timestamp in seconds. In JavaScript:

const payload = JSON.parse(atob(token.split('.')[1]))
const isExpired = Date.now() / 1000 > payload.exp

Most JWT libraries (jsonwebtoken, jose, python-jwt) perform this check automatically inside their verify function and throw a TokenExpiredError when the token has expired.

Where should I store a JWT in the browser?

The two safest options are in-memory (a JavaScript variable or React state) and an HttpOnly cookie. In-memory storage is invisible to scripts and immune to XSS, but the token is lost on page refresh. An HttpOnly cookie survives refreshes and is inaccessible to JavaScript, but requires CSRF protection (use SameSite=Strict or a CSRF token). Avoid localStorage — it is readable by any script on the page and is a common XSS target.

What happens if the JWT secret is compromised?

With a compromised HS256 secret, an attacker can forge valid tokens for any user or role. Rotate the secret immediately — all existing tokens signed with the old secret become invalid, which will log out all active users. For RS256, revoke the private key and publish a new public key via your JWKS endpoint. Consider reducing the token TTL or implementing a token blocklist during the rotation window.

Inspect a JWT token

Paste any JWT into Jsonic's JWT Decoder to decode the header and payload, read all claims, and check the expiration time in human-readable format.

Open JWT Decoder