Authentication and Authorization Patterns
AI-generated auth code is the most dangerous code in your application. It produces JWTs with no expiry, stores tokens in localStorage (XSS-accessible), skips CSRF protection, and implements role checks that can be bypassed by changing a URL parameter. Auth is the one area where "it works" means absolutely nothing if it's not also correct.
skilldb get vibe-coding-security-skills/authentication-authorization-patternsFull skill: 369 linesAuthentication and Authorization Patterns
AI-generated auth code is the most dangerous code in your application. It produces JWTs with no expiry, stores tokens in localStorage (XSS-accessible), skips CSRF protection, and implements role checks that can be bypassed by changing a URL parameter. Auth is the one area where "it works" means absolutely nothing if it's not also correct.
This skill covers session management, token handling, OAuth/OIDC, RBAC/ABAC, and the middleware patterns that make auth reliable.
Session Management
Cookie-Based Sessions (Server-Side State)
import session from 'express-session';
import RedisStore from 'connect-redis';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
app.use(session({
store: new RedisStore({ client: redis }),
name: '__Host-session', // __Host- prefix enforces Secure + Path=/
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
path: '/',
domain: undefined, // Current domain only
},
rolling: true, // Reset expiry on each request
}));
// Login
app.post('/api/auth/login', async (req, res) => {
const { email, password } = LoginSchema.parse(req.body);
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Regenerate session ID to prevent session fixation
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.role = user.role;
req.session.tenantId = user.tenantId;
req.session.save(() => {
res.json({ user: { id: user.id, name: user.name } });
});
});
});
// Logout
app.post('/api/auth/logout', (req, res) => {
req.session.destroy((err) => {
res.clearCookie('__Host-session');
res.json({ success: true });
});
});
JWT vs Session Cookies
| Factor | Session Cookies | JWTs |
|---|---|---|
| Storage | Server (Redis/DB) | Client (cookie/header) |
| Revocation | Instant (delete from store) | Hard (need blocklist) |
| Scalability | Needs shared session store | Stateless — any server can verify |
| XSS risk | Low (httpOnly cookie) | High if in localStorage |
| CSRF risk | Needs CSRF token | Not needed if in Authorization header |
| Best for | Traditional web apps | APIs, microservices, mobile |
JWT in httpOnly Cookies (Best of Both)
// Set JWT as httpOnly cookie — not in response body
function setAuthCookies(res: Response, user: User) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role, tenantId: user.tenantId },
process.env.JWT_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d', algorithm: 'HS256' }
);
res.cookie('__Host-access', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
path: '/',
});
res.cookie('__Host-refresh', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/api/auth/refresh', // Only sent to refresh endpoint
});
}
OAuth 2.0 / OIDC Implementation
AI generates OAuth flows that skip critical security steps: no state parameter, no PKCE, no token validation.
Authorization Code Flow with PKCE
import crypto from 'crypto';
// Step 1: Generate authorization URL
app.get('/api/auth/google', (req, res) => {
// PKCE: Generate code verifier and challenge
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// State parameter prevents CSRF
const state = crypto.randomBytes(16).toString('hex');
// Store both in session
req.session.oauthState = state;
req.session.codeVerifier = codeVerifier;
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
response_type: 'code',
scope: 'openid email profile',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
access_type: 'offline', // Get refresh token
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
// Step 2: Handle callback
app.get('/api/auth/google/callback', async (req, res) => {
const { code, state } = req.query;
// CRITICAL: Verify state parameter
if (state !== req.session.oauthState) {
return res.status(403).json({ error: 'Invalid state parameter' });
}
// Exchange code for tokens
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
code: code as string,
grant_type: 'authorization_code',
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
code_verifier: req.session.codeVerifier, // PKCE verification
}),
});
const tokens = await tokenResponse.json();
// CRITICAL: Verify the ID token
const idToken = jwt.decode(tokens.id_token, { complete: true });
// In production, verify the signature using Google's public keys
// and check iss, aud, exp claims
// Clean up session
delete req.session.oauthState;
delete req.session.codeVerifier;
// Create or update user
const user = await findOrCreateUser({
email: idToken.payload.email,
name: idToken.payload.name,
googleId: idToken.payload.sub,
});
setAuthCookies(res, user);
res.redirect('/dashboard');
});
RBAC (Role-Based Access Control)
// Define permissions per role
const ROLE_PERMISSIONS = {
viewer: ['read:own'],
editor: ['read:own', 'write:own'],
admin: ['read:any', 'write:any', 'delete:any', 'manage:users'],
owner: ['read:any', 'write:any', 'delete:any', 'manage:users', 'manage:billing'],
} as const;
type Role = keyof typeof ROLE_PERMISSIONS;
type Permission = typeof ROLE_PERMISSIONS[Role][number];
function requirePermission(...required: Permission[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const userPermissions = ROLE_PERMISSIONS[req.user.role as Role] || [];
const hasAll = required.every(p => userPermissions.includes(p));
if (!hasAll) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.get('/api/users', requirePermission('read:any'), listAllUsers);
app.delete('/api/users/:id', requirePermission('delete:any'), deleteUser);
app.get('/api/orders', requirePermission('read:own'), listOwnOrders);
ABAC (Attribute-Based Access Control)
For complex authorization that depends on resource attributes, not just roles.
interface AccessContext {
user: { id: string; role: string; tenantId: string; department: string };
resource: { ownerId: string; tenantId: string; status: string };
action: 'read' | 'write' | 'delete';
}
type Policy = (ctx: AccessContext) => boolean;
const policies: Policy[] = [
// Users can read their own resources
(ctx) => ctx.action === 'read' && ctx.resource.ownerId === ctx.user.id,
// Admins can do anything within their tenant
(ctx) => ctx.user.role === 'admin' && ctx.resource.tenantId === ctx.user.tenantId,
// Editors can write to draft resources in their tenant
(ctx) =>
ctx.user.role === 'editor' &&
ctx.action === 'write' &&
ctx.resource.status === 'draft' &&
ctx.resource.tenantId === ctx.user.tenantId,
// Nobody can delete published resources except owners
(ctx) =>
ctx.action === 'delete' &&
ctx.resource.status === 'published' &&
ctx.user.role === 'owner',
];
function isAuthorized(ctx: AccessContext): boolean {
return policies.some(policy => policy(ctx));
}
// Usage in route handler
app.put('/api/documents/:id', requireAuth, async (req, res) => {
const document = await getDocument(req.params.id);
if (!document) return res.status(404).json({ error: 'Not found' });
const authorized = isAuthorized({
user: req.user,
resource: { ownerId: document.ownerId, tenantId: document.tenantId, status: document.status },
action: 'write',
});
if (!authorized) {
return res.status(403).json({ error: 'Access denied' });
}
await updateDocument(req.params.id, req.body);
res.json({ success: true });
});
CSRF Protection
import csrf from 'csurf';
// For session-based auth with cookies
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict',
},
});
// Apply to state-changing routes
app.post('/api/*', csrfProtection);
app.put('/api/*', csrfProtection);
app.delete('/api/*', csrfProtection);
// Provide token to the client
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Alternative: Double-submit cookie pattern
// Set a random token in a non-httpOnly cookie
// Require it in a custom header — CSRF attacks can't read cookies from another origin
Multi-Tenancy Auth Middleware
// Ensure every request is scoped to the correct tenant
function requireTenant(req: AuthRequest, res: Response, next: NextFunction) {
if (!req.user?.tenantId) {
return res.status(403).json({ error: 'No tenant context' });
}
// Prevent tenant ID manipulation via URL params
if (req.params.tenantId && req.params.tenantId !== req.user.tenantId) {
// Log this — it's likely an attack attempt
logger.warn({
msg: 'Tenant ID mismatch',
userId: req.user.id,
claimedTenant: req.params.tenantId,
actualTenant: req.user.tenantId,
});
return res.status(403).json({ error: 'Access denied' });
}
// Inject tenant context for downstream use
req.tenantId = req.user.tenantId;
next();
}
app.use('/api/tenant/:tenantId/*', requireAuth, requireTenant);
Common AI Auth Mistakes
| Mistake | Risk | Fix |
|---|---|---|
| JWT in localStorage | XSS steals tokens | httpOnly cookies |
| No token expiry | Stolen tokens work forever | 15-minute access tokens |
| Same error for missing vs expired token | Breaks client refresh logic | Distinct error codes |
| Role stored only in JWT | Can't revoke role changes | Check DB on sensitive operations |
| No session regeneration on login | Session fixation attack | req.session.regenerate() |
| OAuth without state parameter | CSRF on login flow | Random state, verify on callback |
| Password in JWT payload | Token decode exposes it | Never put secrets in JWTs |
Auth is the foundation of your application's security. If the auth is wrong, nothing else matters. Review every line AI generates for authentication and authorization — it will get something critical wrong.
Install this skill directly: skilldb add vibe-coding-security-skills