HomeArticlesHow ToDecode a JWT Token
HOW TO

How to Decode a JWT Token

Learn how to decode and validate a JWT token — understanding the header, payload, and signature, verifying claims, and using a free JWT decoder online.

Updated 2026-06-26

Free calculators used in this guide

JWT Validator

JWT — JSON Web Token is the most widely used format for stateless authentication and authorisation on the web. Understanding how to decode and validate one correctly is foundational to building secure APIs and debugging authentication issues. Decoding is trivial; verification is where security lives.

Overview

A JWT is a compact, URL-safe token that represents a set of claims as a JSON object. It is defined in RFC 7519. JWTs are used in OAuth 2.0 flows, OpenID Connect, and direct API authentication — anywhere a server needs to issue a verifiable credential that a client can present on future requests.

The key distinction this guide will repeat: decoding is not verifying. Anyone can decode a JWT. Only the party with the correct secret or public key can verify that the token is authentic and unmodified. Use the JWT Validator tool to decode and verify JWT tokens online.

What You Need

  • A JWT token string (three dot-separated Base64URL segments)
  • For verification: the signing secret (HS256) or the issuer's public key / JWKS endpoint (RS256/ES256)
  • Optionally: a JWT debugger or the JWT Validator tool for instant decoding

Step 1: Understand JWT Structure

A JWT is always three Base64URL-encoded strings joined by dots:

header.payload.signature

Example token (abbreviated):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Each of the three segments can be independently decoded with Base64URL decoding. The first two are JSON objects. The third is the raw cryptographic signature bytes.

Base64URL vs Base64: JWT uses Base64URL, which replaces + with - and / with _, and drops trailing = padding. This makes the token safe to embed directly in URLs, Authorization headers, and cookies without percent-encoding.

Step 2: Decode the Header

Base64URL-decode the first segment (before the first dot) to get the header JSON:

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

The header contains:

  • alg — the signing algorithm. Common values:
    • HS256 — HMAC-SHA256 with a shared secret
    • HS384, HS512 — HMAC with SHA-384 and SHA-512
    • RS256 — RSA signature with SHA-256 (asymmetric)
    • ES256 — ECDSA with P-256 curve and SHA-256 (asymmetric)
    • nonedangerous; reject any token with this value
  • typ — token type; always JWT for standard tokens
  • kid — key ID (optional); used to select the correct key from a JWKS when multiple keys are in rotation

The algorithm in the header tells you what you need to verify the signature in Step 4. If the alg field is none, reject the token immediately — this is a known attack vector.

Step 3: Decode the Payload

Base64URL-decode the second segment (between the two dots) to get the payload JSON:

{
  "iss": "https://auth.example.com",
  "sub": "user_abc123",
  "aud": "api.example.com",
  "exp": 1751000000,
  "nbf": 1750996400,
  "iat": 1750996400,
  "jti": "a5f2c3d1-8b4e-4c9f-a2b1-3e7d5f9c1a0b",
  "email": "user@example.com",
  "role": "editor"
}

Standard claims (defined in RFC 7519):

Claim Name Format Purpose
iss Issuer String (URL) Who issued this token
sub Subject String Who this token is about (user ID)
aud Audience String or array Intended recipient(s) of this token
exp Expiration Unix timestamp Token is invalid after this time
nbf Not Before Unix timestamp Token is invalid before this time
iat Issued At Unix timestamp When the token was created
jti JWT ID String Unique ID; used to prevent replay attacks

Any additional fields beyond these (like email, role, permissions) are custom claims defined by the application.

Reading exp: Convert the Unix timestamp to a human-readable date. exp: 1751000000 corresponds to a specific UTC datetime. If the current time is past exp, the token is expired and must be rejected — even if the signature is valid.

Step 4: Verify the Signature

This is the most critical step. Decoding the header and payload is trivial — anyone can do it. The signature is what proves the token has not been tampered with.

For HS256 (symmetric):

The signature is computed as:

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

To verify: recompute the same HMAC using the shared secret and compare it to the signature in the token. If they match, the token is authentic. The shared secret must be kept confidential — anyone with the secret can both verify and forge tokens.

For RS256 (asymmetric):

The issuer signs with their RSA private key:

RSA_SHA256_sign(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)

Verification uses the corresponding public key:

RSA_SHA256_verify(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  publicKey,
  signature
)

The public key is typically published at the issuer's JWKS endpoint (/.well-known/jwks.json). This allows any service to verify tokens without having access to the private key — and without the ability to forge new ones.

Never skip signature verification. A JWT that has been decoded but not verified is an untrustworthy document — payload claims can be anything an attacker chooses to put there.

