JWT Tokens Explained: How to Decode, Verify, and Debug JSON Web Tokens

JSON Web Tokens are the backbone of modern authentication. This guide breaks down JWT structure, explains signing algorithms, covers common security mistakes, and shows you how to decode and inspect tokens safely.

What Is a JWT Token?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format used to securely transmit information between two parties. Defined by RFC 7519, JWTs are the backbone of modern authentication and authorisation flows — from single-page apps calling REST APIs to microservices verifying each other's identity.

Unlike opaque session tokens that require a server-side lookup, JWTs are self-contained: everything needed to validate the token and extract user information is baked right into the token itself. This makes them stateless, scalable, and ideal for distributed systems.

JWT Structure: Header, Payload, and Signature

Every JWT consists of three Base64URL-encoded parts separated by dots:

header.payload.signature

// Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header

The header is a JSON object that describes the token type and the signing algorithm. It is Base64URL-encoded to form the first segment.

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

2. Payload (Claims)

The payload carries the claims — statements about the user and additional metadata. Claims come in three flavours:

  • Registered claims: Standardised fields like iss, exp,sub, and iat.
  • Public claims: Custom fields registered in the IANA JSON Web Token Claims registry to avoid collisions.
  • Private claims: Application-specific fields agreed upon between parties (e.g.,role, orgId).
{
  "sub": "user_4821",
  "name": "Jane Smith",
  "role": "admin",
  "iss": "https://auth.example.com",
  "iat": 1716163200,
  "exp": 1716166800
}

3. Signature

The signature is created by taking the encoded header and payload, concatenating them with a dot, and signing the result using the algorithm specified in the header.

// HMAC-SHA256 example
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)
ℹ️

Base64URL encoding is slightly different from standard Base64: it replaces + with-, / with _, and strips trailing = padding. This makes the token safe for URLs, headers, and cookies.

Signing Algorithms: HS256 vs RS256

The signing algorithm is arguably the most critical decision when working with JWTs. The two most common algorithms are:

AlgorithmTypeKeyBest For
HS256Symmetric (HMAC)Shared secretSingle-service apps where issuer and verifier are the same
RS256Asymmetric (RSA)Private key signs, public key verifiesMicroservices, third-party integrations, OIDC providers
ES256Asymmetric (ECDSA)Smaller keys, faster operationsMobile apps, IoT, performance-sensitive systems

With HS256, the same secret is used to sign and verify the token. This is simple but means every service that needs to verify tokens must have access to the secret. With RS256, only the auth server holds the private key; any service can verify tokens using the freely distributed public key.

⚠️

Never accept tokens where alg is set to "none". This is a well-known attack vector where an attacker removes the signature entirely. Always validate the algorithm against an explicit allowlist on the server.

Common JWT Claims Explained

The registered claims defined in RFC 7519 provide a shared vocabulary. Here are the most important ones:

ClaimFull NameDescription
issIssuerWho issued the token (e.g., your auth server URL)
subSubjectThe principal the token represents (usually a user ID)
audAudienceIntended recipient(s) of the token (e.g., an API identifier)
expExpiration TimeUnix timestamp after which the token is invalid
iatIssued AtUnix timestamp when the token was created
nbfNot BeforeToken is not valid before this Unix timestamp
jtiJWT IDUnique identifier for the token (useful for revocation)

The Verification Process

When your server receives a JWT, it must verify the token before trusting its contents. Here is the standard verification flow:

1

Parse the Token

Split the token by dots into header, payload, and signature segments.

2

Decode the Header

Base64URL-decode the header and confirm that the alg field matches your expected algorithm. Reject tokens with "alg": "none" or unexpected algorithms.

3

Verify the Signature

Recompute the signature using the header and payload with your key (secret or public key). Compare it to the signature in the token. If they don't match, reject the token immediately.

4

Validate the Claims

Check exp (not expired), nbf (not used too early), iss(trusted issuer), and aud (intended for your service).

// Node.js verification with jsonwebtoken
const jwt = require('jsonwebtoken');

try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],       // explicit allowlist
    issuer: 'https://auth.example.com',
    audience: 'my-api',
  });
  console.log('User ID:', decoded.sub);
} catch (err) {
  console.error('Token verification failed:', err.message);
}

Common Security Mistakes

JWTs are secure by design, but misusing them can introduce serious vulnerabilities. Watch out for these common pitfalls:

  • Storing tokens in localStorage: This exposes tokens to cross-site scripting (XSS) attacks. Prefer HttpOnly, Secure, SameSite cookies instead.
  • Not validating the algorithm: An attacker can change the header to"alg": "none" or switch from RS256 to HS256 (using the public key as the HMAC secret). Always enforce the algorithm server-side.
  • Overly long expiration times: Tokens valid for days or weeks increase the window of compromise. Use short-lived access tokens (5–15 minutes) paired with refresh tokens.
  • Sensitive data in the payload: Remember, the payload is only encoded, not encrypted. Anyone can decode it. Never put passwords, credit card numbers, or secrets in the payload.
  • Ignoring token revocation: JWTs are stateless, making revocation non-trivial. Use short expiration times, token blacklists, or the jti claim for revocation scenarios.
💡

Use a dedicated JWT debugger (like jwt.io or a local decoding tool) during development to inspect token contents. Paste the token and immediately see the decoded header, payload, and signature verification status.

Debugging JWT Issues

When a JWT-based flow breaks, here's a systematic approach to tracking down the problem:

  1. Decode the token: Paste it into a JWT decoder to inspect the header and payload. Verify the claims look correct.
  2. Check expiration: Convert the exp claim to a human-readable date. Clock skew between servers is a common cause of premature expiration.
  3. Verify the issuer and audience: Mismatched iss or aud values cause silent rejections in many libraries.
  4. Confirm the signing key: For HS256, ensure the secret matches exactly (including encoding). For RS256, verify you're using the correct public key or JWKS endpoint.
  5. Inspect the HTTP headers: Confirm the token is being sent asAuthorization: Bearer <token> and not accidentally truncated.

Conclusion

JWTs are a powerful, standardised way to handle authentication and authorisation in modern applications. Their self-contained nature makes them perfect for stateless architectures, but that same power demands careful implementation. By understanding the structure, choosing the right algorithm, validating claims rigorously, and avoiding common security pitfalls, you can build JWT-based systems that are both secure and scalable.

When in doubt, lean on well-maintained libraries for signing and verification — don't roll your own crypto. And always keep a JWT debugger handy during development; it will save you hours of frustration.

🎯 Key Takeaways

  • JWTs have three parts: a Base64URL-encoded header, payload, and signature separated by dots.
  • Use HS256 for single-service setups and RS256/ES256 for distributed or third-party systems.
  • Always validate alg, exp, iss, and aud server-side.
  • Never store sensitive data in the payload — it is encoded, not encrypted.
  • Store tokens in HttpOnly cookies, not localStorage, to mitigate XSS attacks.
  • Use short-lived access tokens (5–15 min) with refresh tokens for a secure token lifecycle.
← Назад в блог