Auth0-react and cypress

Okay, I’ve now managed a horrible hack that seems to work

1. Make sure my <Auth0Provider uses cacheLocation="localstorage"

Full code:

 <Auth0Provider
                domain={env.REACT_APP_AUTH0_DOMAIN}
                clientId={env.REACT_APP_AUTH0_CLIENT_ID}
                redirectUri={window.location.origin}
                onRedirectCallback={onRedirectCallback}
                useRefreshTokens={true}
                cacheLocation="localstorage"
              >

2. Update my commands.js


Cypress.Commands.add('login', (username, password) => {
  cy.log(`Logging in as ${username}`);
  const client_id = Cypress.env('auth_client_id');
  const client_secret = Cypress.env('auth_client_secret');
  const audience = Cypress.env('auth_audience');
  const scope = 'openid profile email offline_access';

  const options = {
    method: 'POST',
    url: Cypress.env('auth_url'),
    body: {
      grant_type: 'password',
      username,
      password,
      audience,
      scope,
      client_id,
      client_secret,
    },
  };
  cy.request(options).then(({ body }) => {
    const { access_token, expires_in, id_token } = body;
    const key = `@@auth0spajs@@::${client_id}::default::${scope}`;
    const auth0Cache = {
      body: {
        client_id,
        access_token,
        id_token,
        scope,
        expires_in,
        decodedToken: {
          user: jwt_decode(id_token),
        },
      },
      expiresAt: Math.floor(Date.now() / 1000) + expires_in,
    };
    window.localStorage.setItem(key, JSON.stringify(auth0Cache));
    window.localStorage.setItem('__cypress', JSON.stringify(auth0Cache)); 
  });
});


let LOCAL_STORAGE_MEMORY = {};

Cypress.Commands.add('saveLocalStorageCache', () => {
  Object.keys(localStorage).forEach((key) => {
    LOCAL_STORAGE_MEMORY[key] = localStorage[key];
  });
});

Cypress.Commands.add('restoreLocalStorageCache', () => {
  Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => {
    localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
  });
});

3. In my app, make sure we read __cypress when initializing auth0

(I’m using Hasura)

// src/useAuth.tsx
import { useAuth0 } from '@auth0/auth0-react';
import { useCallback, useEffect, useState } from 'react';

export interface HttpsHasuraIoJwtClaims {
  'x-hasura-default-role': string;
  'x-hasura-allowed-roles': string[];
  'x-hasura-user-id': string;
}

export interface Auth0User {
  'https://hasura.io/jwt/claims': HttpsHasuraIoJwtClaims;
  nickname: string;
  name: string;
  picture: string;
  updated_at: string;
  email: string;
  email_verified: boolean;
  sub: string;
}


type AuthState =
  | {
      state: 'in';
      user: Auth0User;
      token: string;
    }
  | {
      state: 'out';
    }
  | {
      state: 'loading';
    }
  | {
      state: 'error';
      error: Error;
    };

type AuthContext = {
  login(): void;
  logout(): void;
} & AuthState;
export let authToken: string | null = null;
export function useAuth(): AuthContext {
  const auth0 = useAuth0();
  const [state, setState] = useState<AuthState>({
    state: 'loading',
  });

  const login = useCallback(() => {
    auth0.loginWithRedirect({
      appState: {
        targetUrl: window.location.href.substr(window.location.origin.length),
      },
    });
  }, [auth0]);
  const logout = auth0.logout;

  // get access token
  useEffect(() => {
    const { user, isAuthenticated } = auth0;

    async function fetchToken() {
      const tokenClaims = await auth0.getIdTokenClaims();

      let token = tokenClaims?.__raw;
      if (!token && user) {
        // ⚠️ horrible hack to get cypress in
        const item = window.localStorage.getItem('__cypress');
        if (item) {
          const parsed = JSON.parse(item);
          token = parsed.body.id_token;
        }
      }
      if (!token) {
        throw new Error('No token in tokenClaims');
      }

      authToken = token;
      setState({
        state: 'in',
        token,
        user,
      });
    }

    if (user && isAuthenticated) {
      fetchToken().catch((error) => {
        setState({
          state: 'error',
          error,
        });
      });
    }
  }, [auth0]);

  useEffect(() => {
    if (auth0.isLoading) {
      setState({ state: 'loading' });
      return;
    }
    if (auth0.error) {
      setState({
        state: 'error',
        error: auth0.error,
      });
      return;
    }
    if (!auth0.isAuthenticated) {
      setState({ state: 'out' });
      return;
    }
  }, [auth0.error, auth0.isAuthenticated, auth0.isLoading, auth0.user]);

  return {
    ...state,
    login,
    logout,
  };
}

I can then use it in a basic cypress test like this:

/// <reference types="Cypress" />

describe('login', () => {
  before(() => {
    cy.login('test@example.com', 'test');
    cy.saveLocalStorageCache();
  });
  beforeEach(() => {
    cy.restoreLocalStorageCache();
  });
  it('visit /', () => {
    cy.visit('/');
    cy.contains('Apple');
  });
});