Skip to content

How to Create a Custom Login Command with Cypress, React, & Auth0

Auth0 is a tool for handling user authentication and account management. Cypress is a tool for writing end to end tests for your application.

Getting these two technologies to work together can have a few gotchas, especially if your application is set up to have the end to end tests in a separate folder from your frontend code. Today, we’ll go over the steps you’ll need to implement so you can have a custom Cypress command to log in a user, and also let your front end code continue to work safely!


Setup

For this setup, we're making the choice to keep our end to end tests in one folder, and our front end code in a sibling folder. You don't have to do it this way. It's just the choice we've made for this project. So our folder setup has two root level folders, "cypress" and "app":

cypress
| support
| cypress.config.ts
app
| tsconfig.ts
| src

React Adjustments

Auth0 provides a default login form for you to use. We’ll focus on the fact that the forms use web workers to store the user’s access tokens in memory. This helps them keep that access token more secure from attackers.

This is important to note for Cypress. But because Cypress does not have a way to interact with that web worker. So we need to be able to detect when our application is running the site through Cypress so we can adjust how we get the access token.

We’ll need to update the way our Auth0 form saves that token based on if our app is running normally, or if it’s running through Cypress. To do this, we can add a check to the cacheLocation prop in our Auth0Provider field that wraps around our root app component.

app/index.ts

<Auth0Provider
  domain={domain}
  clientId={clientId}
  audience={audience}
  redirectUri={window.location.origin}
  onRedirectCallback={onRedirectCallback}
  scope="openid email"
  useRefreshTokens
  cacheLocation={window.Cypress ? "localstorage" : "memory"}
>
  <App />
</Auth0Provider>

Important note if you’re using TypeScript: If your project is using TypeScript, there will be two more small changes you might need to make. First, your editor may be yelling at you that it doesn’t know what Cypress means, or that it doesn’t exist on the window interface. So you can make a type file to solve this.

src/types/globals.d.ts

  Interface Window {
    Cypress: boolean
  }

Then, in the root level tsconfig file, we need to add an option to the compilerOptions section so it knows to look for this file we just made and use it. Then your editor should be happy! :)

app/tsconfig.ts

  “compilerOptions”: {
    “typeRoots”: [“src/types/globals”]
  },

And that’s all your front end code should need to work! It’ll continue to work as expected when running your app locally or in production. But now when you run your Cypress tests, it’ll store that access token in local storage instead, so we can make use of that within our tests.


Cypress Adjustments

Now we can make our custom login command! Having a custom command for something like logging in a user that might need to be repeated before each step is super helpful. It makes it faster for your tests to get started, and doesn’t need to rely on the exact state of your UI so any UI changes you might make won’t affect this command.

There’s a few different things we’ll need to handle here:

  • Writing the custom command itself
  • Making an e2e file where we’ll set up this command to run before each test
  • Updating the Cypress config file to know about the environment variables we’ll need
  • If you’re using TypeScript, you’ll also need to make a separate “namespace” file and an index file to pull all the pieces together.

We’ll start with the meat of the project: making that custom command! I’ll share the whole command first. Then we’ll walk through the different sections individually to cover what’s going on with each part.

The Custom Command

Cypress automatically sets up a support folder for you, so you’ll likely already find a commands file in there. This will go in that file. If you don’t have it, you can create it!

cypress/support/commands.ts

1 /// <reference types="cypress" />
2 Cypress.Commands.add('loginWithAuth0', (username, password) => {
3 	const client_id = Cypress.env('auth0_client_id');
4 	const client_secret = Cypress.env('auth0_client_secret');
5 	const audience = Cypress.env('auth0_audience');
6 	const scope = Cypress.env('auth0_scope');
7
8 	cy.request({
9 		method: 'POST',
10 		url: `https://${Cypress.env('auth0_domain')}/oauth/token`,
11		body: {
12			grant_type: 'password',
13			username,
14			password,
15			audience,
16			scope,
17			client_id,
18			client_secret,
19		},
20	}).then(({ body }) => {
21		const { access_token, expires_in, id_token, token_type } = body;
22		cy.window().then((win) => {
23			win.localStorage.setItem(
24				`@@auth0spajs@@::${client_id}::${audience}::${scope}`,
25				JSON.stringify({
26					body: {
27						client_id,
28						access_token,
29						id_token,
30						scope,
31						expires_in,
32						token_type,
33						decodedToken: {
34							user: JSON.parse(
35								Buffer.from(id_token.split('.')[1], 'base64').toString('ascii')
36							),
37						},
38						audience,
39					},
40					expiresAt: Math.floor(Date.now() / 1000) + expires_in,
41				})
42			);
43		});
44	});
45	});

