Skip to main content
Technology & EngineeringVibe Coding Security369 lines

Authentication and Authorization Patterns

Quick Summary3 lines
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 lines
Paste into your CLAUDE.md or agent config

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.

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

FactorSession CookiesJWTs
StorageServer (Redis/DB)Client (cookie/header)
RevocationInstant (delete from store)Hard (need blocklist)
ScalabilityNeeds shared session storeStateless — any server can verify
XSS riskLow (httpOnly cookie)High if in localStorage
CSRF riskNeeds CSRF tokenNot needed if in Authorization header
Best forTraditional web appsAPIs, 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

MistakeRiskFix
JWT in localStorageXSS steals tokenshttpOnly cookies
No token expiryStolen tokens work forever15-minute access tokens
Same error for missing vs expired tokenBreaks client refresh logicDistinct error codes
Role stored only in JWTCan't revoke role changesCheck DB on sensitive operations
No session regeneration on loginSession fixation attackreq.session.regenerate()
OAuth without state parameterCSRF on login flowRandom state, verify on callback
Password in JWT payloadToken decode exposes itNever 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

Get CLI access →