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…
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: 368 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
Related Skills
Container and Deployment Security
AI-generated Dockerfiles run everything as root, use latest tags, embed secrets in environment variables, install unnecessary tools, and skip health checks.…
Credential Management
AI-generated code loves hardcoded secrets. API keys inline, database passwords in config files, tokens committed to git. The AI doesn't understand that the…
Database Security Hardening
AI-generated database code is functionally correct and almost always insecure. It concatenates strings into SQL queries, connects as root, skips TLS, and…
Dependency Supply Chain Security
AI coding tools install packages without thinking. "Add a date library" becomes npm install moment — a 4.2MB package with known vulnerabilities, when date-fns…
Error Handling and Information Leakage
AI-generated error handling is designed for debugging, not production. It returns full stack traces to the client, logs passwords in request bodies, exposes…
Input Validation Patterns
AI-generated code trusts user input by default. It parses JSON without schema validation, builds queries from URL parameters, accepts file uploads without…