Auth0-react and cypress

Hey there,

I’m trying to get cypress working with auth0-react. I’ve been banging my head on this for a while so I can’t even find where I found the below code from.

I have this in my cypress commands.js

Cypress.Commands.add(
  'login',
  (username, password, appState = { targetUrl: '/' }) => {
    cy.log(`Logging in as ${username}`);
    const options = {
      method: 'POST',
      url: Cypress.env('auth_url'),
      body: {
        grant_type: 'password',
        username: Cypress.env('auth_username'),
        password: Cypress.env('auth_password'),
        audience: Cypress.env('auth_audience'),
        scope: 'openid profile email',
        client_id: Cypress.env('auth_client_id'),
        client_secret: Cypress.env('auth_client_secret'),
      },
    };
    cy.request(options).then(({ body }) => {
      const { access_token, expires_in, id_token } = body;

      cy.server();

      // intercept Auth0 request for token and return what we have
      cy.route({
        url: 'oauth/token',
        method: 'POST',
        response: {
          access_token: access_token,
          id_token: id_token,
          scope: 'openid profile email',
          expires_in: expires_in,
          token_type: 'Bearer',
        },
      });

      // Auth0 SPA SDK will check for value in cookie to get appState
      // and validate nonce (which has been removed for simplicity)
      const stateId = 'test';
      cy.setCookie('auth0.is.authenticated', 'true');
      cy.setCookie(
        `a0.spajs.txs.${stateId}`,
        encodeURIComponent(
          JSON.stringify({
            appState: appState,
            scope: 'openid profile email',
            audience: Cypress.env('auth_audience'),
            redirect_uri: 'http://localhost:3000',
          }),
        ),
      ).then(() => {
        cy.visit(`/?code=test-code&state=${stateId}`, {
          onBeforeLoad(win) {
            delete win.fetch;
          },
        });
      });
    });
  },
);
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
  const opts = Object.assign({}, options, {
    onBeforeLoad: (window, ...args) => {
      window.fetch = null;
      if (options.onBeforeLoad) {
        return options.onBeforeLoad(window, ...args);
      }
    },
  });
  return originalFn(url, opts);
});
Cypress.on('window:before:load', (win) => {
  delete win.fetch;
});

I have cypress.json:

{
  "experimentalFetchPolyfill": true,
  "baseUrl": "http://localhost:3000"
}

The problem seems to be that @auth0/auth0-react is using fetch which isn’t caught by cy.route() above, even if I frantically try to remove window.fetch + run experimentalFetchPolyfill. I’ve also tried to do window.fetch = null in the top of my index.js.

Have anyone else gotten auth0-react to play nicely with cypress? Could you guide me through it?

1 Like

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');
  });
});

/cypress.env.json looks something like this

{
  "auth_audience": "https://MYAPP.eu.auth0.com/api/v2/",
  "auth_url": "https://MYAPP.eu.auth0.com/oauth/token",
  "auth_client_id": "CLIENT_ID",
  "auth_client_secret": "CLIENT_SECRET",
}

I still want improvements -

  • I’d prefer to have a solution that works w/o local storage
  • How to get rid of the horrible __cypress hack - I needed it as auth0.getAccessTokenSilently() didn’t return a token that included the user that Hasura needs
  • There’s a mismatch on the localstorage key in auth_audience in the commands.js and that I store it as default (because of the application uses default), but I didn’t get why/how.

Anyway, happy I found a workaround. Maybe it helps someone else.

Hello there! After trawling the internet for days and days I finally found this post which culminated in a solution that seems to work pretty much flawlessly. Still no getting round localStorage, but I have managed to get round your auth0.getAccessTokenSilently() bug.

Here is my solution:

Cypress.Commands.add(
  'login',
  (username, password) => {
    const client_id = Cypress.env('auth0_client_id').
    const scope = 'openid profile email'
    const token_type = 'Bearer'
    const audience = 'default'

    const options = {
      method: 'POST',
      url: `https://${Cypress.env('auth0_domain')}/oauth/token`,
      body: {
        grant_type: 'password',
        client_id,
        username,
        password
      },
    }

    cy.request(options).then(({ body }) => {
      const { access_token, expires_in, id_token } = body
      const [header, payload, signature] = id_token.split('.')
      const tokenData = jwt.decode(id_token)

      window.localStorage.setItem(
        `@@auth0spajs@@::${client_id}::${audience}::${scope}`,
        JSON.stringify({
          body: {
            access_token,
            id_token,
            scope,
            expires_in,
            token_type,
            decodedToken: {
              encoded: { header, payload, signature },
              header: {
                alg: 'RS256',
                typ: 'JWT'
              },
              claims: {
                __raw: payload,
                ...tokenData
              },
              user: tokenData
            },
            audience,
            client_id,
          },
          expiresAt: Math.floor(Date.now() / 1000) + expires_in,
        })
      )
    })
  }
);
3 Likes

Your solution is really great and solve my problems, but i found a small issue.
The line “__raw: payload,” should be actually “__raw: id_token,
Otherwise the getIdTokenClaims failing to provide the proper value.

3 Likes

Big thanks @KATT for this solution. I was breaking my head with this thing :frowning_face: :gun:

BTW, maybe I’m missing something but I was able to simulate authenticated user just by doing the following:

  • Log in to the app in a different browser
  • Copy the value of @@auth0spajs@@
  • Change the expiresAt to 2612051263

So my test is basically this:

it(‘Check user is logged in’, () => {
localStorage.setItem(“@@auth0spajs@@::<client_id>::::openid profile email”, <THE VALUE I COPIED AND CHANGED IT’S expiresAt>)
cy.visit(“http://localhost:3000”)
})

This worked for me and the user was logged in at the front.

Does this still work?

Hi is this all you needed to stop the redirect to login from happening? Did you make any changes on the client code side?