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.
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: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 |
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.Creating an Authenticated Fetch Helper
Rather than manually attaching the token to every request, wrapfetch in a reusable helper:
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.jsProtected 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.jsxUsing Private Routes
Wrap your protected routes with thePrivateRoute component. Any <Route> nested inside inherits the authentication check automatically.
App.jsx
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:
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.Common Authentication Pitfalls
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 |
Next Steps
In the next chapter, you’ll learn about Redux & State Management — managing complex global state with Redux Toolkit!
Interview Deep-Dive
Compare storing JWT tokens in localStorage versus HttpOnly cookies. What are the actual attack vectors, and what would you recommend for a production app?
Compare storing JWT tokens in localStorage versus HttpOnly cookies. What are the actual attack vectors, and what would you recommend for a production app?
How do you implement a token refresh flow in a React SPA? What happens when multiple API calls hit a 401 simultaneously?
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 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.
/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:What security considerations should a frontend developer think about when implementing authentication? What is NOT the frontend's responsibility?
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.