Skip to main content

Auth0 Authentication

info

What you'll learn

  • Log in to Auth0 through the UI with cy.origin()
  • Programmatically authenticate with Auth0 via a custom Cypress command
  • Adapt your Auth0 application for programmatic authentication during testing
caution

This guide is setup for testing against an Auth0 Single Page Application using the Classic Universal Login Experience. This configuration is recommended for a "Test Tenant" and/or "Test API" setup for automated end-to-end testing.

tip

Authenticate by visiting a different domain with cy.origin()

Typically, logging in a user within your app by authenticating via a third-party provider requires visiting a login page hosted on a different domain. Before Cypress v12.0.0, Cypress tests were limited to visiting domains of the same origin, making programmatic login the only option for authenticating users with a third-party API. As of Cypress v12.0.0, Cypress tests are no longer limited to visiting domains of a single origin, meaning you can easily authenticate with Auth0 via the UI!

Auth0 Application Setup

To get started with Auth0, an application needs to be setup within the Auth0 Dashboard via the following steps:

  1. Visit the Auth0 Dashboard and click the "Create Application" button.
  2. Enter the desired name for your application.
  3. Select "Single Page Application"

Once your application is created, visit the Application Settings tab under your application, and add your local development URL and port (e.g http://localhost:3000) under the following sections:

  • Allowed Callback URLs
  • Allowed Logout URLs
  • Allowed Web Origins
  • Allowed Origins (CORS)

In the bottom of Application Settings, click Show Advanced Settings, select "Grant Types" tab and check "Password" (unchecked by default).

Next, click your Tenant icon (upper right avatar menu) to go to your Tenant Settings. On the General tab go to the API Authorization Settings

  • Set "Default Audience" to the Audience URL for the Application you are testing (e.g. https://your-api-id.auth0.com/api/v2/)
  • Set "Default Directory" to "Username-Password-Authentication"
info

Refer to the Auth0 Tenant Settings documentation for additional details.

Finally, create a user in the Auth0 User Store for testing with Cypress. This testing-dedicated target user will be login to your application within your test specs. If required for your testing purposes, you can make as many users needed to test your specific application.

Setting Auth0 app credentials in Cypress

To have access to test user credentials within our tests we need to configure Cypress to use the Auth0 environment variables set in the .env file.

const { defineConfig } = require('cypress')
// Populate process.env with values from .env file
require('dotenv').config()

module.exports = defineConfig({
env: {
auth0_username: process.env.AUTH0_USERNAME,
auth0_password: process.env.AUTH0_PASSWORD,
auth0_domain: process.env.REACT_APP_AUTH0_DOMAIN,
auth0_audience: process.env.REACT_APP_AUTH0_AUDIENCE,
auth0_scope: process.env.REACT_APP_AUTH0_SCOPE,
auth0_client_id: process.env.REACT_APP_AUTH0_CLIENTID,
auth0_client_secret: process.env.AUTH0_CLIENT_SECRET,
},
})

Note that auth0_client_secret is only needed for programmatic login.

Custom Command for Auth0 Authentication

There are two ways you can authenticate to Auth0:

Login with cy.origin()

Next, we'll write a custom command called loginToAuth0 to perform a login to Auth0. This command will use cy.origin() to

  1. Navigate to the Auth0 login
  2. Input user credentials
  3. Sign in and redirect back to the Cypress Real World App
  4. Cache the results with cy.session()
// cypress/support/auth-provider-commands/auth0.ts

function loginViaAuth0Ui(username: string, password: string) {
// App landing page redirects to Auth0.
cy.visit('/')

// Login on Auth0.
cy.origin(
Cypress.env('auth0_domain'),
{ args: { username, password } },
({ username, password }) => {
cy.get('input#username').type(username)
cy.get('input#password').type(password, { log: false })
cy.contains('button[value=default]', 'Continue').click()
}
)

// Ensure Auth0 has redirected us back to the RWA.
cy.url().should('equal', 'http://localhost:3000/')
}

Cypress.Commands.add('loginToAuth0', (username: string, password: string) => {
const log = Cypress.log({
displayName: 'AUTH0 LOGIN',
message: [`🔐 Authenticating | ${username}`],
// @ts-ignore
autoEnd: false,
})
log.snapshot('before')

loginViaAuth0Ui(username, password)

log.snapshot('after')
log.end()
})

Now, we can use our loginToAuth0 command in the test. Below is our test to login as a user via Auth0 and run a basic sanity check.

describe('Auth0', function () {
beforeEach(function () {
cy.task('db:seed')
cy.intercept('POST', '/graphql').as('createBankAccount')
cy.loginToAuth0(
Cypress.env('auth0_username'),
Cypress.env('auth0_password')
)
cy.visit('/')
})

it('shows onboarding', function () {
cy.contains('Get Started').should('be.visible')
})
})

Lastly, we can refactor our login command to take advantage of cy.session() to store our logged in user so we don't have to reauthenticate before every test.

Cypress.Commands.add('loginToAuth0', (username: string, password: string) => {
const log = Cypress.log({
displayName: 'AUTH0 LOGIN',
message: [`🔐 Authenticating | ${username}`],
// @ts-ignore
autoEnd: false,
})
log.snapshot('before')

cy.session(
`auth0-${username}`,
() => {
loginViaAuth0Ui(username, password)
},
{
validate: () => {
// Validate presence of access token in localStorage.
cy.wrap(localStorage)
.invoke('getItem', 'authAccessToken')
.should('exist')
},
}
)

log.snapshot('after')
log.end()
})

Programmatic Login

Below is a command to programmatically login into Auth0, using the /oauth/token endpoint and set an item in localStorage with the authenticated users details, which we will use in our application code to verify we are authenticated under test.

The loginByAuth0Api command will execute the following steps:

  1. Use the /oauth/token endpoint to perform the programmatic login.
  2. Finally the auth0Cypress localStorage item is set with the access token, id_token and user profile.
// cypress/support/commands.js
Cypress.Commands.add(
'loginByAuth0Api',
(username: string, password: string) => {
cy.log(`Logging in as ${username}`)
const client_id = Cypress.env('auth0_client_id')
const client_secret = Cypress.env('auth0_client_secret')
const audience = Cypress.env('auth0_audience')
const scope = Cypress.env('auth0_scope')

cy.request({
method: 'POST',
url: `https://${Cypress.env('auth0_domain')}/oauth/token`,
body: {
grant_type: 'password',
username,
password,
audience,
scope,
client_id,
client_secret,
},
}).then(({ body }) => {
const claims = jwt.decode(body.id_token)
const {
nickname,
name,
picture,
updated_at,
email,
email_verified,
sub,
exp,
} = claims

const item = {
body: {
...body,
decodedToken: {
claims,
user: {
nickname,
name,
picture,
updated_at,
email,
email_verified,
sub,
},
audience,
client_id,
},
},
expiresAt: exp,
}

window.localStorage.setItem('auth0Cypress', JSON.stringify(item))

cy.visit('/')
})
}
)

With our Auth0 app setup properly in the Auth0 Developer console, necessary environment variables in place, and our loginByAuth0Api command implemented, we will be able to authenticate with Auth0 while our app is under test. Below is a test to login as a user via Auth0, complete the onboarding process and logout.

describe('Auth0', function () {
beforeEach(function () {
cy.task('db:seed')
cy.loginByAuth0Api(
Cypress.env('auth0_username'),
Cypress.env('auth0_password')
)
})

it('shows onboarding', function () {
cy.contains('Get Started').should('be.visible')
})
})

Adapting an Auth0 App for Testing

info
Note

The previous sections focused on the recommended Auth0 authentication practice within Cypress tests. To use this practice it is assumed you are testing an app appropriately built or adapted to use Auth0.

The following sections provides guidance on building or adapting an app to use Auth0 authentication. Please note that if you are logging in with cy.origin() and your app is already successfully integrated with Auth0, you do not need to make any further changes to your app and the remainder of this guide should be regarded as purely informational.

The Cypress Real World App is used and provides configuration and runnable code for both the React SPA and the Express back end.

The front end uses the auth0-react SDK for React Single Page Applications (SPA), which uses the auth0-spa-js SDK underneath. The back end uses express-jwt to validate JWT's against Auth0.

info
Note

Use the yarn dev:auth0 command when starting the Cypress Real World App.

Adapting the back end

In order to validate API requests from the frontend, we install express-jwt and jwks-rsa and configure validation for JWT's from Auth0.

// backend/helpers.ts
import jwt from 'express-jwt'
import jwksRsa from 'jwks-rsa'

dotenv.config()

const auth0JwtConfig = {
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${process.env.REACT_APP_AUTH0_DOMAIN}/.well-known/jwks.json`,
}),

// Validate the audience and the issuer.
audience: process.env.REACT_APP_AUTH0_AUDIENCE,
issuer: `https://${process.env.REACT_APP_AUTH0_DOMAIN}/`,
algorithms: ['RS256'],
}

Next, we'll define an Express middleware function to be use in our routes to verify the Auth0 JWT sent by the front end API requests as the Bearer token.

// backend/helpers.ts

// ...

export const checkJwt = jwt(auth0JwtConfig).unless({ path: ['/testData/*'] })

Once this helper is defined, we can use globally to apply to all routes:

// backend/app.ts
// initial imports ...
import { checkJwt } from './helpers'

// ...

if (process.env.REACT_APP_AUTH0) {
app.use(checkJwt)
}

// routes ...

Adapting the front end

We need to update our front end React app to allow for authentication with Auth0. As mentioned above, the auth0-react SDK for React Single Page Applications (SPA) is used.

First, we create a AppAuth0.tsx container to render our application as it is authenticated with Auth0. The component is identical to the App.tsx component, but uses the useAuth0 React Hook, removes the need for the Sign Up and Sign In routes and wraps the component with the withAuthenticationRequired higher order function (HOC).

A useEffect hook is added to get the access token for the authenticated user and send an AUTH0 event with the user and token objects to work with the existing authentication layer (authMachine.ts).

// src/containers/AppAuth0.tsx

// initial imports ...

import { withAuthenticationRequired, useAuth0 } from '@auth0/auth0-react'

// ...

const AppAuth0 = () => {
const { isAuthenticated, user, getAccessTokenSilently } = useAuth0()

// ...

useEffect(() => {
;(async function waitForToken() {
const token = await getAccessTokenSilently()
authService.send('AUTH0', { user, token })
})()
}, [user, getAccessTokenSilently])

// ...

const isLoggedIn =
isAuthenticated &&
(authState.matches('authorized') ||
authState.matches('refreshing') ||
authState.matches('updating'))

return <div className={classes.root}>// ...</div>
}

export default withAuthenticationRequired(AppAuth0)

Note: The full AppAuth0.tsx component is in the Cypress Real World App.

Next, we update our entry point (index.tsx) to wrap our application with the <Auth0Provider> from the auth0-react SDK SDK providing a custom onRedirectCallback. We pass props for the Auth0 environment variables set in .env above, and render our <AppAuth0> component as the application.

// src/index.tsx

// initial imports ...

import AppAuth0 from "./containers/AppAuth0";

// ..

const onRedirectCallback = (appState: any) => {
history.replace((appState && appState.returnTo) || window.location.pathname);
};

if (process.env.REACT_APP_AUTH0) {
ReactDOM.render(
<Auth0Provider
domain={process.env.REACT_APP_AUTH0_DOMAIN!}
clientId={process.env.REACT_APP_AUTH0_CLIENTID!}
redirectUri={window.location.origin}
audience={process.env.REACT_APP_AUTH0_AUDIENCE}
scope={process.env.REACT_APP_AUTH0_SCOPE}
onRedirectCallback={onRedirectCallback}
>
<Router history={history}>
<ThemeProvider theme={theme}>
<AppAuth0 />
</ThemeProvider>
</Router>
</Auth0Provider>,
document.getElementById("root")
);
} else {
// render passport-local App.tsx
}

An update to our AppAuth0.tsx component is needed to conditionally use the auth0Cypress localStorage item.

In the code below, we conditionally apply a useEffect block based on being under test with Cypress (using window.Cypress).

In addition, we will update the export to be wrapped with withAuthenticationRequired if we are not under test in Cypress. This allows our application to work with the Auth0 redirect login flow in development/production but not when under test in Cypress.

// src/containers/AppAuth0.tsx

// initial imports ...

import { withAuthenticationRequired, useAuth0 } from "@auth0/auth0-react";

// ...

const AppAuth0 = () => {
const { isAuthenticated, user, getAccessTokenSilently } = useAuth0();

// ...

useEffect(() => {
(async function waitForToken() {
const token = await getAccessTokenSilently();
authService.send("AUTH0", { user, token });
})();
}, [user, getAccessTokenSilently]);

// If under test in Cypress, get credentials from "auth0Cypress" localstorage item and send event to our state management to log the user into the SPA
if (window.Cypress) {
useEffect(() => {
const auth0 = JSON.parse(localStorage.getItem("auth0Cypress")!);
authService.send("AUTH0", {
user: auth0.body.decodedToken.user,
token: auth0.body.access_token,
});
}, []);
} else {
useEffect(() => {
(async function waitForToken() {
const token = await getAccessTokenSilently();
authService.send("AUTH0", { user, token });
})();
}, [isAuthenticated, user, getAccessTokenSilently]);
}

// ...

const isLoggedIn =
isAuthenticated &&
(authState.matches("authorized") ||
authState.matches("refreshing") ||
authState.matches("updating"));

return (
<div className={classes.root}>
// ...
</div>
);
};

// Conditional export wrapped with `withAuthenticationRequired` if we are not under test in Cypress.
let appAuth0 = window.Cypress ? AppAuth0 : withAuthenticationRequired(AppAuth0);
export default appAuth0

Auth0 Rate Limiting Logins

Be aware of the rate limit statement in the Auth0 documentation:

Auth0 Rate Limit - "If a user attempts to login 20 times per minute as the same user from the same location, regardless of having the correct credentials, the rate limit will come into effect. When this happens, the user can make 10 attempts per minute."

This limit can be reached as the size of a test suite grows along with enabling parallelized runs to speed up test run duration.

If you run into this rate limit, a programmatic approach can be added to the loginByAuth0 command to clear a blocked IP prior to the test run.

Next you'll need to obtain a API token to interact with the Auth0 Management API. This token is a JSON Web Token (JWT) and it contains specific granted permissions for the API.

Add this token as environment variable AUTH0_MGMT_API_TOKEN to our Cypress Real World App .env with your API token.

// .env
// ... additional keys
AUTH0_MGMT_API_TOKEN = 'YOUR-MANAGEMENT-API-TOKEN'

With this token in place, we can add interaction with the Auth0 Anomaly remove the blocked IP address endpoint to our loginByAuth0Api command. This will send a delete request to Auth0 Management API anomaly endpoint to unblock an IP that may become blocked during the test run.

info
Tip

icanhazip.com is a free, hosted service to find a system's current external IP address.

Cypress.Commands.add('loginByAuth0Api', (username, password) => {
// Useful when rate limited by Auth0
cy.exec('curl -4 icanhazip.com')
.its('stdout')
.then((ip) => {
cy.request({
method: 'DELETE',
url: `https://${Cypress.env(
'auth0_domain'
)}/api/v2/anomaly/blocks/ips/${ip}`,
auth: {
bearer: Cypress.env('auth0_mgmt_api_token'),
},
})
})

// ... remaining loginByAuth0Api command
})