Step 5: Check Expiry and Other Claims

After verifying the signature, validate the claims:

1. Check exp (expiration):

import time
if payload["exp"] < time.time():
    raise Exception("Token is expired")

An expired token must be rejected even if the signature is valid. Clock skew between services is common — a tolerance of ±30 seconds is acceptable; more than 60 seconds is not.

2. Check nbf (not before):

If present, the token must not be accepted before the nbf timestamp. A token with nbf set 5 minutes in the future should be rejected now.

3. Check iss (issuer):

Verify the issuer matches the expected token source. If your auth server is https://auth.example.com, reject any token where iss is different — this prevents tokens issued by other systems from being accepted by your API.

4. Check aud (audience):

If present, the aud claim must match your service's identifier. A token issued for api.example.com should not be accepted by dashboard.example.com — this prevents token misuse across services in the same organisation.

5. Check jti for replay prevention (if needed):

For high-security flows, maintain a short-lived cache of used jti values. Reject any token whose jti has already been seen — this prevents a stolen token from being replayed within its validity window.

Common Mistakes to Avoid

Confusing decoding with verification: Decoding shows you the payload contents. Verification proves those contents are authentic. Libraries like jsonwebtoken (Node.js) require you to call jwt.verify(token, secret), not jwt.decode(token) — the decode function skips signature verification entirely. Use the verify function in production.

Storing JWT in localStorage: JavaScript running on your page (including third-party scripts) can read localStorage, making stored tokens vulnerable to XSS. Store access tokens in memory and use httpOnly, Secure, SameSite cookies for tokens that need to persist across page reloads.

Not checking exp: Some JWT implementations decode the token and use the claims without explicitly checking expiry. This means a token stolen 2 hours ago still works. Always check exp as the first claim validation after signature verification.

Accepting alg:none attacks: The alg field in the header is attacker-controlled before verification. A vulnerable library that trusts alg:none accepts any unsigned token. Use a strict algorithm allowlist and reject any token whose alg is not on it.

Putting sensitive data in the payload: The payload is encoded, not encrypted. atob(token.split('.')[1]) in a browser console reveals the payload in plain text. Never put passwords, credit card numbers, SSNs, or anything sensitive in JWT claims. Use opaque tokens or JWE (JSON Web Encryption) for sensitive payloads.

Formula & Methodology

JWT encoding uses Base64URL (RFC 4648 §5), which maps 6-bit groups to URL-safe characters. The formula for each segment:

Base64URL(JSON.stringify(header))
Base64URL(JSON.stringify(payload))
CryptoSign(segment1 + "." + segment2, key, alg)

The complete token is:

Base64URL(header) + "." + Base64URL(payload) + "." + Base64URL(signature)

For HS256, the signing input is exactly base64url_header + "." + base64url_payload — a precise ASCII string, no whitespace, no pretty-printing. Changing even one character in the payload (adding a space, changing a letter case) produces a completely different HMAC, making the token fail verification.

The security model of JWT relies on the impossibility of forging the signature without the key. For HS256 with a sufficiently long (≥ 256-bit) random secret, brute-forcing is computationally infeasible. For RS256, security depends on the RSA key length — use minimum 2048-bit keys, preferably 4096-bit for long-lived systems.

Use the JWT Validator to decode any JWT token instantly and inspect all three segments with formatted JSON output.

Frequently Asked Questions

