Web Authentication (React)
Core Patterns
JWT Token Storage
Options and trade-offs:
| Storage | XSS Safe | CSRF Safe | Best For | |---------|----------|-----------|----------| | httpOnly cookie | Yes | No (needs CSRF token) | Most secure for tokens | | localStorage | No | Yes | Simple apps, short-lived tokens | | Memory (state) | Yes | Yes | Very short-lived tokens with refresh |
Cookie-Based Auth (Recommended)
// API client setup
const api = {
async login(email: string, password: string) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Important: send/receive cookies
body: JSON.stringify({ email, password }),
});
return response.json();
},
async logout() {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
},
async getUser() {
const response = await fetch('/api/auth/me', {
credentials: 'include',
});
if (!response.ok) throw new Error('Not authenticated');
return response.json();
},
};
Auth Context Pattern
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refresh: () => Promise<void>;
}
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check auth status on mount
checkAuth();
}, []);
async function checkAuth() {
try {
const userData = await api.getUser();
setUser(userData);
} catch {
setUser(null);
} finally {
setIsLoading(false);
}
}
async function login(email: string, password: string) {
const { user } = await api.login(email, password);
setUser(user);
}
async function logout() {
await api.logout();
setUser(null);
}
async function refresh() {
await checkAuth();
}
return (
<AuthContext.Provider
value={{
user,
isLoading,
isAuthenticated: !!user,
login,
logout,
refresh,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
OAuth Patterns
Google OAuth (Web)
// Using Google Identity Services
import { useEffect } from 'react';
declare global {
interface Window {
google: any;
}
}
function GoogleLoginButton() {
useEffect(() => {
// Load Google Identity Services script
const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
document.body.appendChild(script);
script.onload = () => {
window.google.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
callback: handleGoogleResponse,
});
window.google.accounts.id.renderButton(
document.getElementById('google-signin'),
{ theme: 'outline', size: 'large' }
);
};
return () => {
document.body.removeChild(script);
};
}, []);
async function handleGoogleResponse(response: { credential: string }) {
// Send token to backend for verification
const result = await fetch('/api/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token: response.credential }),
});
if (result.ok) {
window.location.href = '/dashboard';
}
}
return <div id="google-signin" />;
}
NextAuth.js (Recommended for Next.js)
// pages/api/auth/[...nextauth].ts or app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import CredentialsProvider from 'next-auth/providers/credentials';
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
// Verify credentials against your database
const user = await verifyCredentials(
credentials?.email,
credentials?.password
);
return user ?? null;
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
session.user.id = token.id;
return session;
},
},
};
export default NextAuth(authOptions);
// Usage in components
import { useSession, signIn, signOut } from 'next-auth/react';
function AuthButton() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <Spinner />;
}
if (session) {
return (
<div>
<span>Signed in as {session.user?.email}</span>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}
return (
<div>
<button onClick={() => signIn('google')}>Sign in with Google</button>
<button onClick={() => signIn('credentials')}>Sign in</button>
</div>
);
}
Protected Routes
React Router
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '@/contexts/auth';
function ProtectedRoute() {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <LoadingScreen />;
}
if (!isAuthenticated) {
// Redirect to login, preserving intended destination
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <Outlet />;
}
// Usage in router
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
Next.js App Router
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*'],
};
// app/dashboard/layout.tsx
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/login');
}
return <>{children}</>;
}
Token Refresh Pattern
// API client with automatic token refresh
class ApiClient {
private refreshPromise: Promise<void> | null = null;
async fetch(url: string, options: RequestInit = {}) {
const response = await fetch(url, {
...options,
credentials: 'include',
});
if (response.status === 401) {