Setup Cypress and Auth0 for SPA’s

Peer Techniek

How to setup Cypress for end to end testing an SPA that is locked behind an Auth0 login. Auth0 has a blogpost explaining this setup here. However, if you’re building a single page application you are most likely depending on @auth0/auth0-spa-js for managing your login state. This means we need to get the token into our auth0 client. This post will demonstrate how we can achieve that using localstorage.

Setup Auth0 and Cypress

First of all we need to configure Auth0 so that we can retrieve a token with an API request. To do this follow the steps in the paragraph Auth0 Setup & Configuration from the Auth0 blogpost about Cypress.

Add a file cypress.env.json to your cypress project root folder and fill it out with your auth0 application and user data.

{
  "auth_audience": "",
  "auth_url": "",
  "auth_client_id": "",
  "auth_client_secret": "",
  "auth_username": "",
  "auth_password": ""
}

Don’t forget to add it to your .gitignore!

Then add a command to /cypress/support/commands.js:

Cypress.Commands.add('login', (overrides = {}) => {
  Cypress.log({
    name: 'loginViaAuth0',
  });

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

 

Enable Auth0’s localstorage for caching

Now somewhere in your SPA you will have:

import createAuth0Client from '@auth0/auth0-spa-js'

...

this.auth0Client = await createAuth0Client(authOptions)

This is where you should enable localstorage. The parameter authOptions is an object that can contain cacheLocation, which we just need to set to 'localstorage'. Note that saving tokens in localstorage is a security concern. Therefore we will only use localstorage for non-production environments. For production we will use cacheLocation: 'memory', which is also the default if you don’t provide it.

const cacheLocation = process.env.NODE_ENV === 'production' ? 'memory' : 'localstorage'
const optionsWithCache = {
  ...authOptions,
  cacheLocation,
}
this.auth0Client = await createAuth0Client(optionsWithCache)

This also means Cypress login will only work in non-production environments, so I hope you don’t need to run Cypress on your production environment.

 

The login test

describe("login", () => {
  it("should successfully log into our app", () => {
    cy.login()
      .then((resp) => {
        return resp.body;
      })
      .then((body) => {
        const { access_token, expires_in, id_token, token_type } = body;
        cy.visit("/", {
          onBeforeLoad(win) {
            const keyPrefix = "@@auth0spajs@@";
            const defaultScope = "openid profile email";
            const clientId = Cypress.env("auth_client_id");
            const audience = "default";
            const key = `${keyPrefix}::${clientId}::${audience}::${defaultScope}`;
            const decodedToken = {
              user: JSON.parse(
                Buffer.from(body.id_token.split(".")[1], "base64").toString(
                  "ascii"
                )
              ),
            };
            const storageValue = JSON.stringify({
              body: {
                client_id: clientId,
                access_token,
                id_token,
                scope: defaultScope,
                expires_in,
                token_type,
                decodedToken,
                audience,
              },
              expiresAt: Math.floor(Date.now() / 1000) + body.expires_in,
            });
            win.localStorage.setItem(key, storageValue);
          },
        });
      });
  });
});

A few things to note from this snippet:

  • cy.visit("/") assumes you’ve set baseUrl in cypress.json. For example in my case testing locally:
    {
      "baseUrl": "http://localhost:3020"
    }
  • The key and storageValue need to be of a specific format in order for the Auth0 client to pick it up. The snippet contains the default values for keyPrefixdefaultScope and audience. Don’t worry about their peculiar values. Unless you’re tinkering with scope or audience when creating your Auth0 client you don’t need to change those.

Happy testing!

Een afspraak maken bij ons op kantoor of wil je even iemand spreken? Stuur ons een mail of bel met Jolanda.