Skip to content

Testing JWT Authentication in Cypress

JSON Web Tokens are one of the most common authentication mechanisms in modern APIs. This lesson covers obtaining, storing, and testing JWT-based auth flows in Cypress, including token expiration.

How JWTs Typically Flow Through a Test

A JWT-based login usually returns a signed token from a login endpoint, which the frontend stores (commonly in localStorage, sometimes in a cookie) and attaches to subsequent API requests, typically as an Authorization: Bearer <token> header.

cy.request('POST', '/api/login', {
  email: 'user@example.com',
  password: 'SecurePass123!',
}).then((response) => {
  window.localStorage.setItem('authToken', response.body.token);
});

This directly sets the token an application would normally set itself after a real UI login, skipping the UI entirely.

Working with JWTs in Tests

cy.request(...).then((res) => window.localStorage.setItem('token', res.body.token));
cy.intercept('*', (req) => {
  req.headers['Authorization'] = `Bearer ${token}`;
});
cy.request({ headers: { Authorization: `Bearer ${token}` } });
  • Set a token in localStorage/cookies to simulate an already-logged-in state before visiting a page.
  • Attach a token manually to cy.request() calls when testing authenticated API endpoints directly.
  • Use cy.intercept() to inject or modify the Authorization header for specific scenarios.
  • Combine with cy.session() so token setup happens only once per test run.

JWT Testing Cheat Sheet

Common JWT-related test scenarios and how to approach each.

Scenario Approach
Simulate a logged-in user API login, then set token via window.localStorage.setItem()
Test an expired token Set an intentionally expired/invalid token before visiting
Test token refresh flow Stub a 401 response, then a successful refresh response
Test a missing token Clear storage, then visit a protected route
Test an authenticated API call Attach the token manually to cy.request() headers

Testing Expired Token Handling

A robust application should gracefully redirect to login (or trigger a refresh flow) when an API call returns a 401 due to an expired token. Simulating this doesn't require waiting for a real token to actually expire, stub the response directly.

cy.intercept('GET', '/api/me', {
  statusCode: 401,
  body: { message: 'Token expired' },
}).as('expiredCheck');

cy.visit('/dashboard');
cy.wait('@expiredCheck');
cy.url().should('include', '/login');

Testing a Token Refresh Flow

For applications implementing silent token refresh, a common pattern stubs an initial 401, followed by a successful refresh response, and confirms the original request is retried automatically without disrupting the user.

cy.intercept('GET', '/api/data', { statusCode: 401 }).as('initialCall');
cy.intercept('POST', '/api/refresh-token', { fixture: 'refreshed-token.json' }).as('refresh');
cy.intercept('GET', '/api/data', { fixture: 'data.json' }).as('retriedCall'); // after refresh

cy.visit('/dashboard');
cy.wait('@initialCall');
cy.wait('@refresh');
cy.wait('@retriedCall');
cy.get('[data-cy="dashboard-content"]').should('be.visible');

Chaining multiple intercepts for the same URL lets each one apply in sequence as the flow progresses.

Common Mistakes

  • Manually decoding and reconstructing JWTs in tests instead of obtaining a real one from an API login call.
  • Forgetting that setting a token in localStorage must happen before cy.visit() to be picked up during initial app load.
  • Waiting for a real token to expire during a test instead of stubbing the expired-token response directly.
  • Not testing the actual user-facing behavior (redirect, refresh) that should happen when a token expires.

Key Takeaways

  • JWT-based auth typically involves storing a token client-side and attaching it to subsequent requests.
  • Setting a valid token before cy.visit() simulates an already-logged-in state efficiently.
  • Stubbing 401 responses lets you test expired-token handling without waiting for real expiration.
  • Token refresh flows can be tested by chaining sequential intercepts for the same endpoint.

Pro Tip

Write a dedicated test that stubs a 401 on a protected API call specifically to confirm your app's actual recovery behavior (redirect to login, or silent refresh), this is exactly the kind of edge case that's easy to build correctly once and then accidentally break in a later refactor with no test to catch it.