> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 11. Authentication & Protected Routes

> Integrate JWT authentication in React and create protected routes.

# Authentication & Protected Routes

Authentication in a React app is like a bouncer at a club: the server issues a wristband (a token) when you prove who you are, and on every subsequent visit to a VIP area (protected route), the bouncer checks whether you are wearing that wristband. If you are not, you get redirected to the door (the login page).

In this chapter, we will learn how to consume a JWT authentication API, manage auth state globally, and protect routes so only authenticated users can access them.

## How JWT Authentication Works

Before jumping into code, here is the full flow at a glance:

```
┌─────────────────────────────────────────────────────────────┐
│                  JWT Authentication Flow                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. User submits email + password                           │
│         │                                                   │
│         ▼                                                   │
│  2. Server validates credentials                            │
│         │                                                   │
│         ▼                                                   │
│  3. Server returns a signed JWT (JSON Web Token)            │
│         │                                                   │
│         ▼                                                   │
│  4. Client stores the token (localStorage / cookie)         │
│         │                                                   │
│         ▼                                                   │
│  5. Client sends token in headers for every API request     │
│         │                                                   │
│         ▼                                                   │
│  6. Server verifies token and returns protected data        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

## Storing the Token

When the user logs in, the server sends a JWT. We need to store this token so it persists across page refreshes.

### Storage Options

| Storage          | Pros                               | Cons                                           |
| ---------------- | ---------------------------------- | ---------------------------------------------- |
| `localStorage`   | Persists across tabs and refreshes | Vulnerable to XSS attacks                      |
| `sessionStorage` | Cleared when tab closes            | Lost on new tabs                               |
| HttpOnly Cookie  | Not accessible via JS (XSS-safe)   | Requires server-side setup, vulnerable to CSRF |

<Warning>
  **Security tip**: For production apps, HttpOnly cookies set by the server are the safest option because JavaScript cannot read them, which eliminates XSS token theft. For learning and prototypes, `localStorage` is fine.
</Warning>

```javascript theme={null}
const login = async (email, password) => {
  const res = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password })
  });
  
  const data = await res.json();
  
  if (data.token) {
    // Store the token so it survives page refreshes.
    // The token is a signed string like "eyJhbG..." that
    // the server will verify on every protected request.
    localStorage.setItem('token', data.token);
  }
};
```

## Sending the Token

For every subsequent request to a protected route, we must include the token in the headers. Think of this like flashing your ID badge at every secure door in an office building.

```javascript theme={null}
const token = localStorage.getItem('token');

const res = await fetch('/api/protected', {
  headers: {
    // The "Bearer" prefix is the standard convention.
    // Some APIs use a custom header like 'x-auth-token' instead.
    'Authorization': `Bearer ${token}`
  }
});
```

### Creating an Authenticated Fetch Helper

Rather than manually attaching the token to every request, wrap `fetch` in a reusable helper:

```javascript theme={null}
// api.js - a thin wrapper that auto-attaches the auth token
async function authFetch(url, options = {}) {
  const token = localStorage.getItem('token');

  const res = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      // Spread any custom headers the caller provides
      ...options.headers,
      // Attach the token if one exists
      ...(token ? { Authorization: `Bearer ${token}` } : {})
    }
  });

  // If the server responds with 401, the token is expired or invalid.
  // Clear it and redirect to login so the user can re-authenticate.
  if (res.status === 401) {
    localStorage.removeItem('token');
    window.location.href = '/login';
  }

  return res;
}

// Usage throughout your app
const users = await authFetch('/api/users').then(r => r.json());
```

<Tip>
  **Practical tip**: Centralizing token handling in one place means you only need to update the logic once when requirements change (e.g., switching from localStorage to cookies, or adding token refresh).
</Tip>

## Auth Context

We can manage the authentication state globally using Context API. This way every component in the tree -- navigation bars, protected routes, profile pages -- can check "is the user logged in?" without prop drilling.

Think of the Auth Context as a building-wide PA system: when the user logs in or out, every room (component) that cares about identity hears the announcement instantly.

**AuthContext.js**

```javascript theme={null}
import { createContext, useState, useEffect, useContext } from 'react';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  // Start as `true` because we need to check for a saved token
  // before we know if the user is authenticated or not.
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // On mount, check if a token already exists (e.g., the user
    // refreshed the page). If it does, validate it with the server.
    const token = localStorage.getItem('token');
    if (token) {
      // In production, you should verify the token with your
      // backend (e.g., GET /api/auth/me) to confirm it has not
      // expired or been revoked. For now we trust its presence.
      setUser({ token }); 
    }
    setLoading(false);
  }, []);

  const login = (token) => {
    localStorage.setItem('token', token);
    setUser({ token });
  };

  const logout = () => {
    localStorage.removeItem('token');
    setUser(null);
  };

  return (
    // Render children only after the auth check finishes.
    // This prevents a flash of the login page before the
    // saved token is read from localStorage.
    <AuthContext.Provider value={{ user, login, logout, loading }}>
      {!loading && children}
    </AuthContext.Provider>
  );
};

