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.
72
66%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./auth/jwt-auth-skill/SKILL.mdImplement 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 passlibsrc/
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 wrapperssub (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":"user@example.com","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.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.