skills/security/common-security/jwt-security/SKILL.md
JSON Web Token security best practices. Use this skill when implementing JWT authentication, validating tokens, or reviewing JWT usage. Activate when: JWT, JSON Web Token, token authentication, bearer token, refresh token, token validation, JWT secret, token expiry.
npx skillsauth add latestaiagents/agent-skills jwt-securityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Secure implementation of JSON Web Tokens for authentication.
| Vulnerability | Risk | Description | |--------------|------|-------------| | Algorithm None | CRITICAL | Accepting unsigned tokens | | Algorithm Confusion | CRITICAL | RS256 → HS256 attack | | Weak Secret | HIGH | Brute-forceable secrets | | No Expiration | HIGH | Tokens valid forever | | Sensitive Data in Payload | MEDIUM | JWT payload is base64, not encrypted | | Token Leakage | HIGH | Exposed in logs/URLs |
const jwt = require('jsonwebtoken');
const JWT_CONFIG = {
accessSecret: process.env.JWT_ACCESS_SECRET, // 256+ bit random string
refreshSecret: process.env.JWT_REFRESH_SECRET,
accessExpiry: '15m', // Short-lived
refreshExpiry: '7d', // Longer-lived
algorithm: 'HS256', // Or RS256 for asymmetric
issuer: 'your-app-name',
audience: 'your-app-users'
};
function generateAccessToken(user) {
const payload = {
sub: user.id, // Subject (user ID)
email: user.email, // Only non-sensitive data
role: user.role,
// Don't include: password, SSN, credit card, etc.
};
return jwt.sign(payload, JWT_CONFIG.accessSecret, {
algorithm: JWT_CONFIG.algorithm,
expiresIn: JWT_CONFIG.accessExpiry,
issuer: JWT_CONFIG.issuer,
audience: JWT_CONFIG.audience,
jwtid: crypto.randomUUID() // Unique token ID
});
}
function generateRefreshToken(user) {
const payload = {
sub: user.id,
type: 'refresh',
family: crypto.randomUUID() // For refresh token rotation
};
return jwt.sign(payload, JWT_CONFIG.refreshSecret, {
algorithm: JWT_CONFIG.algorithm,
expiresIn: JWT_CONFIG.refreshExpiry,
issuer: JWT_CONFIG.issuer
});
}
function verifyAccessToken(token) {
try {
// CRITICAL: Always specify allowed algorithms
return jwt.verify(token, JWT_CONFIG.accessSecret, {
algorithms: [JWT_CONFIG.algorithm], // Whitelist!
issuer: JWT_CONFIG.issuer,
audience: JWT_CONFIG.audience,
complete: true // Returns header + payload
});
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new AuthError('Token expired', 'TOKEN_EXPIRED');
}
if (error instanceof jwt.JsonWebTokenError) {
throw new AuthError('Invalid token', 'INVALID_TOKEN');
}
throw error;
}
}
// VULNERABLE - Never do this!
// jwt.verify(token, secret); // Accepts any algorithm!
// jwt.decode(token); // No verification at all!
async function authenticateJWT(req, res, next) {
// Extract token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const decoded = verifyAccessToken(token);
// Optional: Check if token is blacklisted
if (await isTokenBlacklisted(decoded.payload.jti)) {
return res.status(401).json({ error: 'Token revoked' });
}
// Attach user to request
req.user = {
id: decoded.payload.sub,
email: decoded.payload.email,
role: decoded.payload.role
};
next();
} catch (error) {
if (error.code === 'TOKEN_EXPIRED') {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED'
});
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Store refresh tokens in database
const refreshTokenSchema = {
id: 'uuid',
userId: 'uuid',
tokenHash: 'string', // Store hash, not token
family: 'string', // For rotation detection
expiresAt: 'datetime',
revokedAt: 'datetime?',
replacedBy: 'uuid?'
};
async function refreshTokens(refreshToken) {
// Verify refresh token
let decoded;
try {
decoded = jwt.verify(refreshToken, JWT_CONFIG.refreshSecret, {
algorithms: [JWT_CONFIG.algorithm]
});
} catch {
throw new AuthError('Invalid refresh token');
}
// Find token in database
const tokenHash = hashToken(refreshToken);
const storedToken = await db.refreshTokens.findOne({
tokenHash,
revokedAt: null
});
if (!storedToken) {
// Token not found or already revoked
// Possible token reuse attack - revoke entire family
await db.refreshTokens.updateMany(
{ family: decoded.family },
{ revokedAt: new Date() }
);
throw new AuthError('Refresh token reuse detected');
}
// Check expiration
if (new Date() > storedToken.expiresAt) {
throw new AuthError('Refresh token expired');
}
// Rotate: revoke old, create new
const user = await db.users.findById(decoded.sub);
const newAccessToken = generateAccessToken(user);
const newRefreshToken = generateRefreshToken(user);
// Revoke old refresh token
await db.refreshTokens.update(storedToken.id, {
revokedAt: new Date(),
replacedBy: newRefreshToken.id
});
// Store new refresh token
await db.refreshTokens.create({
userId: user.id,
tokenHash: hashToken(newRefreshToken),
family: decoded.family, // Same family for rotation tracking
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken
};
}
// Blacklist for immediate revocation
const tokenBlacklist = new Map(); // In production, use Redis
async function revokeToken(token) {
const decoded = jwt.decode(token);
if (decoded && decoded.jti) {
// Store until token would naturally expire
const ttl = decoded.exp * 1000 - Date.now();
await redis.setex(`blacklist:${decoded.jti}`, ttl / 1000, '1');
}
}
async function isTokenBlacklisted(jti) {
return await redis.exists(`blacklist:${jti}`);
}
// Logout endpoint
app.post('/logout', authenticateJWT, async (req, res) => {
// Revoke access token
await revokeToken(req.headers.authorization.substring(7));
// Revoke all refresh tokens for user
await db.refreshTokens.updateMany(
{ userId: req.user.id },
{ revokedAt: new Date() }
);
res.json({ success: true });
});
const fs = require('fs');
// For distributed systems or when verifier != issuer
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
function generateTokenRS256(user) {
return jwt.sign(
{ sub: user.id },
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'auth-service'
}
);
}
function verifyTokenRS256(token) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'], // CRITICAL: Only allow RS256
issuer: 'auth-service'
});
}
# Generate RSA key pair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
// BEST: HttpOnly cookie (for web apps)
res.cookie('accessToken', token, {
httpOnly: true, // No JS access
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 900000 // 15 minutes
});
// OK: Memory (for SPAs, lost on refresh)
let accessToken = null;
function setToken(token) {
accessToken = token;
}
// AVOID: localStorage (XSS vulnerable)
// localStorage.setItem('token', token); // DON'T!
// MISTAKE 1: Not specifying algorithm
jwt.verify(token, secret); // Vulnerable to algorithm switching!
// MISTAKE 2: Using decode instead of verify
const user = jwt.decode(token); // No signature check!
// MISTAKE 3: Sensitive data in payload
jwt.sign({ password: user.password }, secret); // NO!
// MISTAKE 4: Weak secret
jwt.sign(payload, 'secret'); // Easily brute-forced!
// MISTAKE 5: No expiration
jwt.sign(payload, secret); // No exp = valid forever!
// MISTAKE 6: Token in URL
res.redirect(`/dashboard?token=${token}`); // Logged everywhere!
development
Test skills for correct activation, content quality, and regression — both automated checks (frontmatter validity, lint) and manual verification (query-suite activation testing). Covers CI integration and how to catch skill regressions before users do. Use this skill when adding skills to a repo, setting up CI for a skill library, or debugging "the skill exists but doesn't work". Activate when: test skills, validate skills, skill CI, skill linting, skill activation test, skill regression.
documentation
Write the YAML frontmatter for a SKILL.md file so it activates reliably — name, description, and activation keywords that the model matches against. Covers length, tone, and the most common frontmatter mistakes. Use this skill when authoring a new skill, fixing a skill that isn't auto-activating, or reviewing skills for publication. Activate when: SKILL.md frontmatter, skill description, skill activation, skill YAML, write a skill, author a skill.
development
Design skills that fire at the right moment — neither over-eager (noise) nor under-eager (silent). Covers activation specificity, trigger phrases, disambiguation between overlapping skills, and debugging activation. Use this skill when multiple skills could fire on the same query, a skill never fires, or a skill fires too often. Activate when: skill won't activate, skill over-activates, overlapping skills, skill triggers, skill selection, skill disambiguation.
development
Structure SKILL.md content so the model reads just enough — concise summary up front, progressively deeper detail, examples on demand. Covers section ordering, length budgets, when to split into multiple skills. Use this skill when writing or refactoring a skill body, one skill has grown too long, or a skill is wordy but not useful. Activate when: SKILL.md structure, skill content, skill too long, split skill, progressive disclosure, skill body.