๐Ÿณ IntuitiveFE
Login
โ† All concepts

React Auth Patterns

โฑ๏ธ ~6-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿข Auth context = building keycard system. The card reader (AuthProvider) issues a badge on login. Every door (ProtectedRoute) checks your badge before letting you in. When the badge expires, the system silently renews it at the visa office (/auth/refresh) โ€” you never notice the swap.

๐Ÿ—๏ธ

AuthProvider

Issues the badge. Stores user + token in context. One source of truth for the whole app.

๐Ÿšช

ProtectedRoute

The door reader. Checks badge before rendering. Redirects to login if missing.

๐Ÿ”„

Silent Refresh

The auto-renewal desk. Exchanges your expiring badge before it runs out โ€” no interruption.

Token storage security spectrum

localStorage
XSS: HIGHRefresh: yes
sessionStorage
XSS: HIGHRefresh: no
In-memory (React state)
XSS: LOWRefresh: no (silent refresh)โœ“ recommended
httpOnly cookie
XSS: NONERefresh: yesโœ“ recommended
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Auth state machine โ€” click to transition

๐Ÿ”“ Unauthenticated

No user session. App shows public content and login prompt.

Transitions to:

Auth Context โ€” code example
ts
1import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
2ย 
3interface User { id: string; email: string; role: 'admin' | 'user' }
4interface AuthCtx {
5 user: User | null;
6 token: string | null;
7 login: (email: string, password: string) => Promise<void>;
8 logout: () => void;
9}
10ย 
11const AuthContext = createContext<AuthCtx | null>(null);
12ย 
13// Provider โ€” place at the root of your app
14export function AuthProvider({ children }: { children: ReactNode }) {
15 const [user, setUser] = useState<User | null>(null);
16 const [token, setToken] = useState<string | null>(null);
17ย 
18 const login = useCallback(async (email: string, password: string) => {
19 const res = await fetch('/api/auth/login', {
20 method: 'POST',
21 body: JSON.stringify({ email, password }),
22 headers: { 'Content-Type': 'application/json' },
23 });
24 if (!res.ok) throw new Error('Invalid credentials');
25 const { user, accessToken } = await res.json();
26 setUser(user);
27 setToken(accessToken); // keep token in memory โ€” never localStorage
28 }, []);
29ย 
30 const logout = useCallback(() => {
31 setUser(null);
32 setToken(null);
33 }, []);
34ย 
35 return (
36 <AuthContext.Provider value={{ user, token, login, logout }}>
37 {children}
38 </AuthContext.Provider>
39 );
40}
41ย 
42// Hook โ€” every component that needs auth calls this
43export function useAuth() {
44 const ctx = useContext(AuthContext);
45 if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>');
46 return ctx;
47}
48ย 
49// Usage in any component โ€” no props needed
50function ProfileButton() {
51 const { user, logout } = useAuth();
52 return user
53 ? <button onClick={logout}>{user.email}</button>
54 : <a href="/login">Sign in</a>;
55}

When to use

Any React app where multiple components need access to the current user. Context prevents prop drilling 5 levels deep. Combine with a hook so components never import the context object directly.

Gotcha: AuthProvider re-renders all consumers when user or token changes. If token refreshes frequently, memoize the context value with useMemo to avoid cascading re-renders across the tree.
Insight: The useAuth hook is the public API โ€” components never touch AuthContext directly. This means you can swap the underlying implementation (useState โ†’ useReducer โ†’ Zustand) without touching a single consumer.
Explored 1/5:๐Ÿ—๏ธ๐Ÿšช๐Ÿ—„๏ธ๐Ÿ”„๐Ÿ“‹
๐ŸŽฏ

Challenge

Explore all 5 React auth patterns. Step through the auth state machine to see how each pattern connects.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: Why not store JWT in localStorage?

localStorage is readable by any JavaScript on the page. An XSS vulnerability โ€” even in a third-party script โ€” lets an attacker call localStorage.getItem('token') and exfiltrate the token. httpOnly cookies are completely invisible to JS โ€” the browser sends them automatically but no script can read them. For access tokens, in-memory React state is the best compromise: invisible to XSS, no persistence needed (silent refresh on page load restores the session).

Q: Why put auth in context instead of prop-drilling?

Prop drilling auth through 5+ component levels creates tight coupling, forces intermediate components to accept props they don't use, and makes refactoring painful. Context creates a single source of truth โ€” any component calls useAuth() and gets the current user without knowing where it lives in the tree. A single setUser() in the provider propagates everywhere.

