DevToys Web Pro iconDevToys Web ProBlogi
Arvostele meidät:
Kokeile selainlaajennusta:
← Back to Blog

How to Decode a JWT Token: Structure, Claims, and Verification

12 min read

You receive a JWT from an authentication server and need to know what's inside. Or a login fails and you want to check whether the token has expired. Or you're building an API and need to extract the user ID from the sub claim. This guide shows you exactly how JWT tokens are structured, how to decode them, and how to verify that they haven't been tampered with.

What Is a JWT Token?

A JSON Web Token (JWT) is a compact, self-contained string that represents a set of claims — statements about a user or system — in JSON format. JWTs are used to transmit authentication information between parties (typically a client and a server) in a way that can be cryptographically verified.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three Base64url-encoded segments separated by dots. Each segment has a distinct role.

JWT Structure: Three Parts

Every JWT consists of exactly three parts:

[HEADER].[PAYLOAD].[SIGNATURE]
PartContainsEncoded As
HeaderAlgorithm and token typeBase64url(JSON)
PayloadClaims (user data, expiry, etc.)Base64url(JSON)
SignatureCryptographic proof of integrityBase64url(bytes)

Part 1: Header

The header identifies which algorithm was used to sign the token.

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

Common alg values:

AlgorithmTypeNotes
HS256HMAC-SHA256 (symmetric)Same secret for signing and verifying
HS512HMAC-SHA512 (symmetric)Larger hash, same shared-secret model
RS256RSA-SHA256 (asymmetric)Private key signs, public key verifies
ES256ECDSA-SHA256 (asymmetric)Smaller keys than RSA, same trust model
noneNo signatureDangerous — never accept in production

Part 2: Payload (Claims)

The payload is a JSON object containing claims — key-value pairs that carry the actual information. There are three categories of claims:

Registered Claims (standard, short names)

ClaimFull NameDescription
subSubjectIdentifies the principal (e.g., user ID)
issIssuerWho created the token (e.g., "auth.myapp.com")
audAudienceIntended recipient(s)
expExpiration TimeUnix timestamp after which token is invalid
nbfNot BeforeUnix timestamp before which token is not valid
iatIssued AtUnix timestamp when token was created
jtiJWT IDUnique identifier for the token

Example Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Part 3: Signature

The signature is computed over the encoded header and payload using the algorithm specified in the header. For HS256:

HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

The signature ensures the token hasn't been modified. If anyone changes even one character in the header or payload, the signature will no longer match and the token must be rejected.

How to Decode a JWT Token

Decoding a JWT means extracting the header and payload JSON from the Base64url encoding. Decoding is not the same as verification — anyone can decode a JWT without a secret key.

Method 1: DevToys JWT Decoder (fastest)

  1. Open JWT Decoder
  2. Paste your JWT token
  3. See the decoded header and payload instantly, with expiry status and claim descriptions

The token is decoded entirely in your browser — it is never sent to any server.

Method 2: JavaScript (Browser)

function decodeJwt(token) {
  const [headerB64, payloadB64] = token.split('.');

  // Base64url → Base64 → JSON
  function decodeSegment(b64url) {
    // Replace URL-safe chars and add padding
    const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/')
      + '='.repeat((4 - (b64url.length % 4)) % 4);

    const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
    return JSON.parse(new TextDecoder().decode(bytes));
  }

  return {
    header: decodeSegment(headerB64),
    payload: decodeSegment(payloadB64),
  };
}

// Usage
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const { header, payload } = decodeJwt(token);

console.log(header);   // { alg: "HS256", typ: "JWT" }
console.log(payload);  // { sub: "1234567890", name: "John Doe", iat: 1516239022 }

Method 3: Node.js

function decodeJwt(token) {
  const [headerB64, payloadB64] = token.split('.');

  function decodeSegment(b64url) {
    const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
    return JSON.parse(Buffer.from(b64, 'base64').toString('utf-8'));
  }

  return {
    header: decodeSegment(headerB64),
    payload: decodeSegment(payloadB64),
  };
}

const { header, payload } = decodeJwt(process.env.ACCESS_TOKEN);
console.log('User ID:', payload.sub);
console.log('Expires:', new Date(payload.exp * 1000).toISOString());

Method 4: Command Line

TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

# Decode header
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null | python3 -m json.tool

# Decode payload
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool

Checking Token Expiry

The exp claim is a Unix timestamp (seconds since epoch). To check if a token has expired:

function isTokenExpired(payload) {
  if (!payload.exp) return false; // No expiry = never expires

  const nowSeconds = Math.floor(Date.now() / 1000);
  return nowSeconds > payload.exp;
}

function getTimeUntilExpiry(payload) {
  if (!payload.exp) return null;

  const nowSeconds = Math.floor(Date.now() / 1000);
  const secondsRemaining = payload.exp - nowSeconds;

  if (secondsRemaining <= 0) return "Expired";

  const minutes = Math.floor(secondsRemaining / 60);
  const hours = Math.floor(minutes / 60);
  if (hours > 0) return `Expires in ${hours}h ${minutes % 60}m`;
  return `Expires in ${minutes}m ${secondsRemaining % 60}s`;
}

const { payload } = decodeJwt(token);
console.log('Expired?', isTokenExpired(payload));
console.log(getTimeUntilExpiry(payload)); // "Expires in 2h 14m"

JWT Decoding vs. JWT Verification

