Frontend Engineering/
Lesson

You asked AI to add authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. to your app. It generated a login form, a JWTWhat is jwt?JSON Web Token - a self-contained, signed token that carries user data (like user ID and role). The server can verify it without a database lookup.-based auth middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it., and a protected /dashboard route. Everything worked. Then you asked it to add an admin panel. The AI generated the admin page, added a conditional render ({user.role === 'admin' && <AdminPanel />}), and moved on. No server-side authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. check. Any authenticated user who sends a direct APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. request to the admin endpoints gets full access. This is the most common auth mistake in AI-generated code, it implements authentication but forgets that authorization is a completely separate step.

The core distinction

Think of a hotel. When you check in, the front desk verifies your identity, your name, your booking, your ID. That's authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.: proving who you are. But your key card only opens your room, not the room next door, not the executive lounge, not the supply closet. That access control is authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages.: determining what you're allowed to do given who you are.

ConceptQuestion it answersExampleHTTP status on failure
Authentication (AuthN)Who are you?Login with email/password401 Unauthorized
Authorization (AuthZ)What can you do?Can this user delete other users?403 Forbidden

The order matters: you always authenticate first, then authorize. You can't make access control decisions for someone whose identity you haven't verified.

02

AuthenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. methods

Password-based authentication

The most common approach. A user submits credentials, you verify them against your database, and you issue something (a tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. or sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request.) that proves they authenticated:

app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  const user = await db.findUserByEmail(email);
  if (!user) {
    // Return the same error for missing user and wrong password
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign(
    { userId: user.id, role: user.role },
    SECRET,
    { expiresIn: '1h' }
  );
  res.json({ token });
});

Notice that both "user not found" and "wrong password" return the same error message. If you returned different messages, attackers could use your login form to discover which email addresses have accounts (account enumeration).

AI pitfall
AI-generated login handlers frequently return different error messages for "user not found" vs "wrong password." This seems helpful but enables account enumeration, attackers can discover valid email addresses by watching which error message comes back. Always return the same generic "Invalid credentials" for both cases.

JWTWhat is jwt?JSON Web Token - a self-contained, signed token that carries user data (like user ID and role). The server can verify it without a database lookup. vs sessions

FeatureJWTSessions
State storedIn the token (client-side)On the server (Redis, DB)
ScalabilityEasy, no shared stateRequires shared session store
RevocationHard, valid until expiryEasy, delete the session record
Token theft riskHigh if long-livedLower with HttpOnly cookies
Best forMicroservices, APIsTraditional web apps

JWTs are statelessWhat is stateless?A design where each request contains all the information the server needs, so any server can handle any request without remembering previous ones., the server doesn't store anything. This makes them easy to scale across multiple servers. The downside is that you can't invalidate a JWT before it expires. If a token is stolen, it remains valid until its expiry time. Keep expiry times short (15-60 minutes) and use refresh tokens for longer sessions.

Never put sensitive information in a JWT payload. The payload is base64-encoded, not encrypted, anyone with the token can decode and read it. User IDs and roles are fine; passwords, credit card numbers, and secrets are not.

Session-based authentication

Sessions store authentication state on the server. The client gets a session ID (usually as a cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions.), and the server looks up the full session data on each request:

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,
    httpOnly: true,
    maxAge: 3600000
  }
}));

Sessions are easier to invalidate (just delete the session record), but they require shared storage (Redis or a database) when you run multiple server instances.

03

AuthorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages.: the part AI forgets

Role-based access controlWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions. (RBAC)

Assign users to roles, and assign permissions to roles:

function requireRole(...allowedRoles) {
  return (req, res, next) => {
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

// Any authenticated user
app.get('/api/profile', authenticate, (req, res) => { /* ... */ });

// Admins only - server enforces this, not just the UI
app.delete('/api/users/:id', authenticate, requireRole('admin'), (req, res) => {
  // ...
});

Resource-based authorization

RBAC handles role-level permissions, but many applications also need resource-level checks, a user can edit their own posts but not other people's posts:

app.put('/api/posts/:id', authenticate, async (req, res) => {
  const post = await db.getPost(req.params.id);

  const isOwner = post.authorId === req.user.userId;
  const isAdmin = req.user.role === 'admin';

  if (!isOwner && !isAdmin) {
    return res.status(403).json({ error: 'Cannot edit this post' });
  }

  await db.updatePost(req.params.id, req.body);
  res.json({ success: true });
});
AI pitfall
When AI generates CRUD endpoints, it almost always adds authentication middleware but skips resource ownership checks. The result: any logged-in user can edit or delete any other user's content by changing the ID in the URL. This is called horizontal privilege escalation, and it's one of the OWASP Top 10 vulnerabilities. Always verify that the authenticated user owns or has permission to access the specific resource.
04

The middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. pattern

Express (and most backend frameworks) let you compose authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. and authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. as middleware:

// Authentication middleware: verifies identity
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Authorization middleware: checks permissions
function requireRole(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

The key insight: authenticate sets req.user. requireRole reads req.user. They must run in sequence.

05

Common mistakes

MistakeWhat AI generatesActual risk
Client-only role check{user.role === 'admin' && <AdminPanel />}Attacker calls admin API directly
No ownership checkAny user can edit/delete any resourceHorizontal privilege escalation
Different error messages"User not found" vs "Wrong password"Account enumeration
JWT secret in codeconst SECRET = 'mysecret123'Token forgery if code is exposed
No token expiryjwt.sign(payload, secret) without expiresInStolen tokens valid forever

The golden rule: all authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. checks must happen on the server, on every request, for every resource. Client-side checks are user experience improvements, not security.

javascript
// Complete Auth + Authz pattern

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();

const SECRET = process.env.JWT_SECRET;

// Authentication middleware
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Authorization middleware
function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

// Login, same error message for both cases
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await db.findUserByEmail(email);
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const token = jwt.sign(
    { userId: user.id, role: user.role },
    SECRET,
    { expiresIn: '1h' }
  );
  res.json({ token });
});

// Admin only route, server-enforced
app.delete('/api/users/:id',
  authenticate,
  authorize('admin'),
  async (req, res) => {
    await db.deleteUser(req.params.id);
    res.json({ success: true });
  }
);

// Resource-based, owner or admin
app.put('/api/posts/:id', authenticate, async (req, res) => {
  const post = await db.getPost(req.params.id);
  const isOwner = post.authorId === req.user.userId;
  const isAdmin = req.user.role === 'admin';
  if (!isOwner && !isAdmin) {
    return res.status(403).json({ error: 'Cannot edit this post' });
  }
  await db.updatePost(req.params.id, req.body);
  res.json({ success: true });
});