Auth & Security/
Lesson

In 2019, a financial technology company discovered that their customer portal had a protected admin page in React. The React router checked user.role === 'admin' before rendering the admin component. The problem: every APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. the admin page called had no server-side role check. An attacker opened the browser dev tools, found the API URLs, and called them directly with a regular user 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.. They transferred funds between accounts, changed user details, and exported customer data, all because the team treated the frontend route guard as actual security.

Frontend route protection is about user experience. Backend 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. is about security. You need both, but only the backend actually stops attackers.

Frontend route guards

Frontend route guards prevent unauthorized users from seeing pages they should not access. They redirect users to a login page or show an error. But they run in the browser, which the user fully controls.

Here is a typical React Router guard:

// React route guard - UX only, not security
function ProtectedRoute({ children, requiredRole }) {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/login" />;
  }

  if (requiredRole && user.role !== requiredRole) {
    return <Navigate to="/unauthorized" />;
  }

  return children;
}

// Usage in route configuration
<Route
  path="/admin"
  element={
    <ProtectedRoute requiredRole="admin">
      <AdminDashboard />
    </ProtectedRoute>
  }
/>

This is good UX, regular users are not shown admin pages. But an attacker can bypass this entirely by calling the APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. endpoints directly, modifying the JavaScript in dev tools, or manipulating the user object in browser state.

Frontend guards doFrontend guards do not
Hide UI elements the user should not seePrevent API calls from unauthorized users
Redirect unauthenticated users to loginStop attackers who use curl or Postman
Improve navigation flowEnforce access control on data
Reduce confusion for legitimate usersReplace server-side authorization
AI pitfall
When you prompt AI to "protect the admin page," it generates a frontend route guard and nothing else. It treats the React component as the security boundary. You must separately prompt for backend middleware on every API endpoint that the admin page calls.
02

Backend 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. chains

The server is the real security boundary. Middleware functions run in sequence before the route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes.. Each middleware either passes the request to the next function or rejects it.

A robust middleware chain has three layers:

// Layer 1: Authentication - is this a valid user?
// Layer 2: Role authorization - does this user's role allow this action?
// Layer 3: Resource authorization - does this user own or have access to this specific resource?

app.put('/api/posts/:id',
  authenticate,                              // Layer 1
  requirePermission('posts', 'update'),      // Layer 2
  requireOwnership('posts', 'authorId'),     // Layer 3
  updatePostHandler                          // Business logic
);

Each layer depends on the one before it. You cannot check roles without first knowing who the user is. You cannot check ownership without first confirming the user has the right role.

Here is what the ownership middleware looks like:

function requireOwnership(table, ownerField) {
  return async (req, res, next) => {
    const resourceId = req.params.id;
    const resource = await db[table].findById(resourceId);

    if (!resource) {
      return res.status(404).json({ error: 'Not found' });
    }

    // Admins bypass ownership check
    if (req.user.role === 'admin') {
      req.resource = resource;
      return next();
    }

    // Everyone else must own the resource
    if (resource[ownerField] !== req.user.id) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    req.resource = resource;
    next();
  };
}

This middleware is reusable across every route that needs ownership verification. You write it once and apply it declaratively.

03

Deny by default

"Deny by default" means every route is protected unless you explicitly mark it as public. This is the opposite of how most applications start, where routes are open by default and developers add authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. as an afterthought.

The implementation uses a global 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. that rejects unauthenticated requests, combined with a whitelist of public routes:

const PUBLIC_ROUTES = [
  'POST /auth/login',
  'POST /auth/register',
  'GET /health',
];

// Global middleware: deny by default
app.use((req, res, next) => {
  const route = `${req.method} ${req.path}`;
  if (PUBLIC_ROUTES.includes(route)) {
    return next(); // Explicitly public
  }
  // Everything else requires authentication
  return authenticate(req, res, next);
});

With deny-by-default, a developer who adds a new route without thinking about authentication gets a 401 error immediately. The secure path is the default path. This is dramatically safer than the alternative, where forgetting to add middleware means the route is open to the world.

