auth/jwt-auth-skill/SKILL.md
Implement JWT-based authentication with access + refresh token pairs, token rotation, middleware/guard pattern, payload structure, expiration handling, httpOnly cookies vs Authorization header, and revocation strategies.
npx skillsauth add achreftlili/deep-dev-skills jwt-auth-skillInstall 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.
Implement JWT-based authentication with access + refresh token pairs, token rotation, middleware/guard pattern, payload structure, expiration handling, httpOnly cookies vs Authorization header, and revocation strategies.
jsonwebtoken (Node.js) or PyJWT / python-jose (Python)# Node.js
npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs
# Python (FastAPI)
pip install pyjwt[crypto] bcrypt passlib
src/
auth/
auth.controller.ts # Login, refresh, logout endpoints
auth.service.ts # Token generation, validation, rotation
auth.middleware.ts # JWT verification middleware
auth.guard.ts # Route guard (role-based)
types.ts # Token payload interfaces
constants.ts # Token expiry, cookie config
lib/
jwt.ts # Low-level JWT sign/verify wrappers
password.ts # bcrypt hash/compare wrappers
sub (user ID), role, iat, exp. Never include passwords or PII.// In-memory blacklist (use Redis in production for distributed systems)
const blacklistedTokens = new Set<string>();
function blacklistToken(jti: string, expiresIn: number): void {
blacklistedTokens.add(jti);
// Auto-cleanup after token would have expired anyway
setTimeout(() => blacklistedTokens.delete(jti), expiresIn * 1000);
}
function isBlacklisted(jti: string): boolean {
return blacklistedTokens.has(jti);
}
auth/types.ts + auth/constants.ts)// types.ts
export interface AccessTokenPayload {
sub: string; // user ID
role: string;
type: "access";
}
export interface RefreshTokenPayload {
sub: string;
tokenId: string; // unique ID for this refresh token (stored in DB)
type: "refresh";
}
// constants.ts
export const ACCESS_TOKEN_EXPIRY = "15m";
export const REFRESH_TOKEN_EXPIRY = "7d";
export const REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
export const COOKIE_OPTIONS = {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict" as const,
path: "/api/auth",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
};
lib/jwt.ts)import jwt from "jsonwebtoken";
import type { AccessTokenPayload, RefreshTokenPayload } from "../auth/types";
import { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY } from "../auth/constants";
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
export function signAccessToken(payload: Omit<AccessTokenPayload, "type">): string {
return jwt.sign({ ...payload, type: "access" }, ACCESS_SECRET, {
expiresIn: ACCESS_TOKEN_EXPIRY,
});
}
export function signRefreshToken(payload: Omit<RefreshTokenPayload, "type">): string {
return jwt.sign({ ...payload, type: "refresh" }, REFRESH_SECRET, {
expiresIn: REFRESH_TOKEN_EXPIRY,
});
}
export function verifyAccessToken(token: string): AccessTokenPayload {
const payload = jwt.verify(token, ACCESS_SECRET) as AccessTokenPayload;
if (payload.type !== "access") throw new Error("Invalid token type");
return payload;
}
export function verifyRefreshToken(token: string): RefreshTokenPayload {
const payload = jwt.verify(token, REFRESH_SECRET) as RefreshTokenPayload;
if (payload.type !== "refresh") throw new Error("Invalid token type");
return payload;
}
lib/password.ts)import bcrypt from "bcryptjs";
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function comparePassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
auth/auth.service.ts)import crypto from "node:crypto";
import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../lib/jwt";
import { hashPassword, comparePassword } from "../lib/password";
import { REFRESH_TOKEN_EXPIRY_MS } from "./constants";
// Replace with your actual DB queries
import { findUserByEmail, findUserById } from "../modules/users/users.repository";
import {
createRefreshToken,
findRefreshToken,
deleteRefreshToken,
deleteAllUserRefreshTokens,
} from "./refresh-token.repository";
export async function login(email: string, password: string) {
const user = await findUserByEmail(email);
if (!user) throw new Error("Invalid credentials");
const valid = await comparePassword(password, user.password);
if (!valid) throw new Error("Invalid credentials");
return generateTokenPair(user.id, user.role);
}
export async function refresh(refreshTokenStr: string) {
const payload = verifyRefreshToken(refreshTokenStr);
// Check if refresh token exists in DB (not revoked)
const storedToken = await findRefreshToken(payload.tokenId);
if (!storedToken) {
// Token reuse detected — revoke all tokens for this user
await deleteAllUserRefreshTokens(payload.sub);
throw new Error("Refresh token reuse detected");
}
// Rotate: delete old token, issue new pair
await deleteRefreshToken(payload.tokenId);
return generateTokenPair(payload.sub, storedToken.userRole);
}
export async function logout(refreshTokenStr: string) {
try {
const payload = verifyRefreshToken(refreshTokenStr);
await deleteRefreshToken(payload.tokenId);
} catch {
// Token already expired or invalid — ignore
}
}
export async function logoutAll(userId: string) {
await deleteAllUserRefreshTokens(userId);
}
async function generateTokenPair(userId: string, role: string) {
const tokenId = crypto.randomUUID();
const accessToken = signAccessToken({ sub: userId, role });
const refreshToken = signRefreshToken({ sub: userId, tokenId });
// Store refresh token reference in DB
// IMPORTANT: Run a periodic cleanup job to remove expired refresh tokens. In production, prefer Redis with automatic TTL expiry.
await createRefreshToken({
id: tokenId,
userId,
userRole: role,
expiresAt: new Date(Date.now() + REFRESH_TOKEN_EXPIRY_MS),
});
return { accessToken, refreshToken };
}
auth/auth.middleware.ts)import type { Request, Response, NextFunction } from "express";
import { verifyAccessToken } from "../lib/jwt";
import type { AccessTokenPayload } from "./types";
// Extend Express Request
declare global {
namespace Express {
interface Request {
user?: AccessTokenPayload;
}
}
}
export function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ message: "Missing access token" });
}
const token = authHeader.slice(7);
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch {
return res.status(401).json({ message: "Invalid or expired access token" });
}
}
auth/auth.guard.ts)import type { Request, Response, NextFunction } from "express";
export function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: "Insufficient permissions" });
}
next();
};
}
auth/auth.controller.ts)import { Router, type Request, type Response } from "express";
import { login, refresh, logout } from "./auth.service";
import { COOKIE_OPTIONS } from "./constants";
import { authenticate } from "./auth.middleware";
export const authRouter = Router();
authRouter.post("/login", async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
const { accessToken, refreshToken } = await login(email, password);
res.cookie("refresh_token", refreshToken, COOKIE_OPTIONS);
return res.json({ accessToken });
} catch {
return res.status(401).json({ message: "Invalid credentials" });
}
});
authRouter.post("/refresh", async (req: Request, res: Response) => {
const refreshToken = req.cookies?.refresh_token;
if (!refreshToken) {
return res.status(401).json({ message: "No refresh token" });
}
try {
const tokens = await refresh(refreshToken);
res.cookie("refresh_token", tokens.refreshToken, COOKIE_OPTIONS);
return res.json({ accessToken: tokens.accessToken });
} catch {
res.clearCookie("refresh_token", COOKIE_OPTIONS);
return res.status(401).json({ message: "Invalid refresh token" });
}
});
authRouter.post("/logout", authenticate, async (req: Request, res: Response) => {
const refreshToken = req.cookies?.refresh_token;
if (refreshToken) {
await logout(refreshToken);
}
res.clearCookie("refresh_token", COOKIE_OPTIONS);
return res.status(204).send();
});
from datetime import datetime, timedelta, timezone
from typing import Annotated
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
SECRET_KEY = "your-secret-key" # Use env var in production
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 15
security = HTTPBearer()
def create_access_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"type": "access",
"exp": datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
"iat": datetime.now(timezone.utc),
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def verify_access_token(token: str) -> dict:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("type") != "access":
raise HTTPException(status_code=401, detail="Invalid token type")
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
async def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
) -> dict:
return verify_access_token(credentials.credentials)
# Generate JWT secrets
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# Test login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password123"}'
# Test authenticated request
curl http://localhost:3000/api/users/me \
-H "Authorization: Bearer <access_token>"
# Test refresh
curl -X POST http://localhost:3000/api/auth/refresh \
--cookie "refresh_token=<refresh_token>"
oauth2-skill for social login. After OAuth2 callback, issue JWT tokens using the same generateTokenPair function.Authorization header and cookies through to the backend.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.