Testing JWT Authentication in a React + Laravel Clothes Store with Cypress
After spending two weeks trying to create dashboard tests for our React and Laravel e-commerce application, I hit a major roadblock: authentication. Since our application uses stateless API communication, JWT (JSON Web Tokens) with Laravel Sanctum handles authentication. Here's how I successfully implemented Cypress tests for this setup.
Understanding the Authentication Flow
The login functionality comprises:
- Backend: Laravel Sanctum for JWT generation
- Frontend: Axios interceptors + React Context for token management
- Protection: Dashboard pages wrapped in an authentication context
Backend: Laravel Auth Controller
The key authentication endpoints:
class AuthController extends Controller
{
public function login(LoginRequest $request): JsonResponse
{
if (!Auth::attempt($request->only('email', 'password'))) {
return $this->error(null, 'Invalid credentials', 401);
}
$user = $request->user();
$token = $user->createToken('auth_token')->plainTextToken;
return $this->success([
'user' => $user,
'token' => $token
], 'User logged in successfully.');
}
}
Route::prefix('auth')->group(function () {
Route::post('login', [AuthController::class, 'login']);
Route::get('me', [AuthController::class, 'me']);
});
Frontend: React Authentication Context
The AuthContext manages user state and token storage:
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const bootstrapAuth = useCallback(async () => {
const token = localStorage.getItem("token");
if (!token) {
setLoading(false);
return;
}
try {
const { data } = await authApi.me();
setUser(data.data);
} catch {
localStorage.removeItem("token");
setUser(null);
} finally {
setLoading(false);
}
}, []);
async function login(credentials) {
const { data } = await authApi.login(credentials);
localStorage.setItem("token", data.data.token);
setUser(data.data.user);
}
if (loading) return <ClothesLoader />;
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Axios Interceptors for Token Management
The interceptor automatically attaches tokens to protected requests:
export const privateClient = axios.create({
baseURL: import.meta.env.VITE_LARAVEL_APP_API_URL,
headers: { "Content-Type": "application/json" },
});
privateClient.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
Dashboard Route Protection
Protected routes check authentication before loading:
export const Route = createFileRoute("/_dashboard")({
beforeLoad: ({ context }) => {
if (!context.auth?.user) {
throw redirect({ to: "/login" });
}
},
component: DashboardLayout,
});
Implementing Cypress Login Command
The key insight: create a custom Cypress command that mimics the exact authentication flow. This command uses cy.session() to cache login state across tests:
Cypress.Commands.add("login", () => {
cy.session("admin-session", () => {
cy.request("POST", `${Cypress.env('apiUrl')}/auth/login`, {
email: Cypress.env("email"),
password: Cypress.env("password"),
}).then((response) => {
const token = response.body.data.token;
const user = response.body.data.user;
cy.window().then((win) => {
win.localStorage.setItem("token", token);
win.localStorage.setItem("user", JSON.stringify(user));
});
cy.intercept("GET", `${Cypress.env('apiUrl')}/auth/me`, {
statusCode: 200,
body: { data: user, message: "User fetched successfully." }
}).as("getMe");
cy.visit("/");
cy.wait("@getMe");
});
}, {
cacheAcrossSpecs: true,
validate: () => {
cy.window().then((win) => {
expect(win.localStorage.getItem("token")).to.exist;
});
}
});
});
Configuration
Set up environment variables in cypress.config.js:
module.exports = defineConfig({
env: {
email: 'admin@example.com',
password: 'securePassword123',
apiUrl: 'http://clothes-store.test/api/v1'
},
e2e: {
baseUrl: 'http://localhost:5173',
},
})
Using the Login Command in Tests
Now you can easily authenticate in any test:
describe("Add Product Page", () => {
beforeEach(() => {
cy.login();
cy.visit("/dashboard/products/add");
cy.contains("Add New Product").should("be.visible");
});
it("successfully creates a new product", () => {
// Test implementation...
});
});
Key Takeaways
- Understand the authentication flow before writing tests
-
Use
cy.session()to cache login state and speed up tests - Mock API responses that occur during authentication bootstrap
- Set up environment variables for sensitive credentials
- Create reusable commands for common authentication patterns
This approach reduced my test execution time by 60% and made tests more reliable by eliminating flaky login processes.
Resources
Understanding both frontend and backend authentication implementation is crucial for writing effective Cypress tests. The cy.session() command combined with proper API mocking creates a robust testing foundation for JWT-protected applications.

Top comments (0)