ApproachNew route without middlewareRisk
Allow by defaultRoute is publicly accessibleHigh, silent security hole
Deny by defaultRoute returns 401Low, developer notices immediately
AI pitfall
AI never generates deny-by-default architectures. It adds authenticate middleware to individual routes, which means any route where the middleware is forgotten is completely unprotected. When you review an AI-generated Express app with 30 routes, check whether there is a global auth middleware or if each route adds its own.
04

AI security holes: a catalog

Here are the most common security gaps in AI-generated code, organized by how dangerous they are:

Frontend-only protection

// AI generates this:
// Frontend
{user.isAdmin && <button onClick={deleteUser}>Delete User</button>}

// Backend - AI forgets this:
app.delete('/api/users/:id', authenticate, async (req, res) => {
  // No role check! Any authenticated user can delete any user
  await db.users.delete(req.params.id);
  res.json({ success: true });
});

The button is hidden, but the endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. is wide open. An attacker finds it in the network tab or by reading the frontend source code.

Missing 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. on related routes

AI often protects the "main" route but forgets related ones:

// AI protects the update route
app.put('/api/posts/:id', authenticate, requireAdmin, updatePost);

// But forgets to protect the bulk operation
app.post('/api/posts/bulk-delete', authenticate, bulkDeletePosts);
// Missing requireAdmin! Any authenticated user can bulk-delete.

Role check with wrong logic

// AI-generated: logical error in role check
if (user.role !== 'admin' || user.role !== 'editor') {
  return res.status(403).json({ error: 'Forbidden' });
}

This condition is always true, every user's role is either not "admin" or not "editor" (or both). The correct operator is &&, not ||. This is a classic AI logic error that blocks everyone, including admins and editors. The inverse mistake, using && where || is needed, lets everyone through.

Middleware order errors

// Wrong order: authorization runs before authentication
app.delete('/api/posts/:id',
  requirePermission('posts', 'delete'), // Checks role - but req.user doesn't exist yet!
  authenticate,                          // Sets req.user - too late
  deletePostHandler
);

The permission check runs first and reads req.user, which is undefined because authenticate has not run yet. Depending on the implementation, this either crashes (good, you notice) or silently allows the request (bad, security hole).

05

Putting it all together

A well-structured access control system combines everything from this moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions.:

// 1. Deny by default (global)
app.use(globalAuthMiddleware);

// 2. Public routes are explicitly whitelisted
app.post('/auth/login', loginHandler);
app.post('/auth/register', registerHandler);

// 3. Role-based access (RBAC) via middleware
app.get('/api/admin/users', requirePermission('users', 'manage'), listUsersHandler);

// 4. Attribute-based access (ABAC) via ownership
app.put('/api/posts/:id',
  requirePermission('posts', 'update'),
  requireOwnership('posts', 'authorId'),
  updatePostHandler
);

// 5. Frontend route guards for UX (supplementary, not security)
<ProtectedRoute requiredRole="admin">
  <AdminDashboard />
</ProtectedRoute>

Each layer reinforces the others. The frontend provides a clean user experience. The backend enforces every rule regardless of how the request arrives. RBACWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions. handles broad permissions. ABACWhat is abac?Attribute-Based Access Control - an authorization model that makes decisions based on attributes of the user, resource, and environment rather than fixed roles. handles resource-specific rules. Deny-by-default catches anything you forgot.

06

Access control review checklist

When reviewing AI-generated code for access control issues, walk through this list:

  • Does every APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. have authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. 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.?
  • Does every endpoint that requires specific roles have authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. middleware?
  • Are ownership checks present on endpoints that modify user-specific resources?
  • Is there a global deny-by-default middleware, or is each route individually protected?
  • Do frontend route guards have corresponding backend middleware on every API call?
  • Is middleware applied in the correct order (authenticate → authorize → handle)?
  • Are role strings defined as constants, not hardcoded in each route?
  • Does the system return 401 for authentication failures and 403 for authorization failures?
  • Are bulk operations protected with the same middleware as individual operations?