// Custom hook -- always prefer this over exporting the context directly.
// It gives you a single place to add error handling if the provider is missing.
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};
```

<Warning>
  **Common pitfall -- stale auth state after token expiry**: If a user stays on the page for hours, their JWT may expire server-side while `user` in state still looks valid. Protect against this by:

  1. Checking the HTTP response status in your fetch helper (401 means "token expired").
  2. Adding a timer that checks token expiry client-side using the JWT `exp` claim.
  3. Implementing a refresh token flow for seamless re-authentication.
</Warning>

## Protected Routes

We can create a wrapper component to protect routes that require authentication. This acts like a security checkpoint -- if the user has a valid wristband (token), they pass through; otherwise they get redirected to the entrance (login page).

**PrivateRoute.jsx**

```javascript theme={null}
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';

const PrivateRoute = () => {
  const { user, loading } = useAuth();
  const location = useLocation();

  // While checking auth state, show a loading indicator instead of
  // flashing the login page. This prevents a jarring UX on page refresh.
  if (loading) return <div>Loading...</div>;

  // If not authenticated, redirect to login.
  // Save the current location in `state` so we can redirect back
  // after the user successfully logs in.
  return user
    ? <Outlet />
    : <Navigate to="/login" state={{ from: location }} replace />;
};

export default PrivateRoute;
```

## Using Private Routes

Wrap your protected routes with the `PrivateRoute` component. Any `<Route>` nested inside inherits the authentication check automatically.

**App.jsx**

```javascript theme={null}
import { Routes, Route } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';
import Dashboard from './Dashboard';
import Login from './Login';

