| name | auth-patterns |
| description | 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. |
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
@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)
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,
name: '__Host-sid',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000,
},
}));
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))) {
return res.status(401).json(problem(401, 'Invalid credentials'));
}
req.session.regenerate((err) => {
req.session.userId = user.id;
req.session.role = user.role;
res.status(200).json({ data: { id: user.id, email: user.email } });
});
});
export function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json(problem(401, 'Authentication required'));
}
next();
}
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)
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 };
}
app.post('/api/v1/auth/login', async (req, res) => {
const user = await (req.);
{ accessToken, refreshToken } = (user., user.);
res.(, refreshToken, {
: , : , : ,
: * * * * ,
: ,
});
res.({ : { : accessToken, : } });
});
app.(, (req, res) => {
token = req..;
(!token) res.().((, ));
{
payload = jwt.(token, ) jwt.;
used = redis.();
(used) {
redis.(, , { : * * });
res.().((, ));
}
redis.(, , { : * * });
user = .(payload.);
{ accessToken, : newRefresh } = (user., user.);
res.(, newRefresh, { : , : , : ,
: * * * * , : });
res.({ : { : accessToken, : } });
} {
res.().((, ));
}
});
() {
token = req..?.(, );
(!token) res.().((, ));
{
req. = jwt.(token, ) jwt.;
();
} {
res.().((, ));
}
}
Pattern 3: OAuth2 / OIDC (Social Login)
import { randomBytes } from 'crypto';
app.get('/api/v1/auth/github', (req, res) => {
const state = randomBytes(16).toString('hex');
req.session.oauthState = state;
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;
if (state !== req.session.oauthState) {
return res.status(400).json(problem(400, 'Invalid OAuth state'));
}
delete req.session.;
tokenRes = (, {
: ,
: { : , : },
: .({
: process..,
: process..,
code,
}),
});
{ access_token } = tokenRes.();
profileRes = (, {
: { : , : },
});
profile = profileRes.();
user = .(profile.);
(!user) {
user = .({
: profile.,
: profile. ?? profile.,
: profile.,
});
}
req..( {
req.. = user.;
res.(process.. + );
});
});
Pattern 4: API Keys (Server-to-Server)
import { createHash, timingSafeEqual } from 'crypto';
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);
await db.insert(apiKeys).values({ userId, keyHash, prefix });
return key;
}
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 = ().(key).();
apiKey = db...({
: (apiKeys., keyHash),
});
(!apiKey) {
(.(keyHash), .(keyHash));
res.().((, ));
}
db.(apiKeys).({ : () }).((apiKeys., apiKey.));
req. = apiKey.;
();
}
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.
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];
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();
};
}
app.delete('/api/v1/orders/:id',
requireAuth,
requirePermission(),
deleteOrderHandler,
);
Anti-Patterns
Hashing Passwords with SHA-256 or MD5
Wrong:
import { createHash } from 'crypto';
const passwordHash = createHash('sha256').update(password).digest('hex');
Correct:
import bcrypt from 'bcrypt';
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:
app.post('/api/v1/auth/login', async (req, res) => {
const user = await validateCredentials(req.body);
req.session.userId = user.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) => {
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:
let user = await User.findByEmail(profile.email);
if (!user) user = await User.create({ email: profile.email });
Correct:
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:
await db.insert(apiKeys).values({ userId, key: rawKey });
Correct:
import { createHash } from 'crypto';
const keyHash = createHash('sha256').update(rawKey).digest('hex');
const prefix = rawKey.substring(0, 12);
await db.insert(apiKeys).values({ userId, keyHash, prefix });
return rawKey;
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:
function AdminPanel() {
if (user.role !== 'admin') return null;
return <button onClick={() => deleteUser(id)}>Delete User</button>;
}
Correct:
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