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.
| Concept | Question it answers | Example | HTTP status on failure |
|---|---|---|---|
| Authentication (AuthN) | Who are you? | Login with email/password | 401 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.
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).
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
| Feature | JWT | Sessions |
|---|---|---|
| State stored | In the token (client-side) | On the server (Redis, DB) |
| Scalability | Easy, no shared state | Requires shared session store |
| Revocation | Hard, valid until expiry | Easy, delete the session record |
| Token theft risk | High if long-lived | Lower with HttpOnly cookies |
| Best for | Microservices, APIs | Traditional 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.
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.
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 });
});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.
Common mistakes
| Mistake | What AI generates | Actual risk |
|---|---|---|
| Client-only role check | {user.role === 'admin' && <AdminPanel />} | Attacker calls admin API directly |
| No ownership check | Any user can edit/delete any resource | Horizontal privilege escalation |
| Different error messages | "User not found" vs "Wrong password" | Account enumeration |
| JWT secret in code | const SECRET = 'mysecret123' | Token forgery if code is exposed |
| No token expiry | jwt.sign(payload, secret) without expiresIn | Stolen 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.
// 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 });
});