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.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Authentication (who are you?) and authorization (what can you do?) are where most security vulnerabilities live. This skill covers correct implementation — not just review.
@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
Simpler, more secure than JWT for traditional web applications. The server holds state; tokens can be immediately invalidated.
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();
});
});
Use when: mobile apps, microservices needing stateless auth, horizontal scaling without shared session store.
Critical rules:
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'));
}
}
// 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');
});
});
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();
}
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-patternsfor K8s RBAC, or consider a dedicatedrbac-patternsskill for application-level RBAC.
// 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,
);
Wrong:
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:
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.
Wrong:
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:
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.
Wrong:
// 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:
// 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.
Wrong:
// If the DB is compromised, all API keys are immediately usable
await db.insert(apiKeys).values({ userId, key: rawKey });
Correct:
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.
Wrong:
// 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:
// 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.
crypto.randomBytes()session.regenerate() called on login (prevent session fixation)timingSafeEqual (prevent timing attacks)httpOnly, secure, sameSite all set