The first piece to note is line 2.

Cypress.Commands.add('loginWithAuth0', (username, password) => {...

This is what actually tells Cypress “hey, this is a command you can use”. The name can be anything you want. I’m using “loginWithAuth0”, but you could call it “login” or “kitty” or whatever makes the most sense for your project. Just make sure you know what it means! :)

Lines 3-6 are setting our the environment variables that the Auth0 call will use. Then, on line 8, we use Cypress to make the actual request to Auth0 that allows us to log in. For this use case, we’re choosing to login with a username and password, so we tell the call that we’re using the “password” type and send all the necessary environment variables for the call to work. (You can find info on what these values should be from the Auth0 docs.)

Then, on lines 20 and 21, we’re starting to deal with the response we get back. If the call was successful, this should contain the information on the test user we just signed in and the access token we need for them. So we extract those values from the body of the response so we can use them.

On line 22, we again use Cypress to get the browser’s window, and let us store the user access token. We'll use localStorage for this as seen on line 23.

Important note here!

Pay extra special attention to the way the string is set up that we’re storing in localStorage on line 24! Auth0 needs the access token to be stored in this specific manner. This is the only way it will find our token! So make sure yours is set up the same way.

The rest of the code here is taking the information we got from the Auth0 call and adding it to our new localStorage value. You’ll see that on line 35 we’re parsing the user information so we have access to that, and then setting an expiration on line 40 so the token won’t last forever.

And that’s it - our command is set up! Now on to the rest of the things we need to set up so we can actually use it. :)

Supporting Files

If you have commands that should be run before every test call, you can create an e2e file in your support folder. These are your global imports and functions that will apply to all of your Cypress test files.

Here we’ll import our custom commands file and actually call our new command in a beforeEach hook, passing in the test username and password we want to use.

cypress/support/e2e.ts

import "./commands";

beforeEach(function () {
  cy.loginWithAuth0(
    Cypress.env("auth0_username"),
    Cypress.env("auth0_password")
  );
});

TypeScript Note: To get the typings to work properly for your custom commands, you’ll need to do two things. First, you’ll want to make a new file called namespace in your “support” folder. Then, you’ll want to declare the Cypress namespace in there, and set up a line for your new custom command so Cypress knows the type for it. (If you originally edited the default Cypress commands file, you’ll also want to go to the bottom of that file and remove the namespace section from it - this is replacing that.)

declare namespace Cypress {
  interface Chainable {
    loginWithAuth0(username: string, password: string): Chainable<string>;
  }
}

Then in your support folder, create an “index.ts” file and add your commands and namespace imports to it.

  Import./commands”;
  Import./namespace;

That should clear up any TypeScript related warnings in your files!

The final piece is updating our Cypress configuration file. We need to add all the environment variables we need in here so Cypress is aware of them and we don’t have them hard coded in our files.

cypress.config.ts

import { defineConfig } from "cypress";

require("dotenv").config();
export default defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
  },
  env: {
    auth0_username: process.env.AUTH0_TEST_USERNAME,
    auth0_password: process.env.AUTH0_TEST_PASSWORD,
    auth0_domain: process.env.AUTH0_DOMAIN,
    auth0_audience: process.env.AUTH0_AUDIENCE,
    auth0_scope: process.env.AUTH0_SCOPE,
    auth0_client_id: process.env.AUTH0_CLIENT_ID,
    auth0_client_secret: process.env.AUTH0_CLIENT_SECRET,
  },
});

Wrap Up

With that in place, you should be good to go! Whenever you run your Cypress tests now, you should see that a user is automatically logged in for you so you start on the first page of your app.

We hope this helps! If you run into any issues or have questions on anything, please feel free to reach out to us. Leave a comment on this post or ask in our Discord. We’ll be happy to help!