This is the most important distinction to understand:

OperationWhat it doesRequires secret/key?Safe for authorization?
DecodeReads the header and payload JSONNoNo — payload can be forged
VerifyChecks that the signature matchesYesYes — confirms token hasn't been tampered with

Never use a decoded-but-unverified JWT to grant access. Anyone can craft a JWT with any payload and sign it with their own secret. Verification proves the token was signed by the expected issuer.

How to Verify a JWT Signature

Use a trusted library — don't implement signature verification yourself.

Node.js (jsonwebtoken)

const jwt = require('jsonwebtoken');

// HS256: symmetric — same secret for sign and verify
const secret = process.env.JWT_SECRET;
try {
  const payload = jwt.verify(token, secret, { algorithms: ['HS256'] });
  console.log('Valid! User:', payload.sub);
} catch (err) {
  if (err.name === 'TokenExpiredError') {
    console.error('Token expired at', err.expiredAt);
  } else if (err.name === 'JsonWebTokenError') {
    console.error('Invalid token:', err.message);
  }
}

// RS256: asymmetric — verify with public key
const publicKey = fs.readFileSync('public.pem');
const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Browser (Web Crypto API)

async function verifyHs256Jwt(token, secret) {
  const encoder = new TextEncoder();
  const [headerB64, payloadB64, signatureB64] = token.split('.');

  // Import the secret key
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']
  );

  // Decode signature from Base64url
  const signatureBytes = Uint8Array.from(
    atob(signatureB64.replace(/-/g, '+').replace(/_/g, '/')),
    c => c.charCodeAt(0)
  );

  // Verify: the signed data is "header.payload"
  const data = encoder.encode(`${headerB64}.${payloadB64}`);
  return crypto.subtle.verify('HMAC', key, signatureBytes, data);
}

Security Pitfalls to Avoid

1. Accepting the "none" algorithm

Some early JWT libraries accepted tokens with "alg": "none", which have no signature at all. An attacker could forge any payload. Always allowlist the algorithms you accept:

// Good: explicitly require HS256
jwt.verify(token, secret, { algorithms: ['HS256'] });

// Bad: accepting any algorithm (including 'none')
jwt.verify(token, secret); // some libraries default to allowing 'none'

2. Using decode instead of verify for authorization

// WRONG: trusts the payload without verifying the signature
const { payload } = decodeJwt(token); // no signature check
if (payload.role === 'admin') grantAccess(); // attacker can forge this

// CORRECT: verifies signature first
const payload = jwt.verify(token, secret, { algorithms: ['HS256'] });
if (payload.role === 'admin') grantAccess();

3. Not checking exp

// Good: jsonwebtoken checks exp automatically
const payload = jwt.verify(token, secret); // throws if expired

// If decoding manually, always check exp yourself
if (isTokenExpired(payload)) throw new Error('Token expired');

4. Storing JWTs in localStorage

localStorage is accessible to any JavaScript on the page, making JWTs stored there vulnerable to XSS attacks. Prefer HttpOnly cookies for sensitive tokens, or keep short-lived tokens in memory.

Common JWT Debugging Scenarios

Scenario 1: "Token expired" errors in production

  1. Decode the token and check exp — convert from Unix timestamp: new Date(exp * 1000).toISOString()
  2. Check if clocks are synchronized between your auth server and API server (NTP drift)
  3. Add a small clock skew tolerance: clockTolerance: 30 (seconds) in jsonwebtoken

Scenario 2: Token looks valid but authorization fails

  1. Decode the payload and check aud — the audience must match your API's expected value
  2. Check iss — the issuer should be the expected identity provider
  3. Verify your server is using the correct public key / JWKS endpoint

Scenario 3: Decoding a token from an OAuth provider

OAuth access tokens from providers like Auth0 or Google may be opaque tokens (not JWTs) or JWTs signed with RS256. For RS256, verify using the provider's public JWKS endpoint instead of a shared secret:

// Example: verify Auth0 token using their JWKS
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');

const client = jwksClient({ jwksUri: 'https://YOUR_DOMAIN/.well-known/jwks.json' });

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    callback(err, key?.getPublicKey());
  });
}

jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, payload) => {
  if (err) console.error('Invalid:', err.message);
  else console.log('Valid! Sub:', payload.sub);
});

Quick Reference: JWT Claim Cheat Sheet

ClaimTypeWhat to check
expUnix timestamp (number)Must be in the future
nbfUnix timestamp (number)Must be in the past (or absent)
iatUnix timestamp (number)Should be recent; flag tokens issued far in the past
issString or URIMust match your expected issuer
audString or arrayMust include your service's identifier
subStringThe user/entity identifier — use this, not a custom "userId"
jtiStringUnique ID — useful for token revocation (store revoked JTIs)
algString (in header)Allowlist only the algorithms your system supports

Summary

A JWT is three Base64url-encoded segments: a header declaring the algorithm, a payload carrying claims, and a signature proving integrity. Decoding reads the header and payload — anyone can do it. Verification checks the signature and requires the correct key.

Key rules:

  • Always verify the signature server-side using a trusted library before trusting any claims
  • Always check exp, iss, and aud
  • Never accept "alg": "none"
  • Use decode-only for debugging and inspection, never for authorization decisions

Need to inspect a JWT quickly? JWT Decoder decodes any token in your browser instantly — header, payload, claims, and expiry status — without sending your token anywhere.