function App() {
  return (
    <Routes>
      {/* Public routes -- anyone can access these */}
      <Route path="/login" element={<Login />} />
      
      {/* Protected routes -- PrivateRoute checks auth first */}
      <Route element={<PrivateRoute />}>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Route>
    </Routes>
  );
}
```

### Redirecting Back After Login

When a user tries to access `/dashboard` without being logged in, we saved that location in the `Navigate` state above. Now the login page can redirect them back:

```javascript theme={null}
function LoginPage() {
  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();

  // The route the user was trying to visit before being redirected.
  // Falls back to /dashboard if they navigated to /login directly.
  const from = location.state?.from?.pathname || '/dashboard';

  const handleSubmit = async (e) => {
    e.preventDefault();
    const token = await authenticateWithServer(email, password);
    login(token);
    // Send the user to where they originally wanted to go
    navigate(from, { replace: true });
  };

  return (/* login form */);
}
```

***

## Role-Based Access Control

Many apps need more than just "logged in vs. not." You may have admins, editors, and regular users who see different things.

**Analogy**: If basic authentication is a bouncer checking IDs at the door, role-based access is the VIP section inside. You got past the door (you are authenticated), but the velvet rope (the role check) decides whether you can enter the VIP area.

```javascript theme={null}
function RoleRoute({ allowedRoles }) {
  const { user } = useAuth();

  // First check: is the user authenticated at all?
  if (!user) {
    return <Navigate to="/login" replace />;
  }

  // Second check: does the user's role match the allowed list?
  // This is an authorization check, not an authentication check.
  // The user is who they say they are -- but are they permitted?
  if (!allowedRoles.includes(user.role)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <Outlet />;
}

// Usage -- only admins and editors can access /admin
<Route element={<RoleRoute allowedRoles={['admin', 'editor']} />}>
  <Route path="/admin" element={<AdminPanel />} />
</Route>
```

<Warning>
  **Important**: Role checks on the client are a UX convenience, not a security boundary. A determined user can modify JavaScript in the browser to bypass any client-side check. Always verify roles on the server for every API request that returns or modifies sensitive data.
</Warning>

***

## Common Authentication Pitfalls

<Warning>
  **Pitfall 1 -- Storing sensitive data in the JWT payload**: JWTs are base64-encoded, not encrypted. Anyone can decode the payload. Never put passwords, credit card numbers, or secrets inside a JWT.

  **Pitfall 2 -- Not handling token expiry**: A token that lives forever is a security risk. Always set an expiration (`exp` claim) on the server side and handle 401 responses gracefully on the client.

  **Pitfall 3 -- Forgetting the loading state**: If your `PrivateRoute` renders before the auth check finishes (e.g., reading from `localStorage` in a `useEffect`), users will see a flash of the login page on every refresh. Always gate rendering on `loading === false`.
</Warning>

***

## Summary

| Concept                  | Description                                                                |
| ------------------------ | -------------------------------------------------------------------------- |
| **JWT**                  | A signed token the server issues after successful login                    |
| **localStorage**         | Simple client-side storage for tokens (use HttpOnly cookies in production) |
| **Auth Context**         | Global state that makes `user`, `login`, and `logout` available everywhere |
| **PrivateRoute**         | Wrapper component that redirects unauthenticated users to login            |
| **Redirect after login** | Save the attempted URL in `location.state` and navigate back after auth    |
| **Role-based access**    | Check `user.role` against allowed roles in a route guard                   |
| **Token expiry**         | Handle 401 responses and prompt re-authentication                          |

<Card title="Next Steps" icon="arrow-right">
  In the next chapter, you'll learn about **Redux & State Management** -- managing complex global state with Redux Toolkit!
</Card>

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="Compare storing JWT tokens in localStorage versus HttpOnly cookies. What are the actual attack vectors, and what would you recommend for a production app?">
    **Strong Answer:**
    localStorage is vulnerable to Cross-Site Scripting (XSS). If an attacker injects JavaScript into your page (through a vulnerable dependency, user-generated content, or a DOM-based XSS flaw), that script can call `localStorage.getItem('token')` and exfiltrate the token to an attacker-controlled server. Once they have the token, they can impersonate the user from any device until the token expires. The attack surface is large because any JavaScript running on your origin has full localStorage access.

    HttpOnly cookies are invisible to JavaScript -- `document.cookie` cannot read them, and no client-side API can access them. The cookie is automatically attached to HTTP requests by the browser. This eliminates XSS-based token theft entirely. However, HttpOnly cookies introduce Cross-Site Request Forgery (CSRF) risk: a malicious site can trigger a request to your API (via an image tag, form submission, or fetch from a different origin), and the browser automatically includes the cookie. You mitigate CSRF with SameSite cookie attributes (`SameSite=Strict` or `SameSite=Lax`), CSRF tokens, or checking the Origin/Referer header on the server.

    My recommendation for production: HttpOnly cookies with `Secure`, `SameSite=Lax`, and a short expiration (15-30 minutes) paired with a refresh token (also HttpOnly, longer expiration). The access token is verified on every request; the refresh token is used only at a dedicated `/refresh` endpoint to get a new access token. This way, even if a CSRF attack triggers a request, the access token expires quickly, limiting the damage window.

    For SPAs where the backend is on a different domain (common with microservices), cookies require careful CORS configuration (`credentials: 'include'`, `Access-Control-Allow-Credentials: true`). Some teams use the Backend-for-Frontend (BFF) pattern: a thin server on the same origin as the SPA handles auth cookies and proxies API requests, keeping the token flow entirely server-side.

    **Follow-up: A user reports that they are logged out after refreshing the page even though you store the token in localStorage. What could cause this?**

    Several possible causes, in order of likelihood:

    First, the auth state initialization reads from localStorage but the component renders before the useEffect that loads the token completes. The PrivateRoute sees `user === null`, redirects to login, and by the time the effect runs, the user is already on the login page. Fix: initialize the loading state as `true` and do not render protected routes until loading is false.

    Second, the token in localStorage has expired. The server rejects it on the validation API call, and your auth logic clears the token and sets user to null. Fix: implement token refresh before the access token expires (proactive refresh using a timer based on the `exp` claim).

    Third, a bug in the logout flow clears localStorage as a side effect. For example, a cleanup function in a useEffect that runs on unmount and clears the token. Or a redirect to login that triggers the login page's "clear stale tokens" logic.

    Fourth, browser privacy settings or extensions (like auto-clearing storage on tab close). Some browsers in private/incognito mode clear localStorage when the session ends.

    Debugging approach: add a console.log at the start of the auth initialization showing the localStorage value. If the token is there but the user state is null, the initialization logic has a bug. If the token is missing, something is clearing it -- search the codebase for `localStorage.removeItem('token')` and `localStorage.clear()`.
  </Accordion>

  <Accordion title="How do you implement a token refresh flow in a React SPA? What happens when multiple API calls hit a 401 simultaneously?">
    **Strong Answer:**
    The basic refresh flow: when an API response returns 401 (unauthorized), the client sends the refresh token to `/api/auth/refresh`, receives a new access token, retries the original request with the new token, and updates stored tokens. This should be transparent to the user -- they should not see a login page unless the refresh token itself has expired.

    The tricky part is the concurrent 401 problem. If the user lands on a dashboard that fires 5 parallel API calls and the access token has just expired, all 5 return 401 simultaneously. Without coordination, you would fire 5 refresh requests, which is wasteful (and some servers invalidate the refresh token after one use, causing 4 of the 5 to fail).

    The solution is a refresh token lock. When the first 401 arrives, your fetch wrapper sets a "refreshing" flag and initiates the refresh. All subsequent 401 responses during the refresh check this flag and wait (using a Promise) for the refresh to complete. Once the new token arrives, all queued requests retry with the fresh token.

    Implementation: create a module-level variable `let refreshPromise = null`. When a 401 arrives and `refreshPromise` is null, start the refresh and store the Promise. If `refreshPromise` is not null, await it. When the refresh completes (success or failure), set `refreshPromise` back to null.

    If the refresh itself fails (refresh token expired or revoked), clear all auth state and redirect to login. At this point, the queued requests should all fail gracefully -- do not retry them.

    **Follow-up: How would you proactively refresh tokens before they expire instead of waiting for a 401?**

    Decode the access token's `exp` claim (JWTs are base64-encoded, so you can read the payload without a secret: `JSON.parse(atob(token.split('.')[1]))`). Calculate the time until expiry. Set a timer to refresh the token a few minutes before it expires -- for a 15-minute token, refresh at 12 minutes.

    Use a useEffect in your AuthProvider that sets this timer when the token changes:

    ```javascript theme={null}
    useEffect(() => {
      if (!token) return;
      const { exp } = parseJwt(token);
      const msUntilExpiry = exp * 1000 - Date.now();
      const refreshAt = msUntilExpiry - 2 * 60 * 1000; // 2 min before expiry
      const timer = setTimeout(() => refreshToken(), Math.max(refreshAt, 0));
      return () => clearTimeout(timer);
    }, [token]);
    ```

    This approach is better than reactive 401 handling because: the user never experiences a failed request followed by a retry (no latency spike), the refresh happens in the background, and the UX is seamless. Keep the reactive 401 handler as a fallback for edge cases like server clock drift or network delays that cause the proactive refresh to arrive late.
  </Accordion>

  <Accordion title="What security considerations should a frontend developer think about when implementing authentication? What is NOT the frontend's responsibility?">
    **Strong Answer:**
    The frontend is responsible for: securely storing tokens (HttpOnly cookies over localStorage), implementing HTTPS everywhere (never sending tokens over HTTP), sanitizing user input to prevent XSS (the primary vector for token theft), implementing proper CORS headers to prevent unauthorized origins from reading API responses, clearing sensitive data on logout (tokens, cached user data, in-memory state), and protecting routes client-side (redirecting unauthenticated users).

    What is NOT the frontend's responsibility (but I see frontend developers trying to do it): token validation. Never verify JWT signatures in the browser. The client does not have the secret key, and even if it did, any validation logic in JavaScript can be bypassed by a malicious actor. The server must validate every token on every request. Client-side "validation" (checking the `exp` claim to show/hide UI) is UX logic, not security.

    Password hashing: never hash passwords in the browser before sending them. The HTTPS transport layer encrypts the password in transit. If you hash on the client, the hash becomes the de facto password -- an attacker who steals the hash can authenticate without knowing the original password. Server-side hashing with bcrypt/argon2 is the only correct approach.

    Rate limiting and brute force protection: this must be server-side. A client-side "lock after 5 attempts" can be bypassed by directly calling the API. The server should implement account lockout, CAPTCHA triggers, and IP-based rate limiting.

    CSRF protection: while SameSite cookies handle most cases, the server must verify the Origin header or CSRF token on state-changing requests.

    The mental model: the frontend is a convenience layer for the user. All security enforcement happens on the server. If removing the frontend entirely and calling APIs directly with curl would bypass a security measure, that measure is implemented in the wrong place.

    **Follow-up: How does Content Security Policy (CSP) help protect your React app, and what challenges does it introduce?**

    CSP is an HTTP response header that tells the browser which sources of scripts, styles, images, and other resources are allowed. It is the most effective defense against XSS because even if an attacker injects a script tag, the browser refuses to execute it if the source is not in the CSP whitelist.

    For React apps, a strict CSP typically includes: `script-src 'self'` (only scripts from your origin), `style-src 'self' 'unsafe-inline'` (inline styles are common in React), and `connect-src 'self' api.yourdomain.com` (restrict fetch/XHR to known APIs).

    Challenges: React's development mode uses `eval()` for error overlays, which requires `script-src 'unsafe-eval'` in development (never in production). CSS-in-JS libraries that inject `<style>` tags at runtime need `style-src 'unsafe-inline'` or nonce-based CSP. Dynamic script loading (analytics, third-party widgets) requires adding each vendor's domain to the CSP, which is a maintenance burden.

    The strictest approach is nonce-based CSP: the server generates a unique nonce per request, includes it in the CSP header and on each script/style tag. This allows specific inline scripts while blocking injected ones. Next.js supports this natively with the `nonce` attribute on Script components.
  </Accordion>
</AccordionGroup>
