CtrlK
BlogDocsLog inGet started
Tessl Logo

jwt-auth-skill

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

Quality

66%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./auth/jwt-auth-skill/SKILL.md
SKILL.md
Quality
Evals
Security

JWT Auth Skill

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.

Prerequisites

  • Node.js >= 20.x or Python >= 3.11
  • A database for storing refresh tokens (PostgreSQL, Redis, or MongoDB)
  • jsonwebtoken (Node.js) or PyJWT / python-jose (Python)

Scaffold Command

# Node.js
npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs

# Python (FastAPI)
pip install pyjwt[crypto] bcrypt passlib

Project Structure

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

Key Conventions

  • Use short-lived access tokens (15 min) and long-lived refresh tokens (7-30 days).
  • Access tokens are stateless (verified by signature). Refresh tokens are stateful (stored in DB for revocation).
  • Store refresh tokens in httpOnly, Secure, SameSite=Strict cookies. Send access tokens in the Authorization header.
  • Implement token rotation: issuing a new refresh token invalidates the old one (prevents replay attacks).
  • Include only minimal claims in the JWT payload: sub (user ID), role, iat, exp. Never include passwords or PII.
  • Hash passwords with bcrypt (cost factor 12+).
  • Middleware extracts and verifies the access token. Guards check roles/permissions.

Essential Patterns

Token Blacklist (Logout)

// 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);
}

Token Types and Constants (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
};

JWT Utilities (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;
}

Password Utilities (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 Service (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 Middleware (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" });
  }
}

Role Guard (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 Controller (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();
});

Python / FastAPI Variant

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)

Common Commands

# 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>"

Integration Notes

  • OAuth2: Pair with oauth2-skill for social login. After OAuth2 callback, issue JWT tokens using the same generateTokenPair function.
  • NextAuth: If using NextAuth, it manages its own sessions. Use JWT auth skill for custom API backends not using NextAuth.
  • Database: Store refresh tokens using Prisma, TypeORM, Drizzle, or Redis. Redis with TTL is ideal for auto-expiring refresh tokens.
  • Nginx: Configure Nginx to pass Authorization header and cookies through to the backend.
  • Testing: Mock JWT verification in tests. Use a test secret key and pre-signed tokens for integration tests.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.