What Is a JWT Token and How Do You Decode It?

JWT explained: what the three parts mean, how to decode one without a library, what each claim does, and the security gotchas developers miss.

BY ALI HASSAN·

What Is a JWT Token and How Do You Decode It?

A JWT (JSON Web Token) is a compact, URL-safe token format used to transmit claims between systems. You'll find them in Authorization headers, OAuth flows, and API authentication. Understanding what's inside a JWT — and how to decode one quickly — saves you hours of debugging auth issues.

Use Mizakii's JWT Decoder to paste any JWT and see the decoded header, payload, and expiry at a glance. No libraries, no code.

The Three-Part Structure

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaSBIYXNzYW4iLCJpYXQiOjE3MDAwMDAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three Base64url-encoded segments, separated by dots:

HEADER.PAYLOAD.SIGNATURE

Part 1: Header

Decoded, the header is a JSON object describing the token type and signing algorithm:

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

alg values you'll commonly see:

  • HS256 — HMAC-SHA256 (symmetric — same key signs and verifies)
  • RS256 — RSA-SHA256 (asymmetric — private key signs, public key verifies)
  • ES256 — ECDSA with P-256 (asymmetric, smaller keys than RSA)

Part 2: Payload

The payload contains the claims — statements about the user or session:

{
  "sub": "1234567890",
  "name": "Ali Hassan",
  "email": "ali@example.com",
  "iat": 1700000000,
  "exp": 1700086400,
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

Part 3: Signature

The signature is computed by the server:

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

The signature proves the token wasn't tampered with. You cannot verify it without the secret (for HS256) or the public key (for RS256/ES256).


Standard JWT Claims

| Claim | Name | Type | Meaning | |-------|------|------|---------| | sub | Subject | String | User ID or entity the token represents | | iss | Issuer | String | Who issued the token (auth server URL) | | aud | Audience | String/Array | Who the token is intended for | | exp | Expiration | Unix timestamp | Token expires at this time | | iat | Issued At | Unix timestamp | When the token was created | | nbf | Not Before | Unix timestamp | Token is not valid before this time | | jti | JWT ID | String | Unique identifier for this token (for revocation) |

exp and iat are Unix timestamps (seconds since 1970-01-01 UTC). A JWT Decoder converts them to readable dates automatically.


How to Decode a JWT Without a Library

The header and payload are just Base64url encoded — you can decode them manually in any environment.

Browser console

// Paste your JWT here
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

const [header, payload] = token.split('.');

// Base64url to Base64: replace - with + and _ with /
const decode = (str) => JSON.parse(atob(str.replace(/-/g, '+').replace(/_/g, '/')));

console.log(decode(header));   // {alg: "HS256", typ: "JWT"}
console.log(decode(payload));  // {sub: "1234567890"}

Command line

# Decode the payload (second segment)
echo "eyJzdWIiOiIxMjM0NTY3ODkwIn0" | base64 -d

Note: Base64url uses - and _ where standard Base64 uses + and /. Add padding = if your tool requires it.

Or just use Mizakii's JWT Decoder

Paste the token, see the decoded header and payload formatted as JSON, with the expiry date in human-readable form. No JavaScript required.


JWTs Are Not Encrypted By Default

This is the most important security note: the payload is encoded, not encrypted. Anyone who has the token can read the payload — they just can't fake the signature without the secret key.

Never put sensitive data in a JWT payload. Password hashes, credit card numbers, PII — none of it. The payload is visible to anyone who gets hold of the token.

If you need an encrypted JWT, the spec is called JWE (JSON Web Encryption) and is a separate implementation.


JWT vs Session Tokens

| | JWT | Server-side session | |---|-----|---------------------| | Storage | Client (localStorage, cookie) | Server (DB, Redis) | | Stateless | ✅ Yes | ❌ No | | Revocation | Hard (requires token blocklist) | Easy (delete session) | | Payload visible to client | ✅ Yes | ❌ No | | Scalable across servers | ✅ Easy | Requires shared session store | | Best for | Stateless APIs, microservices | Traditional web apps |

JWTs are well-suited for APIs and distributed systems. For traditional server-rendered apps where revocation matters (banking, admin panels), session tokens are often safer.


Common JWT Mistakes

Trusting alg: none — some old libraries accepted a none algorithm, meaning no signature. A JWT with "alg": "none" in the header is unsigned and unverified. Always validate the algorithm on the server side.

Not checking exp — decode is not the same as validate. Your backend must reject tokens where exp is in the past.

Storing JWTs in localStorage — accessible to JavaScript, so vulnerable to XSS. HttpOnly cookies are safer for web apps.

Long expiry on access tokens — access tokens should expire in minutes to hours. Use refresh tokens for longer sessions.


JWT Libraries by Language

You almost never need to implement JWT parsing yourself. Use an established library:

| Language | Library | Notes | |----------|---------|-------| | Node.js | jsonwebtoken | Most widely used, sign and verify | | Node.js | jose | Modern, supports JWE and JWK | | Python | PyJWT | Simple API, well-maintained | | Python | python-jose | Broader JWE/JWK support | | PHP | firebase/php-jwt | Official Firebase port | | Java | jjwt | Full JJWT spec support | | Go | golang-jwt/jwt | Actively maintained fork | | Ruby | ruby-jwt | Covers HS and RS algorithms | | .NET | System.IdentityModel.Tokens.Jwt | Built into ASP.NET Core |

Minimal Node.js example

const jwt = require('jsonwebtoken');

// Sign a token
const token = jwt.sign(
  { sub: 'user_123', name: 'Ali Hassan' },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);

// Verify and decode
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  console.log(decoded.sub); // 'user_123'
} catch (err) {
  // TokenExpiredError, JsonWebTokenError, etc.
  console.error('Invalid token:', err.message);
}

Always wrap verify() in a try/catch — it throws on any validation failure (expired, wrong secret, tampered).


Refresh Tokens and Access Tokens

JWTs used for authentication typically come in pairs:

Access token — short-lived (5–60 minutes). Sent in the Authorization: Bearer header on every API request. If stolen, it expires quickly.

Refresh token — long-lived (days or weeks). Stored securely (HttpOnly cookie). Used only to request a new access token when the current one expires. Should be rotated on use — each refresh issues a new refresh token and invalidates the old one.

The flow:

  1. User logs in → server issues access token + refresh token
  2. Client uses access token for API calls
  3. Access token expires → client uses refresh token to get a new access token
  4. Refresh token expires → user must log in again

This limits the blast radius of a stolen token: the access token expires in minutes, and the refresh token is never exposed in API requests.


Decode Any JWT Instantly

Open Mizakii JWT Decoder →

Paste a token and see the header, payload, and expiry time decoded immediately. Free, no signup, runs entirely in your browser.