Setup Cypress and Auth0 for SPA’s
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 setbaseUrl
incypress.json
. For example in my case testing locally:{ "baseUrl": "http://localhost:3020" }
- The
key
andstorageValue
need to be of a specific format in order for the Auth0 client to pick it up. The snippet contains the default values forkeyPrefix
,defaultScope
andaudience
. Don’t worry about their peculiar values. Unless you’re tinkering withscope
oraudience
when creating your Auth0 client you don’t need to change those.
Happy testing!