Blog

Tips, tutorials, and insights about online tools

JWT Authentication Explained: Tokens, Claims & Security
2026-08-20Keynou Team

JWT Authentication Explained: Tokens, Claims & Security

JWT authentication is the mechanism most modern web and mobile applications use to verify identity across stateless API requests. Instead of storing session data on the server, the server issues a signed JSON Web Token (JWT) after login. The client sends this token with each subsequent request, and the server verifies the signature to confirm the user's identity. This approach scales horizontally without shared session state and works across domains, microservices, and mobile platforms. This guide covers how JWT authentication works, claim types, token expiration and refresh, and the security practices that separate a correct implementation from a vulnerable one.

How JWT Authentication Works

The JWT authentication flow has four phases: login, token issuance, token storage, and token verification.

<svg viewBox="0 0 800 360" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:800px;margin:24px auto;display:block">
  <rect x="0" y="0" width="800" height="360" fill="#f8fafc" rx="12"/>
  <text x="400" y="35" fill="#1e293b" font-size="17" font-family="sans-serif" text-anchor="middle" font-weight="bold">JWT Authentication Flow</text>
  <rect x="40" y="60" width="140" height="50" fill="#3b82f6" rx="8"/>
  <text x="110" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">1. Login</text>
  <rect x="40" y="120" width="140" height="30" fill="#dbeafe" rx="4"/>
  <text x="110" y="140" fill="#1e40af" font-size="10" font-family="sans-serif" text-anchor="middle">POST /login</text>
  <line x1="190" y1="85" x2="230" y2="85" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
  <rect x="240" y="60" width="140" height="50" fill="#8b5cf6" rx="8"/>
  <text x="310" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">2. Issue Token</text>
  <rect x="240" y="120" width="140" height="30" fill="#ede9fe" rx="4"/>
  <text x="310" y="140" fill="#5b21b6" font-size="10" font-family="sans-serif" text-anchor="middle">Sign &amp; return JWT</text>
  <line x1="390" y1="85" x2="430" y2="85" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
  <rect x="440" y="60" width="140" height="50" fill="#10b981" rx="8"/>
  <text x="510" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">3. Store Token</text>
  <rect x="440" y="120" width="140" height="30" fill="#d1fae5" rx="4"/>
  <text x="510" y="140" fill="#065f46" font-size="10" font-family="sans-serif" text-anchor="middle">Cookie / localStorage</text>
  <line x1="590" y1="85" x2="630" y2="85" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
  <rect x="640" y="60" width="120" height="50" fill="#f59e0b" rx="8"/>
  <text x="700" y="90" fill="white" font-size="12" font-family="sans-serif" text-anchor="middle" font-weight="bold">4. Send Token</text>
  <rect x="640" y="120" width="120" height="30" fill="#fef3c7" rx="4"/>
  <text x="700" y="140" fill="#92400e" font-size="10" font-family="sans-serif" text-anchor="middle">Authorization: Bearer</text>
  <rect x="200" y="180" width="400" height="60" fill="#1e293b" rx="8"/>
  <text x="400" y="205" fill="white" font-size="13" font-family="sans-serif" text-anchor="middle" font-weight="bold">Server: Verify Signature</text>
  <text x="400" y="225" fill="#94a3b8" font-size="11" font-family="sans-serif" text-anchor="middle">Check exp, iss, aud → Allow or Reject</text>
  <line x1="700" y1="155" x2="700" y2="175" stroke="#94a3b8" stroke-width="2"/>
  <line x1="700" y1="175" x2="400" y2="175" stroke="#94a3b8" stroke-width="2"/>
  <line x1="400" y1="175" x2="400" y2="180" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
  <rect x="200" y="270" width="180" height="50" fill="#10b981" rx="8"/>
  <text x="290" y="300" fill="white" font-size="13" font-family="sans-serif" text-anchor="middle" font-weight="bold">Valid → 200 OK</text>
  <rect x="420" y="270" width="180" height="50" fill="#ef4444" rx="8"/>
  <text x="510" y="300" fill="white" font-size="13" font-family="sans-serif" text-anchor="middle" font-weight="bold">Invalid → 401</text>
  <line x1="350" y1="240" x2="290" y2="270" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
  <line x1="450" y1="240" x2="510" y2="270" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr5)"/>
  <defs>
    <marker id="arr5" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth">
      <path d="M0,0 L0,6 L9,3 z" fill="#94a3b8"/>
    </marker>
  </defs>
</svg>

1. Login

The client sends credentials (username/password) to a login endpoint:

POST /api/login
Content-Type: application/json

{
  "username": "alice",
  "password": "secret123"
}

2. Token Issuance

The server verifies the credentials, creates a JWT containing the user's identity and claims, signs it with a secret or private key, and returns it:

