auth-patterns
Authentication and authorization implementation patterns: JWT, sessions (httpOnly cookies), OAuth2/OIDC, API keys, RBAC, and MFA. Covers TypeScript, Python, Go, and Java with security-hardened code examples.
Best use case
auth-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Authentication and authorization implementation patterns: JWT, sessions (httpOnly cookies), OAuth2/OIDC, API keys, RBAC, and MFA. Covers TypeScript, Python, Go, and Java with security-hardened code examples.
Teams using auth-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/auth-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How auth-patterns Compares
| Feature / Agent | auth-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Authentication and authorization implementation patterns: JWT, sessions (httpOnly cookies), OAuth2/OIDC, API keys, RBAC, and MFA. Covers TypeScript, Python, Go, and Java with security-hardened code examples.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Auth Patterns Skill
Authentication (who are you?) and authorization (what can you do?) are where most security vulnerabilities live. This skill covers correct implementation — not just review.
## When to Activate
- Implementing login / logout flows
- Choosing between JWT and server-side sessions
- Adding OAuth2 / social login (GitHub, Google, etc.)
- Implementing API key authentication (server-to-server)
- Adding role-based access control (RBAC)
- Implementing refresh token rotation
- Adding MFA (multi-factor authentication)
- Auditing an existing auth flow for security gaps or token leakage
---
## Choosing the Right Auth Mechanism
```plantuml
@startuml
start
if (Server-to-server API?) then (yes)
:API Keys\n(X-API-Key header);
stop
else (no)
if (Third-party login\n(GitHub, Google)?) then (yes)
:OAuth2 / OIDC\n(Authorization Code + PKCE);
stop
else (no)
if (Stateless / horizontal scale\nor mobile app?) then (yes)
:JWT Access Token\n+ Refresh Token Rotation\n(store in httpOnly cookie);
stop
else (no)
:Server-Side Sessions\n(Redis-backed, httpOnly cookie);
note right
Simpler, more secure
for traditional web apps
end note
stop
endif
endif
endif
@enduml
```
---
## Pattern 1: Session-Based Auth (recommended for web apps)
Simpler, more secure than JWT for traditional web applications. The server holds state; tokens can be immediately invalidated.
### TypeScript (Express + Redis)
```typescript
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
import bcrypt from 'bcrypt';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET, // min 32 random bytes
name: '__Host-sid', // __Host- prefix = Secure + no Domain = phishing protection
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // JS cannot read it (XSS protection)
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
},
}));
// Login
app.post('/api/v1/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
// Same response for wrong email AND wrong password (timing-safe)
return res.status(401).json(problem(401, 'Invalid credentials'));
}
req.session.regenerate((err) => { // Prevent session fixation
req.session.userId = user.id;
req.session.role = user.role;
res.status(200).json({ data: { id: user.id, email: user.email } });
});
});
// Auth middleware
export function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json(problem(401, 'Authentication required'));
}
next();
}
// Logout
app.post('/api/v1/auth/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('__Host-sid');
res.status(204).send();
});
});
```
---
## Pattern 2: JWT Auth (stateless / mobile)
Use when: mobile apps, microservices needing stateless auth, horizontal scaling without shared session store.
**Critical rules:**
- Store access token in memory (JS variable), NOT localStorage (XSS risk)
- Store refresh token in httpOnly cookie
- Short-lived access tokens (15 min), long-lived refresh tokens (7 days)
- Rotate refresh tokens on every use (detect theft)
```typescript
import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
const ACCESS_TTL = '15m';
const REFRESH_TTL = '7d';
function issueTokens(userId: string, role: string) {
const accessToken = jwt.sign({ sub: userId, role }, ACCESS_SECRET, { expiresIn: ACCESS_TTL });
const refreshToken = jwt.sign({ sub: userId, jti: randomBytes(16).toString('hex') }, REFRESH_SECRET, { expiresIn: REFRESH_TTL });
return { accessToken, refreshToken };
}
// Login
app.post('/api/v1/auth/login', async (req, res) => {
const user = await validateCredentials(req.body);
const { accessToken, refreshToken } = issueTokens(user.id, user.role);
// Refresh token: httpOnly cookie
res.cookie('refresh_token', refreshToken, {
httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/api/v1/auth/refresh', // Only sent to refresh endpoint
});
// Access token: response body (client stores in memory)
res.json({ data: { access_token: accessToken, expires_in: 900 } });
});
// Refresh — rotate refresh token on every use
app.post('/api/v1/auth/refresh', async (req, res) => {
const token = req.cookies.refresh_token;
if (!token) return res.status(401).json(problem(401, 'No refresh token'));
try {
const payload = jwt.verify(token, REFRESH_SECRET) as jwt.JwtPayload;
// Check token hasn't been used before (rotation theft detection)
const used = await redis.get(`refresh:used:${payload.jti}`);
if (used) {
// Token reuse detected — invalidate all refresh tokens for user
await redis.set(`refresh:revoked:${payload.sub}`, '1', { EX: 7 * 24 * 3600 });
return res.status(401).json(problem(401, 'Refresh token reuse detected'));
}
await redis.set(`refresh:used:${payload.jti}`, '1', { EX: 7 * 24 * 3600 });
const user = await User.findById(payload.sub);
const { accessToken, refreshToken: newRefresh } = issueTokens(user.id, user.role);
res.cookie('refresh_token', newRefresh, { httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, path: '/api/v1/auth/refresh' });
res.json({ data: { access_token: accessToken, expires_in: 900 } });
} catch {
res.status(401).json(problem(401, 'Invalid refresh token'));
}
});
// Auth middleware
export function requireAuth(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json(problem(401, 'Missing token'));
try {
req.user = jwt.verify(token, ACCESS_SECRET) as jwt.JwtPayload;
next();
} catch {
res.status(401).json(problem(401, 'Invalid token'));
}
}
```
---
## Pattern 3: OAuth2 / OIDC (Social Login)
```typescript
// GitHub OAuth2 — Authorization Code flow with state param (CSRF protection)
import { randomBytes } from 'crypto';
app.get('/api/v1/auth/github', (req, res) => {
const state = randomBytes(16).toString('hex');
req.session.oauthState = state; // Store state in session
const params = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID,
redirect_uri: `${process.env.APP_URL}/api/v1/auth/github/callback`,
scope: 'read:user user:email',
state,
});
res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});
app.get('/api/v1/auth/github/callback', async (req, res) => {
const { code, state } = req.query;
// Validate state (CSRF protection)
if (state !== req.session.oauthState) {
return res.status(400).json(problem(400, 'Invalid OAuth state'));
}
delete req.session.oauthState;
// Exchange code for token
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
}),
});
const { access_token } = await tokenRes.json();
// Fetch user profile
const profileRes = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${access_token}`, 'User-Agent': 'MyApp' },
});
const profile = await profileRes.json();
// Find or create user — match by GitHub ID, not email (emails can change)
let user = await User.findByGithubId(profile.id);
if (!user) {
user = await User.create({
githubId: profile.id,
name: profile.name ?? profile.login,
avatarUrl: profile.avatar_url,
});
}
req.session.regenerate(() => {
req.session.userId = user.id;
res.redirect(process.env.APP_URL + '/dashboard');
});
});
```
---
## Pattern 4: API Keys (Server-to-Server)
```typescript
import { createHash, timingSafeEqual } from 'crypto';
// Generate: store prefix (for display) + hash (for verification)
async function createApiKey(userId: string) {
const key = `sk_live_${randomBytes(32).toString('base64url')}`;
const keyHash = createHash('sha256').update(key).digest('hex');
const prefix = key.substring(0, 12); // e.g. "sk_live_Abc1"
await db.insert(apiKeys).values({ userId, keyHash, prefix });
return key; // Return the raw key ONCE — never store it
}
// Middleware: constant-time comparison (prevents timing attacks)
export async function apiKeyAuth(req, res, next) {
const key = req.headers['x-api-key'];
if (!key) return res.status(401).json(problem(401, 'Missing API key'));
const keyHash = createHash('sha256').update(key).digest('hex');
const apiKey = await db.query.apiKeys.findFirst({
where: eq(apiKeys.keyHash, keyHash),
});
if (!apiKey) {
// Still do the comparison to prevent timing attacks
timingSafeEqual(Buffer.from(keyHash), Buffer.from(keyHash));
return res.status(401).json(problem(401, 'Invalid API key'));
}
await db.update(apiKeys).set({ lastUsedAt: new Date() }).where(eq(apiKeys.id, apiKey.id));
req.userId = apiKey.userId;
next();
}
```
---
## Pattern 5: RBAC (Role-Based Access Control)
> This section covers flat role-based permission checks. For full RBAC implementation patterns (attribute-based access, hierarchical roles, policy engines like OPA/Casbin) → see `kubernetes-patterns` for K8s RBAC, or consider a dedicated `rbac-patterns` skill for application-level RBAC.
```typescript
// Define roles and permissions
const permissions = {
admin: ['users:read', 'users:write', 'orders:read', 'orders:write', 'orders:delete'],
manager: ['orders:read', 'orders:write'],
customer: ['orders:read'],
} as const;
type Permission = typeof permissions[keyof typeof permissions][number];
// Middleware factory
export function requirePermission(permission: Permission) {
return (req, res, next) => {
const userRole = req.session?.role ?? req.user?.role;
const allowed = permissions[userRole] ?? [];
if (!allowed.includes(permission)) {
return res.status(403).json(problem(403, 'Insufficient permissions'));
}
next();
};
}
// Usage
app.delete('/api/v1/orders/:id',
requireAuth,
requirePermission('orders:delete'),
deleteOrderHandler,
);
```
---
## Anti-Patterns
### Hashing Passwords with SHA-256 or MD5
**Wrong:**
```typescript
import { createHash } from 'crypto';
// SHA-256 is fast — attackers can brute-force billions of guesses per second
const passwordHash = createHash('sha256').update(password).digest('hex');
```
**Correct:**
```typescript
import bcrypt from 'bcrypt';
// bcrypt is deliberately slow — cost factor 12 = ~250ms per hash
const passwordHash = await bcrypt.hash(password, 12);
const valid = await bcrypt.compare(password, passwordHash);
```
**Why:** Fast hash functions (SHA, MD5) allow offline brute-force attacks to crack passwords in seconds; bcrypt/argon2id are designed to be computationally expensive.
---
### Skipping Session Regeneration After Login
**Wrong:**
```typescript
app.post('/api/v1/auth/login', async (req, res) => {
const user = await validateCredentials(req.body);
req.session.userId = user.id; // Reuses the pre-login session ID
res.json({ data: user });
});
```
**Correct:**
```typescript
app.post('/api/v1/auth/login', async (req, res) => {
const user = await validateCredentials(req.body);
req.session.regenerate((err) => { // Issues a new session ID on login
req.session.userId = user.id;
res.json({ data: { id: user.id, email: user.email } });
});
});
```
**Why:** Reusing the pre-login session ID enables session fixation attacks, where an attacker plants a known session ID and hijacks the session after the victim logs in.
---
### Matching OAuth Users by Email Instead of Provider ID
**Wrong:**
```typescript
// If an attacker registers a GitHub account with the same email, they take over the account
let user = await User.findByEmail(profile.email);
if (!user) user = await User.create({ email: profile.email });
```
**Correct:**
```typescript
// Match by the immutable provider-specific ID — email can be changed or shared
let user = await User.findByGithubId(profile.id);
if (!user) user = await User.create({ githubId: profile.id, name: profile.name });
```
**Why:** Emails are not unique across providers and can be changed; provider IDs are immutable and scoped to a single identity.
---
### Storing Raw API Keys in the Database
**Wrong:**
```typescript
// If the DB is compromised, all API keys are immediately usable
await db.insert(apiKeys).values({ userId, key: rawKey });
```
**Correct:**
```typescript
import { createHash } from 'crypto';
// Store only the SHA-256 hash; return the raw key once to the user
const keyHash = createHash('sha256').update(rawKey).digest('hex');
const prefix = rawKey.substring(0, 12); // For display only
await db.insert(apiKeys).values({ userId, keyHash, prefix });
return rawKey; // Show once — never store
```
**Why:** A database breach exposing raw keys gives attackers immediate full access; hashed keys require brute-force that is infeasible for long random keys.
---
### Performing RBAC Checks Only on the Frontend
**Wrong:**
```typescript
// React component hides the button, but the API has no authorization check
function AdminPanel() {
if (user.role !== 'admin') return null;
return <button onClick={() => deleteUser(id)}>Delete User</button>;
}
```
**Correct:**
```typescript
// Every state-changing endpoint enforces permissions server-side
app.delete('/api/v1/users/:id',
requireAuth,
requirePermission('users:write'),
deleteUserHandler,
);
```
**Why:** Frontend checks are bypassed by calling the API directly; authorization must be enforced on every server-side request.
---
## Security Hardening Checklist
- [ ] Passwords hashed with bcrypt (cost factor ≥ 12) or argon2id — never SHA/MD5
- [ ] Session secret is 32+ random bytes from `crypto.randomBytes()`
- [ ] `session.regenerate()` called on login (prevent session fixation)
- [ ] Refresh tokens rotated on every use, old tokens invalidated
- [ ] OAuth2 state parameter validated (CSRF protection)
- [ ] OAuth2 users matched by provider ID, not email
- [ ] API keys hashed with SHA-256 before storage — raw key never stored
- [ ] API key comparison uses `timingSafeEqual` (prevent timing attacks)
- [ ] Auth cookies: `httpOnly`, `secure`, `sameSite` all set
- [ ] RBAC checked server-side on every request, never client-side only
- [ ] Rate limiting on login (5 attempts/15min per IP)
- [ ] Account lockout or CAPTCHA after brute force threshold
- [ ] Error messages identical for wrong email AND wrong password
- [ ] No sensitive data (tokens, passwords) in logs or error responsesRelated Skills
zero-trust-patterns
Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.
webrtc-patterns
WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
webhook-patterns
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
wasm-patterns
WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).
ux-micro-patterns
UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.
typescript-patterns
TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.
typescript-patterns-advanced
Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.
typescript-monorepo-patterns
TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.
terraform-patterns
Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.
swiftui-patterns
SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.
swift-patterns
Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.
swift-patterns-advanced
Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.