jwtauthenticationoauth

JWT and Modern Web Authentication

· Cosyslabs

JSON Web Tokens (JWT) are a compact, self-contained format for transmitting claims between parties as a signed JSON object. A JWT carries its own metadata — expiry time, issuer, subject — and can be verified without a database lookup. Understanding JWT structure, algorithms, and attack vectors is essential for implementing secure authentication in web applications.

JWT Structure

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

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfMTIzIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzE2MDAwMDAwLCJleHAiOjE3MTYwMDM2MDB9.signature

Header (algorithm and token type):

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

Payload (claims):

{
  "sub": "usr_123",           // subject (user ID)
  "name": "Alice Chen",
  "email": "[email protected]",
  "roles": ["admin"],
  "iat": 1716000000,          // issued at (Unix timestamp)
  "exp": 1716003600,          // expires at (1 hour later)
  "iss": "https://auth.example.com",  // issuer
  "aud": "https://api.example.com"    // audience
}

Signature: cryptographic proof that the header and payload were not tampered with.

Signing Algorithms

Choose the right algorithm based on your architecture:

AlgorithmTypeUse Case
HS256HMAC-SHA256 (symmetric)Monolith where one server signs and verifies
RS256RSA-SHA256 (asymmetric)Microservices — auth server signs, others verify with public key
ES256ECDSA-P256 (asymmetric)Like RS256 but shorter signatures, better performance
noneNo signatureNever use — allows any payload to pass verification
// Node.js — jsonwebtoken library
import jwt from "jsonwebtoken";

// HS256 — symmetric
const token = jwt.sign(
  { sub: "usr_123", roles: ["admin"] },
  process.env.JWT_SECRET,
  { algorithm: "HS256", expiresIn: "15m", issuer: "https://auth.example.com" }
);

// RS256 — asymmetric (sign with private key)
const token = jwt.sign(
  { sub: "usr_123" },
  fs.readFileSync("private.pem"),
  { algorithm: "RS256", expiresIn: "15m" }
);

// Verify (RS256 — use public key; attackers don't have it)
try {
  const payload = jwt.verify(token, fs.readFileSync("public.pem"), {
    algorithms: ["RS256"],     // NEVER allow ["RS256", "none"]
    issuer: "https://auth.example.com",
    audience: "https://api.example.com",
  });
  // payload.sub is the user ID
} catch (err) {
  if (err.name === "TokenExpiredError") {
    // Token expired — send 401 and ask client to refresh
  }
  // Other errors: invalid signature, wrong issuer, etc.
}

Verifying JWTs Without a Library

Understanding verification helps when debugging:

function decodeJwt(token) {
  const [header, payload, signature] = token.split(".");

  function base64UrlDecode(str) {
    const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
    const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
    return JSON.parse(atob(padded));
  }

  return {
    header: base64UrlDecode(header),
    payload: base64UrlDecode(payload),
    // Note: only decoding, not verifying the signature
  };
}

// NEVER trust a decoded payload without verifying the signature server-side

Refresh Token Pattern

Access tokens should be short-lived (15 minutes). Use refresh tokens for session continuity:

1. Login → server returns { accessToken (15min), refreshToken (7 days) }
2. Client stores refreshToken in httpOnly cookie (not localStorage)
3. Client includes accessToken in Authorization header for API calls
4. When accessToken expires (401), client calls POST /auth/refresh
5. Server validates refreshToken, issues new accessToken (and rotates refreshToken)
6. On logout: server invalidates the refreshToken in the database
// Express refresh endpoint
app.post("/auth/refresh", async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  if (!refreshToken) return res.status(401).json({ error: "No refresh token" });

  // Look up refresh token in database (allows revocation)
  const stored = await db.refreshToken.findUnique({ where: { token: refreshToken } });
  if (!stored || stored.revokedAt) return res.status(401).json({ error: "Invalid token" });
  if (stored.expiresAt < new Date()) return res.status(401).json({ error: "Expired" });

  // Rotate: delete old token, issue new one
  await db.refreshToken.delete({ where: { id: stored.id } });
  const newRefreshToken = crypto.randomBytes(32).toString("hex");
  await db.refreshToken.create({
    data: {
      token: newRefreshToken,
      userId: stored.userId,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  const accessToken = jwt.sign({ sub: stored.userId }, process.env.JWT_SECRET, {
    expiresIn: "15m",
  });

  res.cookie("refresh_token", newRefreshToken, { httpOnly: true, secure: true, sameSite: "Strict" });
  res.json({ accessToken });
});

JWT Security Vulnerabilities

Algorithm confusion attack: An attacker changes alg from RS256 to HS256 and signs the token using the public key as the HMAC secret. Fix: always specify allowed algorithms explicitly.

none algorithm: Setting alg: "none" removes the signature. Some libraries accepted this by default. Fix: reject tokens with alg: "none".

Sensitive data in payload: The payload is Base64-encoded, not encrypted. Anyone with the token can decode the payload. Never include passwords, credit card numbers, or secrets in JWT claims.

Missing expiry: A JWT with no exp claim is valid forever. Always set expiresIn.

Storing in localStorage: XSS can steal tokens from localStorage. Store refresh tokens in httpOnly cookies; keep short-lived access tokens in memory.

OAuth 2.0 and OpenID Connect

OAuth 2.0 is an authorization framework; OpenID Connect (OIDC) adds authentication on top:

Authorization Code Flow:
1. App redirects user to Auth Server: GET /authorize?response_type=code&client_id=...
2. User authenticates at Auth Server
3. Auth Server redirects back: GET /callback?code=AUTH_CODE
4. App exchanges code: POST /token { code, client_secret, ... }
5. Auth Server returns { access_token, refresh_token, id_token }
6. id_token is a JWT containing user identity claims

The id_token (OIDC) contains who the user is. The access_token (OAuth 2.0) grants access to resources — it may or may not be a JWT depending on the provider.

Tools

  • JWT Decoder — decode and inspect JWT headers and payloads
  • JWT Verifier — verify JWT signatures with a public key or secret
  • HMAC Generator — generate HMAC-SHA256 signatures for HS256 tokens