How to Test JWT Authentication and Protected Routes in React Without a Backend
How to Test JWT Authentication and Protected Routes in React Without a Backend
Implementing user authentication is one of the most critical responsibilities in modern frontend engineering. A flawed authentication flow can leak sensitive user data, expose protected routes, or cause session desynchronization where users are randomly logged out while filling out forms.
However, during early application development, the backend authentication service (OAuth providers, Auth0, Cognito, or custom microservices) is often the last component to be deployed. Frontend engineers are left unable to test login screens, password resets, role-based access control (RBAC), and private routes.
In this guide, we break down the architecture of JSON Web Tokens (JWT), compare token storage strategies, build a complete React AuthContext with token refresh interceptors, and demonstrate how to test authenticated routes using Gen-Endpoint's free JWT Auth API.
Anatomy of a JWT: Header, Payload, and Signature
A JSON Web Token is a compact, URL-safe string composed of three parts separated by periods (.):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzg0ODIwMDAwfQ.abcdef123456...
`
1. **Header (Base64Url):** Contains the token type (`JWT`) and signing algorithm (`HS256` or `RS256`).
2. **Payload (Base64Url):** Contains the token claims, such as user ID (`sub`), permissions (`role`), and expiration timestamp (`exp`).
3. **Signature:** Cryptographic hash created by the server using its private secret key to verify the payload has not been tampered with.
> **Crucial Security Reminder:** The payload of a JWT is Base64 encoded, **not encrypted**. Anyone who intercepts the token can decode its payload. Never store sensitive credentials (passwords, social security numbers, API keys) inside a JWT payload.
---
## Token Storage Strategy: LocalStorage vs. HttpOnly Cookies vs. In-Memory
Where should your frontend store the JWT? Here is how the three main approaches compare:
| Storage Mechanism | Vulnerable to XSS? | Vulnerable to CSRF? | Survives Page Refresh? | Recommended Use Case |
| :--- | :---: | :---: | :---: | :--- |
| **LocalStorage / SessionStorage** | **High** (Any injected script can read `localStorage.getItem('token')`) | Immune | Yes | Quick prototypes, non-sensitive internal tools |
| **In-Memory (React State / Variable)** | **Low** (Isolated from direct DOM inspection) | Immune | No (Resets on refresh unless refreshed silently) | Highest security for Single Page Applications |
| **HttpOnly, Secure Cookies** | **Immune** (JavaScript cannot access document.cookie) | Moderate (Requires SameSite & CSRF tokens) | Yes | **Production Standard** for web applications |
For production applications, the gold standard is **Short-Lived Access Tokens in memory** coupled with **Long-Lived Refresh Tokens stored in an `HttpOnly`, `SameSite=Lax` cookie**.
---
## Testing Login with Gen-Endpoint's Mock JWT Auth API
Gen-Endpoint provides a live JWT authentication endpoint that accepts test credentials and issues signed JWT tokens:
```bash
# 1. Log in with test credentials
curl -X POST https://gen-endpoint.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "developer@example.com",
"password": "Password123!"
}'
Sample Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJ1c3JfOTA4MSIsImVtYWlsIjoiZGV2ZWxvcGVyQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzYyMDA5MjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"user": {
"id": "usr_9081",
"email": "developer@example.com",
"name": "Jane Developer",
"role": "admin"
},
"expiresIn": 3600
}
Now you can test authenticated requests by attaching the token to the Authorization: Bearer <TOKEN> header:
# 2. Access protected profile endpoint
curl https://gen-endpoint.com/api/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
Building a Typed React AuthContext
Here is a complete, production-ready AuthContext that manages user authentication state, provides login and logout actions, and automatically injects authentication headers into outbound requests:
// context/AuthContext.tsx
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
interface AuthContextType {
user: User | null;
token: string | null;
isLoading: boolean;
login: (email: string, pass: string) => Promise<boolean>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
// Restore session from storage on client mount
useEffect(() => {
const savedToken = localStorage.getItem('auth_token');
const savedUser = localStorage.getItem('auth_user');
if (savedToken && savedUser) {
try {
setToken(savedToken);
setUser(JSON.parse(savedUser));
} catch {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
}
}
setIsLoading(false);
}, []);
const login = async (email: string, pass: string): Promise<boolean> => {
try {
const res = await fetch('https://gen-endpoint.com/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: email, password: pass }),
});
if (!res.ok) {
throw new Error('Invalid credentials');
}
const data = await res.json();
setToken(data.token);
setUser(data.user);
localStorage.setItem('auth_token', data.token);
localStorage.setItem('auth_user', JSON.stringify(data.user));
return true;
} catch (err) {
console.error('Login error:', err);
return false;
}
};
const logout = () => {
setUser(null);
setToken(null);
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
};
return (
<AuthContext.Provider
value={{
user,
token,
isLoading,
login,
logout,
isAuthenticated: !!user && !!token,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Implementing the Protected Route Guard
To prevent unauthorized visitors from accessing private application views (such as /dashboard or /settings), create a reusable route wrapper component:
// components/ProtectedRoute.tsx
'use client';
import React from 'react';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
interface ProtectedRouteProps {
children: React.ReactNode;
allowedRoles?: Array<'admin' | 'user'>;
}
export function ProtectedRoute({ children, allowedRoles }: ProtectedRouteProps) {
const { user, isAuthenticated, isLoading } = useAuth();
const router = useRouter();
React.useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login?redirect=' + encodeURIComponent(window.location.pathname));
}
}, [isLoading, isAuthenticated, router]);
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
</div>
);
}
if (!isAuthenticated) {
return null;
}
// Role-Based Access Control check
if (allowedRoles && user && !allowedRoles.includes(user.role as any)) {
return (
<div className="p-8 max-w-md mx-auto text-center space-y-4">
<h2 className="text-xl font-bold text-destructive">403 Forbidden</h2>
<p className="text-sm text-muted-foreground">
You do not have administrative permission to view this section.
</p>
</div>
);
}
return <>{children}</>;
}
Handling 401 Unauthorized with Axios Interceptors
When an access token expires mid-session, the server responds with 401 Unauthorized. Rather than dumping the user back to the login screen, production applications automatically catch this error and trigger a refresh token rotation:
// lib/apiClient.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: 'https://gen-endpoint.com/api',
});
// Request Interceptor: Inject JWT token into headers
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response Interceptor: Catch 401s
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
console.warn('Session token expired. Logging out.');
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
window.location.href = '/login?expired=true';
}
return Promise.reject(error);
}
);
Summary & Practice
By setting up a clean token abstraction layer and using Gen-Endpoint's JWT Auth API, you can build and test your complete authentication architecture—login forms, password validation, protected routes, and role-based permissions—without waiting for a backend auth service.
- Test login payloads live on the JWT Auth API Page.
- Test simulated 401 and 403 errors using our Status API.
Ready to Test These Endpoints?
Access 23+ free mock REST APIs, test file uploads, and generate Next.js API handlers with zero signup.