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