A JWT (JSON Web Token) consists of three Base64URL-encoded strings separated by dots: the header, the payload, and the signature. The header specifies the token type (JWT) and the signing algorithm used (such as HS256, RS256, or ES256). The payload contains claims — statements about the user or session. The signature is a cryptographic value that verifies the token has not been tampered with. Together they form the pattern: xxxxx.yyyyy.zzzzz.
Decoding a JWT means reading the contents of the header and payload by reversing the Base64URL encoding — anyone can do this without any key or secret. Verifying a JWT means checking that the signature is valid, which requires the signing secret (for HS256) or the public key (for RS256). A decoded JWT tells you what claims are present; a verified JWT proves those claims have not been tampered with since the token was issued. Never trust claims in a decoded but unverified JWT.
Base64URL is a variant of standard Base64 that is safe for use in URLs and HTTP headers. It replaces the + character with - and the / character with _, and omits the trailing = padding characters. This makes the encoded string safe to use in URL query parameters without percent-encoding. JWT uses Base64URL specifically because tokens are frequently passed in Authorization headers, URL fragments, and query strings — contexts where standard Base64 characters would cause parsing problems.
The JWT specification (RFC 7519) defines seven standard claims: iss (issuer — who created the token), sub (subject — who the token is about), aud (audience — who the token is intended for), exp (expiration time as Unix timestamp), nbf (not before — token is invalid before this time), iat (issued at time), and jti (JWT ID — unique identifier to prevent replay). Of these, exp is the most critical to check on every request — an expired token must be rejected regardless of whether the signature is valid.
The alg:none vulnerability is a critical security flaw that affects JWT libraries that allow the algorithm field in the header to be set to 'none', meaning no signature is required. An attacker can take any JWT, modify the payload (changing user ID, role, or permissions), re-encode it, set alg to 'none', and omit the signature — resulting in a token that a vulnerable library will accept as valid. This vulnerability has been exploited in real-world attacks. Any JWT library you use must explicitly reject tokens with alg:none or alg not in a strict allowlist.
Storing JWT in localStorage is vulnerable to XSS (Cross-Site Scripting) attacks — any injected JavaScript on your page can read localStorage and steal the token. The recommended approach is to store the JWT in an httpOnly cookie, which is inaccessible to JavaScript and automatically sent with every request. HttpOnly cookies are still vulnerable to CSRF attacks, so they should be paired with SameSite=Strict or SameSite=Lax and a CSRF token for state-changing requests. For SPAs that cannot use httpOnly cookies, keep access tokens in memory (not localStorage) and use refresh tokens in httpOnly cookies.
The exp (expiration) claim is a Unix timestamp (seconds since 1 January 1970 UTC) indicating when the token becomes invalid. For example, exp: 1751000000 means the token expires at a specific moment in 2025. When a request arrives with an expired JWT, the server must reject it with a 401 Unauthorized response — an expired signature is still cryptographically valid, but the token must not be accepted. Access tokens commonly expire in 15 minutes to 1 hour; refresh tokens last hours to days. Always check exp before trusting any other claim.
JWT supports symmetric algorithms (HS256, HS384, HS512, which use a shared secret) and asymmetric algorithms (RS256, RS384, RS512 using RSA keys; ES256, ES384, ES512 using elliptic curve keys). HS256 is simplest but requires all parties to share the secret — if the consumer of the JWT is also the issuer, HS256 is fine. If the JWT is consumed by a different service than the one that issued it (common in microservices and third-party OAuth flows), use RS256 or ES256 so the consumer only needs the public key and cannot forge tokens. ES256 is preferred over RS256 for performance and key size.
No. The JWT payload is only Base64URL-encoded, not encrypted — anyone who possesses the token can decode and read the payload without needing any key or secret. Never put passwords, credit card numbers, PII beyond what is strictly necessary, or any data that would be harmful if exposed in the payload. If you need to transmit sensitive claims, use a JWE (JSON Web Encryption) token instead, which encrypts the payload. Standard JWTs are signed for integrity, not encrypted for confidentiality.
For RS256 tokens, the signature is created using the issuer's RSA private key. Verification requires the corresponding RSA public key, which issuers typically expose at a well-known JWKS (JSON Web Key Set) endpoint — for example, https://your-auth-server/.well-known/jwks.json. Your application fetches the public key, then uses it to verify: signature = RSA_verify(base64url(header) + '.' + base64url(payload), publicKey, SHA256). A matching signature proves the token was issued by the holder of the private key and has not been modified since.
An access token is a short-lived JWT (typically 15 minutes to 1 hour) that authorises specific API requests. A refresh token is a long-lived credential (hours to days) used solely to obtain a new access token when the current one expires. Refresh tokens are typically opaque strings stored server-side (not JWTs), so they can be revoked. The pattern works as follows: the client uses the access token for API calls; when the access token expires, the client presents the refresh token to the auth server to receive a new access token without requiring the user to log in again.
JWT tokens have no hard size limit defined in the specification, but practical limits apply. HTTP headers, where JWTs are most commonly passed in the Authorization header, are limited to 8KB by most web servers (Nginx, Apache). A typical JWT with 10–15 claims is around 500–800 bytes after Base64URL encoding. Adding large claims (long lists, nested objects, verbose strings) can push tokens past header limits and cause 400 or 431 errors. Keep JWT payloads lean — store only the claims your services actually need for authorisation, and fetch additional data from the database when required.

Related Articles

GUIDE

API Security Guide — Keys, Auth & Best Practices

HOW TO

How to Generate a Strong Password

BEST OF

Best Password Generators Online 2026

COMPARISON

MD5 vs SHA-256 vs bcrypt — Hashing Algorithm Comparison

GUIDE

Security & Identity Validators: Passwords, Barcodes & Crypto Addresses