React Authentication System
Configure JWT-based authentication for React 19 with TypeScript.
File Structure
code
src/ ├── contexts/AuthContext.tsx # Auth provider and context ├── hooks/useAuth.ts # Auth hook ├── services/authService.ts # API calls ├── types/auth.ts # Type definitions └── components/ProtectedRoute.tsx
Types (types/auth.ts)
tsx
export interface User {
id: string;
email: string;
firstName: string;
lastName: string;
role: 'USER' | 'ADMIN';
}
export interface LoginRequest {
email: string;
password: string;
}
export interface RegisterRequest {
email: string;
password: string;
firstName: string;
lastName: string;
}
export interface AuthResponse {
accessToken: string;
refreshToken: string;
expiresIn: number;
user: User;
}
export interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (credentials: LoginRequest) => Promise<void>;
register: (data: RegisterRequest) => Promise<void>;
logout: () => void;
}
Auth Service (services/authService.ts)
tsx
const API_URL = import.meta.env.VITE_API_URL;
const TOKEN_KEY = 'access_token';
const REFRESH_TOKEN_KEY = 'refresh_token';
export const authService = {
async login(credentials: LoginRequest): Promise<AuthResponse> {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
if (!response.ok) throw new Error('Invalid credentials');
const data = await response.json();
this.setTokens(data);
return data;
},
setTokens(data: AuthResponse): void {
localStorage.setItem(TOKEN_KEY, data.accessToken);
localStorage.setItem(REFRESH_TOKEN_KEY, data.refreshToken);
},
getAccessToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
},
clearTokens(): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
},
isAuthenticated(): boolean {
return !!this.getAccessToken();
},
};
Auth Context (contexts/AuthContext.tsx)
tsx
export const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check for existing session on mount
const storedUser = authService.getStoredUser();
if (storedUser) setUser(storedUser);
setIsLoading(false);
}, []);
const login = useCallback(async (credentials: LoginRequest) => {
const response = await authService.login(credentials);
setUser(response.user);
}, []);
const logout = useCallback(() => {
authService.clearTokens();
setUser(null);
}, []);
const value = useMemo(() => ({
user,
isAuthenticated: !!user,
isLoading,
login,
logout,
}), [user, isLoading, login, logout]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
useAuth Hook (hooks/useAuth.ts)
tsx
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
Protected Route (components/ProtectedRoute.tsx)
tsx
interface ProtectedRouteProps {
children: ReactNode;
requiredRole?: 'USER' | 'ADMIN';
}
export const ProtectedRoute: FC<ProtectedRouteProps> = ({ children, requiredRole }) => {
const { isAuthenticated, isLoading, user } = useAuth();
const location = useLocation();
if (isLoading) return <Spinner />;
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRole && user?.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
};
Login Form Usage
tsx
export const LoginForm: FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { login, isLoading } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
try {
await login({ email, password });
navigate('/dashboard');
} catch (err) {
setError('Invalid credentials');
}
};
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit" disabled={isLoading}>Login</button>
</form>
);
};
App Router Setup
tsx
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
} />
<Route path="/admin/*" element={
<ProtectedRoute requiredRole="ADMIN">
<AdminPage />
</ProtectedRoute>
} />
</Routes>
</AuthProvider>
</BrowserRouter>
Security Notes
- •Store access token in localStorage (short-lived)
- •Implement token refresh mechanism
- •Clear tokens on logout
- •Use HTTPS in production