Q37 of 48 · Cypress

How do you handle authentication for tests that need different user roles?

CypressSeniorcypressauthcy-sessionrbacsenior

Short answer

Short answer: Define one `cy.loginAs(role)` custom command backed by `cy.session(role, ...)` so each role logs in once per spec (or once across the suite with `cacheAcrossSpecs`). Validate the cached session with a `validate` callback to refresh expired tokens. Seed users via `cy.task` so the test environment matches expectations.

Detail

A real-world suite has multiple personas: admin, manager, viewer, anonymous. Naive setups log in for every test, doubling or tripling suite runtime.

The clean architecture:

1. Seed users once per environment. A cy.task('seedUsers') call (or a one-off setup script) ensures admin@x.com, manager@x.com, viewer@x.com all exist with known passwords. Specs assume these users; they don't create them.

2. Single loginAs command keyed by role.

Cypress.Commands.add('loginAs', (role: 'admin' | 'manager' | 'viewer') => {
  cy.session(
    role,
    () => {
      cy.request('POST', '/api/login', {
        email: Cypress.env(`${role}Email`),
        password: Cypress.env('password'),
      }).then(({ body }) => {
        window.localStorage.setItem('token', body.token);
      });
    },
    {
      cacheAcrossSpecs: true,
      validate() {
        cy.request({ url: '/api/me', failOnStatusCode: false })
          .its('status').should('eq', 200);
      },
    },
  );
});

3. Cache across specs. cacheAcrossSpecs: true means each role logs in once across the entire suite, not once per spec. With four roles and 200 specs, that's four logins instead of 800.

4. Validate to handle expiry. Long suites can outlast token TTL. The validate callback checks /api/me on every restore; if it fails, Cypress re-runs setup transparently.

5. Different tokens for different tests. If a single spec needs to switch personas, just call cy.loginAs(otherRole) — the session command swaps the cached state. Each role's session is independent.

6. Avoid logging in via the UIcy.request is faster and more reliable. Test the UI login flow in dedicated specs (login page tests); use API login for everything else.

Edge cases worth mentioning: SSO / OAuth flows can't always be hit via cy.request (some require interactive consent). The pattern then is to use a "test mode" backdoor in your auth service, or to log in manually once and reuse a captured token.

// EXAMPLE

support/commands.ts + spec

// support/commands.ts
type Role = 'admin' | 'manager' | 'viewer';

Cypress.Commands.add('loginAs', (role: Role) => {
  cy.session(
    role,
    () => {
      cy.request('POST', '/api/login', {
        email: Cypress.env(`${role}Email`),
        password: Cypress.env('password'),
      }).then(({ body }) => {
        window.localStorage.setItem('token', body.token);
      });
    },
    {
      cacheAcrossSpecs: true,
      validate() {
        cy.request({ url: '/api/me', failOnStatusCode: false })
          .its('status').should('eq', 200);
      },
    },
  );
});

declare global {
  namespace Cypress {
    interface Chainable {
      loginAs(role: Role): Chainable<void>;
    }
  }
}
export {};

// Spec
describe('Permissions matrix', () => {
  it('admin can delete a user', () => {
    cy.loginAs('admin');
    cy.visit('/users/u1');
    cy.get('[data-test=delete]').should('be.visible').click();
  });

  it('viewer cannot see the delete button', () => {
    cy.loginAs('viewer');
    cy.visit('/users/u1');
    cy.get('[data-test=delete]').should('not.exist');
  });
});

// WHAT INTERVIEWERS LOOK FOR

One command keyed by role + `cy.session` with `cacheAcrossSpecs` + `validate`; seeding via `cy.task`; API login over UI login.

// COMMON PITFALL

Re-implementing login per role with copy-pasted code, or logging in through the UI on every test.