Documentation Index
Fetch the complete documentation index at: https://mintlify.com/jorgemarazba/Front-end/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Authentication is critical for securing your application and managing user access. This guide covers modern authentication patterns, JWT handling, protected routes, and best practices.Authentication Strategies
- JWT (Recommended)
- Session-Based
- OAuth/Social Login
JSON Web Tokens are stateless and work well with modern APIs.Pros:
- Stateless authentication
- Works across multiple domains
- Mobile-friendly
- Scalable
- Cannot revoke tokens before expiry
- Tokens can be large
- Need refresh token strategy
Traditional approach using server-side sessions.Pros:
- Easy to revoke
- Server controls session
- Familiar pattern
- Requires server state
- CORS complexity
- Scaling challenges
Delegate authentication to third-party providers.Pros:
- No password management
- Better UX
- Trusted providers
- External dependency
- Limited control
- Privacy concerns
Setting Up Authentication Context
Create Auth Context
AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
interface User {
id: string;
email: string;
name: string;
role: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
register: (email: string, password: string, name: string) => Promise<void>;
isLoading: boolean;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check if user is logged in on mount
checkAuth();
}, []);
const checkAuth = async () => {
try {
const token = localStorage.getItem('accessToken');
if (!token) {
setIsLoading(false);
return;
}
const response = await fetch('/api/auth/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
localStorage.removeItem('accessToken');
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setIsLoading(false);
}
};
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
const { user, accessToken, refreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setUser(user);
};
const logout = async () => {
try {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
},
});
} catch (error) {
console.error('Logout failed:', error);
} finally {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
setUser(null);
}
};
const register = async (email: string, password: string, name: string) => {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
});
if (!response.ok) {
throw new Error('Registration failed');
}
const { user, accessToken, refreshToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setUser(user);
};
return (
<AuthContext.Provider
value={{
user,
login,
logout,
register,
isLoading,
isAuthenticated: !!user,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Wrap App with Provider
App.tsx
import { AuthProvider } from './AuthContext';
function App() {
return (
<AuthProvider>
<YourApp />
</AuthProvider>
);
}
Use in Components
Profile.tsx
import { useAuth } from './AuthContext';
export function Profile() {
const { user, logout, isLoading } = useAuth();
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>Not logged in</div>;
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>{user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
Store sensitive tokens in httpOnly cookies when possible. Use localStorage only for non-sensitive data or when cookies aren’t an option.
Login Form Implementation
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useAuth } from './AuthContext';
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(1, 'Password is required'),
});
type LoginFormData = z.infer<typeof loginSchema>;
export function LoginForm() {
const { login } = useAuth();
const navigate = useNavigate();
const [error, setError] = useState<string | null>(null);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginFormData) => {
try {
setError(null);
await login(data.email, data.password);
navigate('/dashboard');
} catch (err) {
setError('Invalid email or password');
}
};
return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">Sign In</h2>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded">
<p className="text-red-800 text-sm">{error}</p>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Email
</label>
<input
type="email"
{...register('email')}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Password
</label>
<input
type="password"
{...register('password')}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
)}
</div>
<div className="flex items-center justify-between">
<label className="flex items-center">
<input type="checkbox" className="rounded border-gray-300" />
<span className="ml-2 text-sm text-gray-600">Remember me</span>
</label>
<a href="/forgot-password" className="text-sm text-blue-600 hover:underline">
Forgot password?
</a>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</button>
</form>
<p className="mt-4 text-center text-sm text-gray-600">
Don't have an account?{' '}
<a href="/register" className="text-blue-600 hover:underline">
Sign up
</a>
</p>
</div>
);
}
Protected Routes
ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string;
}
export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
const { user, isLoading, isAuthenticated } = useAuth();
const location = useLocation();
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRole && user?.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
}
routes.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ProtectedRoute } from './ProtectedRoute';
function AppRoutes() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginForm />} />
<Route path="/register" element={<RegisterForm />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
}
Always validate permissions on the server. Client-side protection is for UX only and can be bypassed.
Token Refresh Pattern
api-client.ts
let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];
function subscribeTokenRefresh(callback: (token: string) => void) {
refreshSubscribers.push(callback);
}
function onTokenRefreshed(token: string) {
refreshSubscribers.forEach((callback) => callback(token));
refreshSubscribers = [];
}
async function refreshAccessToken(): Promise<string> {
const refreshToken = localStorage.getItem('refreshToken');
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
throw new Error('Token refresh failed');
}
const { accessToken } = await response.json();
localStorage.setItem('accessToken', accessToken);
return accessToken;
}
export async function authenticatedFetch(url: string, options: RequestInit = {}) {
let accessToken = localStorage.getItem('accessToken');
const makeRequest = async (token: string) => {
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
};
let response = await makeRequest(accessToken!);
// If token expired, refresh and retry
if (response.status === 401) {
if (!isRefreshing) {
isRefreshing = true;
try {
const newToken = await refreshAccessToken();
isRefreshing = false;
onTokenRefreshed(newToken);
response = await makeRequest(newToken);
} catch (error) {
isRefreshing = false;
throw error;
}
} else {
// Wait for token refresh to complete
const newToken = await new Promise<string>((resolve) => {
subscribeTokenRefresh(resolve);
});
response = await makeRequest(newToken);
}
}
return response;
}
Token refresh logic should handle race conditions when multiple requests fail simultaneously. Queue requests during refresh.
Social Authentication
SocialLogin.tsx
export function SocialLogin() {
const handleGoogleLogin = () => {
window.location.href = '/api/auth/google';
};
const handleGitHubLogin = () => {
window.location.href = '/api/auth/github';
};
return (
<div className="space-y-3">
<button
onClick={handleGoogleLogin}
className="w-full flex items-center justify-center gap-3 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
<img src="/google-icon.svg" alt="" className="w-5 h-5" />
Continue with Google
</button>
<button
onClick={handleGitHubLogin}
className="w-full flex items-center justify-center gap-3 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
<img src="/github-icon.svg" alt="" className="w-5 h-5" />
Continue with GitHub
</button>
</div>
);
}
Best Practices
- Never store sensitive data in localStorage (use httpOnly cookies when possible)
- Always use HTTPS in production
- Implement CSRF protection for session-based auth
- Use secure, httpOnly cookies for tokens
- Implement rate limiting on auth endpoints
- Add multi-factor authentication for sensitive operations
- Log authentication events for security monitoring
- Use strong password requirements
- Implement account lockout after failed attempts
- Provide password reset functionality
Next Steps
- Secure API calls with Data Fetching patterns
- Build auth forms with Form Handling
- Deploy securely with Production Setup