HTTP/1.1 200 OK

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFsaWNlIiwiZXhwIjoxNzMyNzU4NDAwLCJpYXQiOjE3MzI3NTQ4MDB9.sN3aZqKJ8wQyVbR5xT9mF2pL6hY0jN4kD7sW1cE3bA"
}

You can inspect this token with the JWT decoder to see the header, payload, and claims.

3. Token Storage

The client stores the token. The storage location matters for security (covered below). Common options:

  • HttpOnly cookies — not accessible via JavaScript, protected from XSS
  • localStorage — accessible via JavaScript, vulnerable to XSS
  • Memory (in-memory variable) — cleared on page reload, most secure but least convenient

4. Token Verification

On each subsequent request, the client sends the token in the Authorization header:

GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

The server verifies the signature, checks the exp claim, and processes the request. No database lookup needed — the token itself carries the identity.

JWT Claim Types

Claims are statements about the token and the user. They fall into three categories:

Registered Claims

Standard claims defined in RFC 7519:

Claim Purpose
iss Issuer — who created the token
sub Subject — who the token is about (user ID)
aud Audience — intended recipient
exp Expiration — when the token expires
nbf Not Before — when the token becomes valid
iat Issued At — when the token was created
jti JWT ID — unique token identifier

Private Claims

Custom claims defined by your application — role, permissions, email, department. These are agreed upon between the issuer and consumer:

{
  "sub": "1234",
  "role": "admin",
  "permissions": ["read", "write", "delete"],
  "department": "engineering"
}

Public Claims

Claims registered in the IANA JSON Web Token Claims Registry to avoid collisions between applications. Use these when your tokens are consumed by external parties.

Token Expiration and Refresh Tokens

Access Tokens

Access tokens are short-lived JWTs used to authenticate API requests. They should expire quickly — 5 to 15 minutes is common. Short expiration limits the damage if a token is stolen: the attacker has a narrow window before it becomes invalid.

The exp claim sets the expiration as a Unix timestamp:

{
  "iat": 1732754800,
  "exp": 1732755700
}

This token is valid for 900 seconds (15 minutes). You can decode and check exp with the JWT decoder.

Refresh Tokens

Since access tokens expire quickly, users would need to log in constantly without refresh tokens. A refresh token is a longer-lived credential (days or weeks) stored securely. When the access token expires, the client sends the refresh token to get a new access token without requiring the user to re-enter credentials:

POST /api/refresh
Authorization: Bearer <refresh_token>

→ Response: { "token": "<new_access_token>" }

Refresh tokens are typically stored in HttpOnly cookies and revoked server-side when the user logs out. Unlike access tokens, refresh tokens require server-side state because they can be revoked.

Security Best Practices

Use HTTPS Always

JWTs sent over HTTP can be intercepted. Always use HTTPS in production. The token in the Authorization header is bearer — anyone who captures it can use it until it expires. Learn how to verify your site's TLS setup with our SSL checker guide.

Keep Access Tokens Short-Lived

Set exp to 5-15 minutes. The shorter the token, the less damage a stolen token can do. Use refresh tokens for session continuity.

Store Tokens Securely

Storage XSS Risk CSRF Risk Recommendation
HttpOnly cookie Low Medium Best for web apps
localStorage High Low Acceptable for SPAs with CSP
Memory None None Most secure, lost on reload

Don't Put Sensitive Data in the Payload

The JWT payload is Base64-encoded, not encrypted. Anyone with the token can read it. Never include passwords, API keys, or personal data in claims. Use the Base64 converter to see how easily payload data is readable.

Verify the Algorithm

Ensure your verification library enforces the expected algorithm. Some older libraries accepted alg: none in the header, allowing attackers to forge tokens. Modern libraries reject this, but always verify explicitly:

// Node.js example with jsonwebtoken
jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => {
  if (err) {
    // Invalid token
    return;
  }
  // Token is valid — use decoded claims
});

Use Strong Signing Keys

For HMAC (HS256), use a secret of at least 256 bits (32 bytes). For RSA (RS256), use at least 2048-bit keys. Weak keys can be brute-forced. Generate a strong secret using a hash generator or a cryptographic random generator.

JWT Authentication in Code

Here's a minimal Node.js implementation:

const jwt = require('jsonwebtoken');

const SECRET = process.env.JWT_SECRET;

// Issue a token on login
function login(user) {
  const token = jwt.sign(
    { sub: user.id, role: user.role },
    SECRET,
    { expiresIn: '15m', issuer: 'myapp' }
  );
  return token;
}

// Verify token on each request
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }
  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

Published: August 20, 2026
Category: Data Tools
Reading Time: 8 minutes

Verified DR - Verified Domain Rating for keynou.com
FlowDrive