Q: When to use React Query vs raw fetch for auth?

React Query excels at data fetching with caching, retries, and background refresh โ€” but auth state has different requirements. Auth needs to survive across the whole app (not per-query), drive routing decisions, and handle token refresh as a cross-cutting concern. Use React Query for data endpoints that sit behind the auth layer. Use custom context + Axios interceptors for the auth flow itself โ€” you need precise control over the 401 retry loop and the token refresh queue.

๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

AuthContext with TypeScript generics

The context can be typed generically so different apps extend the User type without rewriting the provider. The AuthContext<T extends BaseUser> pattern lets you add role, permissions, or orgId fields at the app level while the provider stays generic.

ts
1interface BaseUser { id: string; email: string; }
2ย 
3function createAuthContext<T extends BaseUser>() {
4 const Ctx = createContext<AuthCtx<T> | null>(null);
5 function useAuth() {
6 const ctx = useContext(Ctx);
7 if (!ctx) throw new Error('useAuth outside provider');
8 return ctx;
9 }
10 return { Ctx, useAuth };
11}
12ย 
13// Per-app usage:
14interface AppUser extends BaseUser { role: 'admin' | 'viewer'; orgId: string; }
15export const { Ctx: AuthCtx, useAuth } = createAuthContext<AppUser>();

Axios request + response interceptors

The request interceptor attaches the Bearer token to every outgoing API call. The response interceptor catches 401 errors and retries with a refreshed token. The isRefreshing flag and queue prevent multiple simultaneous refresh calls when many requests expire at once.

js
1// Attach token to every request
2api.interceptors.request.use((config) => {
3 const token = tokenStore.get();
4 if (token) config.headers.Authorization = `Bearer ${token}`;
5 return config;
6});
7ย 
8// Catch 401 โ†’ refresh โ†’ retry
9api.interceptors.response.use(
10 (res) => res,
11 async (err) => {
12 if (err.response?.status === 401 && !err.config._retried) {
13 err.config._retried = true;
14 const newToken = await silentRefresh(); // calls /api/auth/refresh
15 tokenStore.set(newToken);
16 err.config.headers.Authorization = `Bearer ${newToken}`;
17 return api(err.config);
18 }
19 return Promise.reject(err);
20 }
21);

React Router v6 loader pattern for auth-gated routes

React Router v6 loaders run before the component renders. Checking auth in the loader means the redirect happens before any component mounts โ€” no flash of protected content.

js
1import { redirect } from 'react-router-dom';
2ย 
3// Loader runs before the component โ€” redirect is server-style
4async function dashboardLoader() {
5 const user = await getCurrentUser(); // check session
6 if (!user) return redirect('/login');
7 return { user }; // passed to useLoaderData()
8}
9ย 
10const router = createBrowserRouter([
11 {
12 path: '/dashboard',
13 loader: dashboardLoader,
14 element: <Dashboard />,
15 },
16]);

PKCE flow for SPAs โ€” why implicit flow is deprecated

OAuth 2.0 Implicit flow returned the access token directly in the URL fragment โ€” readable by browser history, server logs, and Referer headers. PKCE (Proof Key for Code Exchange) fixes this: the SPA generates a random code_verifier, hashes it to a code_challenge, sends the challenge to the auth server, then exchanges the authorization code for tokens by proving it knows the original verifier. The token never appears in the URL. RFC 9700 (2025) deprecates implicit flow entirely โ€” all SPAs must use PKCE.

1. Generate code_verifier (random 64 bytes)2. SHA-256 hash โ†’ code_challenge3. Redirect to /authorize?code_challenge=...4. Receive auth code (not a token)5. POST /token with code + code_verifier6. Server verifies hash โ†’ returns tokens
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

XSS can steal anything in localStorage โ€” httpOnly cookies are JS-invisible.

Memoize the context value with useMemo so only changed fields trigger consumers.

Serializes concurrent 401s into a single refresh call to prevent token rotation race conditions.

Client-side guards can be bypassed in DevTools; only the server enforces real access control.

Refresh tokens live in a DB row that can be deleted; short access token TTL limits the revocation gap.

React Query for user profile data; AuthContext for identity, routing decisions, and cross-cutting token management.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is the purpose of a Content Security Policy (CSP) header in an auth context?