auth-patterns
JWT, OAuth, session management, and token refresh implementations with security-correct patterns
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
JWT, OAuth, session management, and token refresh implementations with security-correct patterns
Agent definition
auth-patterns.mddescription: JWT, OAuth, session management, and token refresh implementations with security-correct patterns
Authentication Patterns for Node.js APIs
> **Scope**: JWT-based auth, OAuth 2.0 integration, session management, and password security for Express/Next.js APIs. Does not cover frontend auth flows or mobile OAuth. > **Version range**: Node.js 18+, `jsonwebtoken` 9.0+, `bcrypt` 5.0+ > **Generated**: 2026-04-08
---
Overview
Auth fails in two ways: insecure defaults (long-lived tokens, weak secrets, missing expiry) and broken error handling (timing attacks via username enumeration, stack traces in 401 responses).
---
Pattern Table
| Pattern | Version | Use When | Avoid When | |---------|---------|----------|------------| | `jwt.sign()` with `expiresIn` | jsonwebtoken 9+ | All access token issuance | Never omit — tokens live forever | | `jwt.verify()` with `algorithms` | jsonwebtoken 9+ | Token validation | `jwt.decode()` — no signature check | | `bcrypt.hash()` with cost 12 | bcrypt 5+ | Password storage | `md5`, `sha1`, any fast hash | | `crypto.timingSafeEqual()` | Node 16+ | Webhook signature comparison | `===` string comparison | | Refresh token rotation | — | Session persistence beyond 15min | Single long-lived access token |
---
Correct Patterns
JWT Issuance with Short Expiry and Refresh
import jwt from 'jsonwebtoken';
import { randomUUID } from 'crypto';
const ACCESS_TOKEN_TTL = '15m';
const REFRESH_TOKEN_TTL = '7d';
interface TokenPair {
accessToken: string;
refreshToken: string;
expiresIn: number; // seconds
}
function issueTokenPair(userId: string, roles: string[]): TokenPair {
const accessToken = jwt.sign(
{ sub: userId, roles },
process.env.JWT_SECRET!,
{
algorithm: 'HS256',
expiresIn: ACCESS_TOKEN_TTL,
issuer: 'api.example.com',
jwtid: randomUUID(), // Unique ID for revocation
}
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
process.env.REFRESH_SECRET!,
{ algorithm: 'HS256', expiresIn: REFRESH_TOKEN_TTL }
);
return { accessToken, refreshToken, expiresIn: 15 * 60 };
}**Why**: 15-minute access tokens limit breach window. `jwtid` enables per-token revocation.
---
JWT Middleware with Explicit Algorithm Pinning
import { Request, Response, NextFunction } from 'express';
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Missing authorization header' });
return;
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'], // Explicit — prevents alg:none attack
issuer: 'api.example.com',
}) as { sub: string; roles: string[] };
req.user = { id: payload.sub, roles: payload.roles };
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
} else {
res.status(401).json({ error: 'Invalid token' });
// Do NOT leak err.message — reveals token structure to attackers
}
}
}**Why**: Prevents the "algorithm none" attack. Without `algorithms`, `jsonwebtoken` < 9 accepted unsigned tokens.
---
Webhook Signature Verification (Stripe/GitHub pattern)
import { createHmac, timingSafeEqual } from 'crypto';
export function verifyWebhookSignature(
payload: Buffer,
signature: string,
secret: string,
tolerance = 300 // 5 minutes
): boolean {
// Stripe format: t=timestamp,v1=signature
const parts = Object.fromEntries(
signature.split(',').map((p) => p.split('=') as [string, string])
);
const timestamp = parseInt(parts['t'] ?? '0', 10);
// Reject stale webhooks (replay attack prevention)
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > tolerance) return false;
const signedPayload = `${timestamp}.${payload.toString('utf-8')}`;
const expected = createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison — prevents timing oracle attacks
return timingSafeEqual(
Buffer.from(parts['v1'] ?? '', 'hex'),
Buffer.from(expected, 'hex')
);
}**Why**: `timingSafeEqual` takes constant time. Regular `===` returns early on first mismatch, leaking signature bytes via timing oracle.
---
Pattern Catalog
Always Use jwt.verify() with Algorithm Pinning
**Detection**:
grep -rn 'jwt\.decode(' --include="*.ts" --include="*.js" src/
rg 'jwt\.decode\(' --type ts src/**Signal**:
// "decode" sounds like "verify" — it isn't
const payload = jwt.decode(req.headers.authorization?.split(' ')[1] ?? '');
if (payload && payload.sub) {
req.user = { id: payload.sub };
next();
}**Why this matters**: `jwt.decode()` does NOT verify the signature. Any attacker can craft a token with any `sub` value. Auth completely bypassed.
**Preferred action**: Always use `jwt.verify()` with `algorithms` specified.
---
Use Constant-Time Auth Responses
**Detection**:
grep -rn 'return.*null\|return.*false' --include="*.ts" src/auth
# Look for early returns before bcrypt.compare
grep -rn 'findUser\|findByEmail' --include="*.ts" src/ -A10 | grep -v 'compare\|bcrypt'
**Signal**:
async function login(email: string, password: string) {
const user = await db.users.findByEmail(email);
if (!user) {
return { error: 'User not found' }; // Responds fast — user doesn't exist
}
const valid = await bcrypt.compare(password, user.passwordHash); // Responds slow
if (!valid) {
return { error: 'Invalid password' }; // Responds after bcrypt delay
}
return { token: issueToken(user.id) };
}**Why this matters**: User doesn't exist = ~1ms (DB miss). Wrong password = ~100ms (bcrypt). Attackers enumera
Read more
description: JWT, OAuth, session management, and token refresh implementations with security-correct patterns
Authentication Patterns for Node.js APIs
> **Scope**: JWT-based auth, OAuth 2.0 integration, session management, and password security for Express/Next.js APIs. Does not cover frontend auth flows or mobile OAuth. > **Version range**: Node.js 18+, `jsonwebtoken` 9.0+, `bcrypt` 5.0+ > **Generated**: 2026-04-08
---
Overview
Auth fails in two ways: insecure defaults (long-lived tokens, weak secrets, missing expiry) and broken error handling (timing attacks via username enumeration, stack traces in 401 responses).
---
Pattern Table
| Pattern | Version | Use When | Avoid When | |---------|---------|----------|------------| | `jwt.sign()` with `expiresIn` | jsonwebtoken 9+ | All access token issuance | Never omit — tokens live forever | | `jwt.verify()` with `algorithms` | jsonwebtoken 9+ | Token validation | `jwt.decode()` — no signature check | | `bcrypt.hash()` with cost 12 | bcrypt 5+ | Password storage | `md5`, `sha1`, any fast hash | | `crypto.timingSafeEqual()` | Node 16+ | Webhook signature comparison | `===` string comparison | | Refresh token rotation | — | Session persistence beyond 15min | Single long-lived access token |
---
Correct Patterns
JWT Issuance with Short Expiry and Refresh
import jwt from 'jsonwebtoken';
import { randomUUID } from 'crypto';
const ACCESS_TOKEN_TTL = '15m';
const REFRESH_TOKEN_TTL = '7d';
interface TokenPair {
accessToken: string;
refreshToken: string;
expiresIn: number; // seconds
}
function issueTokenPair(userId: string, roles: string[]): TokenPair {
const accessToken = jwt.sign(
{ sub: userId, roles },
process.env.JWT_SECRET!,
{
algorithm: 'HS256',
expiresIn: ACCESS_TOKEN_TTL,
issuer: 'api.example.com',
jwtid: randomUUID(), // Unique ID for revocation
}
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
process.env.REFRESH_SECRET!,
{ algorithm: 'HS256', expiresIn: REFRESH_TOKEN_TTL }
);
return { accessToken, refreshToken, expiresIn: 15 * 60 };
}**Why**: 15-minute access tokens limit breach window. `jwtid` enables per-token revocation.
---
JWT Middleware with Explicit Algorithm Pinning
import { Request, Response, NextFunction } from 'express';
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Missing authorization header' });
return;
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'], // Explicit — prevents alg:none attack
issuer: 'api.example.com',
}) as { sub: string; roles: string[] };
req.user = { id: payload.sub, roles: payload.roles };
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
} else {
res.status(401).json({ error: 'Invalid token' });
// Do NOT leak err.message — reveals token structure to attackers
}
}
}**Why**: Prevents the "algorithm none" attack. Without `algorithms`, `jsonwebtoken` < 9 accepted unsigned tokens.
---
Webhook Signature Verification (Stripe/GitHub pattern)
import { createHmac, timingSafeEqual } from 'crypto';
export function verifyWebhookSignature(
payload: Buffer,
signature: string,
secret: string,
tolerance = 300 // 5 minutes
): boolean {
// Stripe format: t=timestamp,v1=signature
const parts = Object.fromEntries(
signature.split(',').map((p) => p.split('=') as [string, string])
);
const timestamp = parseInt(parts['t'] ?? '0', 10);
// Reject stale webhooks (replay attack prevention)
const age = Math.floor(Date.now() / 1000) - timestamp;
if (age > tolerance) return false;
const signedPayload = `${timestamp}.${payload.toString('utf-8')}`;
const expected = createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison — prevents timing oracle attacks
return timingSafeEqual(
Buffer.from(parts['v1'] ?? '', 'hex'),
Buffer.from(expected, 'hex')
);
}**Why**: `timingSafeEqual` takes constant time. Regular `===` returns early on first mismatch, leaking signature bytes via timing oracle.
---
Pattern Catalog
Always Use jwt.verify() with Algorithm Pinning
**Detection**:
grep -rn 'jwt\.decode(' --include="*.ts" --include="*.js" src/
rg 'jwt\.decode\(' --type ts src/**Signal**:
// "decode" sounds like "verify" — it isn't
const payload = jwt.decode(req.headers.authorization?.split(' ')[1] ?? '');
if (payload && payload.sub) {
req.user = { id: payload.sub };
next();
}**Why this matters**: `jwt.decode()` does NOT verify the signature. Any attacker can craft a token with any `sub` value. Auth completely bypassed.
**Preferred action**: Always use `jwt.verify()` with `algorithms` specified.
---
Use Constant-Time Auth Responses
**Detection**:
grep -rn 'return.*null\|return.*false' --include="*.ts" src/auth # Look for early returns before bcrypt.compare grep -rn 'findUser\|findByEmail' --include="*.ts" src/ -A10 | grep -v 'compare\|bcrypt'
**Signal**:
async function login(email: string, password: string) {
const user = await db.users.findByEmail(email);
if (!user) {
return { error: 'User not found' }; // Responds fast — user doesn't exist
}
const valid = await bcrypt.compare(password, user.passwordHash); // Responds slow
if (!valid) {
return { error: 'Invalid password' }; // Responds after bcrypt delay
}
return { token: issueToken(user.id) };
}**Why this matters**: User doesn't exist = ~1ms (DB miss). Wrong password = ~100ms (bcrypt). Attackers enumera
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

