JWT Core Knowledge

SkillSecurity

JSON Web Tokens for authentication. Covers token structure, signing, and validation. Use for stateless authentication.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the JWT Core Knowledge skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/authentication/jwt/SKILL.md and read by ahel’s review.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: jwt for comprehensive documentation.

Token Structure

header.payload.signature

Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "1234", "name": "John", "iat": 1516239022 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)

Node.js Implementation

import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;

// Generate token
function generateToken(user: User): string {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: '1h' }
  );
}

// Verify token
function verifyToken(token: string): JwtPayload {
  return jwt.verify(token, SECRET) as JwtPayload;
}

// Refresh token pattern
function generateRefreshToken(user: User): string {
  return jwt.sign(
    { sub: user.id, type: 'refresh' },
    SECRET,
    { expiresIn: '7d' }
  );
}

Middleware

const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const token = authHeader.split(' ')[1];
  try {
    req.user = verifyToken(token);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

When NOT to Use This Skill

  • Session-based authentication - Use traditional server-side sessions with cookies
  • OAuth 2.0 flows - Use oauth2 skill for third-party authentication
  • NextAuth.js - Use nextauth skill for Next.js authentication
  • Simple internal APIs - API keys might be sufficient

Best Practices

DoDon't
Use HTTPSStore in localStorage (use httpOnly cookies)
Short expiry (15m-1h)Put sensitive data in payload
Validate all claimsUse weak secrets
Use refresh tokensIgnore expiration

Anti-Patterns

Anti-PatternWhy It's BadCorrect Approach
Storing JWT in localStorageVulnerable to XSS attacksUse httpOnly cookies
Long-lived access tokensSecurity risk if compromised15-minute expiry + refresh tokens
Weak secrets (< 32 bytes)Easy to brute forceUse 256-bit random secret
Ignoring algorithm verificationAlgorithm confusion attacksExplicitly specify allowed algorithms
Putting passwords in payloadToken is base64, not encryptedOnly non-sensitive claims
No token revocationCan't logout usersImplement blacklist or token versioning

Quick Troubleshooting

IssueCauseSolution
"Invalid signature"Wrong secret or algorithmVerify JWT_SECRET matches, check algorithm
"Token expired"exp claim in pastImplement refresh token flow
"Missing token"Authorization header not sentCheck Authorization: Bearer <token>
Token not recognizedMalformed tokenVerify header.payload.signature format
CORS errors with cookiesSameSite/Secure flagsSet sameSite:'strict', secure:true
Logout doesn't workTokens are statelessImplement revocation with Redis/DB

Standard Claims

ClaimPurpose
subSubject (user ID)
iatIssued at
expExpiration
issIssuer
audAudience

Production Readiness

Security Configuration

// Use asymmetric keys (RS256) for production
import * as jose from 'jose';

// Generate key pair (run once, store securely)
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem

const privateKey = await jose.importPKCS8(
  process.env.JWT_PRIVATE_KEY!,
  'RS256'
);
const publicKey = await jose.importSPKI(
  process.env.JWT_PUBLIC_KEY!,
  'RS256'
);

// Sign token
async function generateToken(user: User): Promise<string> {
  return new jose.SignJWT({
    sub: user.id,
    email: user.email,
  })
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setIssuedAt()
    .setIssuer(process.env.JWT_ISSUER!)
    .setAudience(process.env.JWT_AUDIENCE!)
    .setExpirationTime('15m')  // Short-lived access token
    .sign(privateKey);
}

// Verify token
async function verifyToken(token: string): Promise<jose.JWTPayload> {
  const { payload } = await jose.jwtVerify(token, publicKey, {
    issuer: process.env.JWT_ISSUER!,
    audience: process.env.JWT_AUDIENCE!,
  });
  return payload;
}

Secure Token Storage

// Server-side: HttpOnly cookie for access token
res.cookie('access_token', token, {
  httpOnly: true,     // Prevents XSS access
  secure: true,       // HTTPS only
  sameSite: 'strict', // CSRF protection
  maxAge: 15 * 60 * 1000, // 15 minutes
  path: '/',
});

// Refresh token in separate cookie
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
  path: '/api/auth/refresh', // Only sent to refresh endpoint
});

Token Rotation & Revocation

// Refresh token rotation
async function refreshTokens(refreshToken: string) {
  // Verify refresh token
  const payload = await verifyRefreshToken(refreshToken);

  // Check if refresh token is in blacklist (revoked)
  if (await isTokenRevoked(refreshToken)) {
    throw new Error('Token revoked');
  }

  // Revoke old refresh token
  await revokeToken(refreshToken);

  // Generate new tokens
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  return {
    accessToken: await generateToken(user),
    refreshToken: await generateRefreshToken(user),
  };
}

// Token revocation with Redis
async function revokeToken(token: string): Promise<void> {
  const payload = await jose.decodeJwt(token);
  const ttl = payload.exp! - Math.floor(Date.now() / 1000);
  if (ttl > 0) {
    await redis.set(`revoked:${token}`, '1', 'EX', ttl);
  }
}

// Logout: revoke all user tokens
async function logoutAll(userId: string): Promise<void> {
  // Increment user's token version, invalidating all existing tokens
  await db.users.update({
    where: { id: userId },
    data: { tokenVersion: { increment: 1 } },
  });
}

Algorithm Security

// NEVER allow 'none' algorithm
// ALWAYS specify allowed algorithms explicitly
const { payload } = await jose.jwtVerify(token, publicKey, {
  algorithms: ['RS256'], // Only allow RS256
  issuer: process.env.JWT_ISSUER!,
  audience: process.env.JWT_AUDIENCE!,
});

// Validate token type to prevent token confusion
if (payload.type !== 'access') {
  throw new Error('Invalid token type');
}

Monitoring Metrics

MetricAlert Threshold
Token verification failures> 100/min
Refresh token reuse attempts> 10/min
Expired token requests> 500/min
Invalid signature errors> 50/min

Claims Validation

async function validateTokenClaims(payload: jose.JWTPayload): Promise<void> {
  // Check required claims
  if (!payload.sub || !payload.iat || !payload.exp) {
    throw new Error('Missing required claims');
  }

  // Check user still exists and is active
  const user = await db.users.findUnique({ where: { id: payload.sub } });
  if (!user || !user.isActive) {
    throw new Error('User not found or inactive');
  }

  // Check token version (for logout-all functionality)
  if (payload.tokenVersion !== user.tokenVersion) {
    throw new Error('Token invalidated');
  }
}

Checklist

  • Use RS256 (asymmetric) in production
  • Short access token expiry (15 minutes)
  • Refresh tokens with rotation
  • HttpOnly cookies (not localStorage)
  • Secure + SameSite cookie flags
  • Token revocation mechanism
  • Validate issuer and audience
  • Specify allowed algorithms explicitly
  • Include token version for logout-all
  • Monitor verification failures
  • Rate limit token endpoints

Reference Documentation

Signals

GitHub stars
33
Forks
8
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
jwt
Source
github.com/claude-dev-suite/claude-dev-suite