Issue
Unable to pragmatically detect the user is logged in for uses in E2E tests such as Cypress.
Reasoning
Following this tutorial from January. Seems a little out of date as the client library doesn't accept the returned id_token/access_token instead requires a code.
To bypass this it would be useful if the param id_token_hint was usable to pass the token directly to createAuth0Client. Also, the handleRedirectCallback could accept the token instead of performing a request to the auth API end point here https://github.com/auth0/auth0-spa-js/blob/master/src/Auth0Client.ts#L249.
If you have a work around for this it would be much appreciated. Theres a few people struggling with this issue here: https://community.auth0.com/t/end-to-end-testing-with-cypress-and-auth0/19082/16
The tutorial should work just fine, considering it's using a different library (Auth0.js, https://github.com/danlourenco/auth0-cypress-demo/blob/base/package.json#L6).
We use cypress in this library and we actually use the 'naive' approach in that blog post and it works pretty well for us: https://github.com/auth0/auth0-spa-js/blob/master/cypress/integration/loginWithRedirect.js
Because of how PKCE works (first ask for a code, then exchange this code for the token), it's unlikely you'll be able to authenticate a user without doing the full authentication pipeline.
Also, you can send the id_token_hint to the Authorization Server (Auth0 in this case), but I don't think it will automatically log you in, though.
For the benefit of others, I have managed to get this working with the following which:
Cypress.Commands.add(
'login',
(username, password, appState = { target: '/' }) => {
cy.log(`Logging in as ${username}`);
const options = {
method: 'POST',
url: Cypress.env('Auth0TokenUrl'),
body: {
grant_type: 'password',
username,
password,
audience: Cypress.env('Auth0Audience'),
scope: 'openid profile email',
client_id: Cypress.env('Auth0ClientId'),
client_secret: Cypress.env('Auth0ClientSecret')
}
};
cy.request(options).then(({ body }) => {
const { access_token, expires_in, id_token } = body;
cy.server();
// intercept Auth0 request for token and return what we have
cy.route({
url: 'oauth/token',
method: 'POST',
response: {
access_token,
expires_in,
id_token,
token_type: 'Bearer'
}
});
// Auth0 SPA SDK will check for value in cookie to get appState
// and validate nonce (which has been removed for simplicity)
const stateId = 'test';
const encodedAppState = encodeURI(JSON.stringify(appState));
cy.setCookie(
`a0.spajs.txs.${stateId}`,
`{%22appState%22:${encodedAppState}%2C%22scope%22:%22openid%20profile%20email%22%2C%22audience%22:%22default%22}`
);
const callbackUrl = `/auth/callback?code=test-code&state=${stateId}`;
return cy.visit(callbackUrl);
});
}
);
declare namespace Cypress {
interface Chainable<Subject> {
login(
username: string,
password: string,
appState?: any
): Chainable<Subject>;
}
}
@cwmrowe Thank you for benefit, but it doesn't work for us. I got the token but when the cookie was set according your example, the user isn't logged. The cookie auth0.is.authenticated with true value is set but user isn't still logged. I wonder in what step auth0 spa sdk should check the cookie and validate it?

Hey there, I noticed this issue was closed with no real solution provided. As a member of a team that relies on this library for authentication and interfacing with Auth0, I would appreciate some guidance on how to handle integration testing while following best practices. I'm happy to contribute, as our team is heavily invested in Auth0, would just need some direction.
Hi all -- just a followup as someone else who has been hitting these same challenges and finally found a solution that worked...
I took what @cwmrowe provided above and proceeded a bit further as I encountered the same issues that @Nadya913 was hitting.
I was able to finally get this working by inspecting the actual cookies generated on localhost when loading the oauth screen. In the end, there were a few differences from what he provided above versus what I was observing in the live cookies.
Once I adjusted these items, I got it working... here's the working code:
Cypress.Commands.add(
'login',
(username, password, appState = {targetUrl: '/'}) => {
cy.log(`Logging in as ${username}`);
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).then(({body}) => {
const {access_token, expires_in, id_token} = body;
cy.server();
// intercept Auth0 request for token and return what we have
cy.route({
url: 'oauth/token',
method: 'POST',
response: {
access_token: access_token,
id_token: id_token,
scope: "openid profile email",
expires_in: expires_in,
token_type: 'Bearer'
}
});
// Auth0 SPA SDK will check for value in cookie to get appState
// and validate nonce (which has been removed for simplicity)
const stateId = 'test';
cy.setCookie(
`a0.spajs.txs.${stateId}`,
encodeURIComponent(JSON.stringify({
"appState": appState,
"scope": "openid profile email",
"audience": Cypress.env('auth_audience'),
"redirect_uri": "http://localhost:3000"
}))
).then(() => {
cy.visit(`/?code=test-code&state=${stateId}`);
});
});
}
);
Cheers,
Dave
Thanks @Taskle!
Hi all -- just a followup as someone else who has been hitting these same challenges and finally found a solution that worked...
I took what @cwmrowe provided above and proceeded a bit further as I encountered the same issues that @Nadya913 was hitting.
I was able to finally get this working by inspecting the actual cookies generated on localhost when loading the oauth screen. In the end, there were a few differences from what he provided above versus what I was observing in the live cookies.
Once I adjusted these items, I got it working... here's the working code:
Cypress.Commands.add( 'login', (username, password, appState = {target: '/'}) => { cy.log(`Logging in as ${username}`); 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).then(({body}) => { const {access_token, expires_in, id_token} = body; cy.server(); // intercept Auth0 request for token and return what we have cy.route({ url: 'oauth/token', method: 'POST', response: { access_token: access_token, id_token: id_token, scope: "openid profile email", expires_in: expires_in, token_type: 'Bearer' } }); // Auth0 SPA SDK will check for value in cookie to get appState // and validate nonce (which has been removed for simplicity) const stateId = 'test'; cy.setCookie( `a0.spajs.txs.${stateId}`, encodeURIComponent(JSON.stringify({ "appState": {"targetUrl": "/"}, "scope": "openid profile email", "audience": Cypress.env('auth_audience'), "redirect_uri": "http://localhost:3000" })) ).then(() => { cy.visit(`/?code=test-code&state=${stateId}`); }); }); } );Cheers,
Dave
Thank you for the help, this has been really hard for me recently and there are little resources :/ What is const stateId = 'test'; used for? Can it be any random string? I didn't understand that part so well.
@occult TLDR to get this to work, the state in the url params just needs to match the cookie. If you look at where it's generated in the spa library, https://github.com/auth0/auth0-spa-js/blob/master/src/Auth0Client.ts#L249 -- you'll see:
const stateIn = encodeState(createRandomString());
Also note the source code indicates:
Random and secure `state` and `nonce` parameters will be auto-generated.
So what matters is that they match. But for e2e testing, 'test' is fine.
If you still have auth issues with the above approach, it's likely because the audience param or other params aren't matching your auth0 configuration, or that you need to configure your app to have the callback url set in the auth0 spa library which then will handle the code/state params automatically.
Hope this is helpful!
Dave
@occult TLDR to get this to work, the state in the url params just needs to match the cookie. If you look at where it's generated in the spa library, https://github.com/auth0/auth0-spa-js/blob/master/src/Auth0Client.ts#L249 -- you'll see:
const stateIn = encodeState(createRandomString());Also note the source code indicates:
Random and secure `state` and `nonce` parameters will be auto-generated.So what matters is that they match. But for e2e testing, 'test' is fine.
If you still have auth issues with the above approach, it's likely because the audience param or other params aren't matching your auth0 configuration, or that you need to configure your app to have the callback url set in the auth0 spa library which then will handle the code/state params automatically.
Hope this is helpful!
Dave
Thank you Dave for the explanation. It helped me understand the solution. I ended up using the .env file to make a test version of my app which mocks authentication and is faster than waiting for Auth0.
Hi @Taskle, @cwmrowe and everyone else,
Thank you for your help with this. Like @occult I've been having trouble implementing this as well. I've created a github repo https://github.com/dunson062786/login-programmatically-into-auth0-with-cypress that adds cypress to the simple react app that auth0 makes you create in their quick start. In my README.md I specify what parts that need to be configured. Currently I have 1 cypress test and it's failing. It has 1 cypress test and the test tests that the login button says 'Log out' instead of 'Log in' after logging in. I created the login command @Taskle provided. and my login_spec.js file uses it:
describe('login', () => {
it('should successfully log into our app', () => {
cy.login(Cypress.env('auth_username'), Cypress.env("auth_password"))
.then(() => {
// currently this fails
cy.get('button').contains('Log out')
})
})
});
for some reason after I
cy.visit(`/?code=test-code&state=${stateId}`);
auth0 does not think I'm authenticated.
const isAuthenticated = await auth0FromHook.isAuthenticated();
in line 31 of /src/react-auth0-spa.js
I've tried debugging isAuthenticated, but it's typescript so stepping through the code doesn't work the way I expect. It's weird because @auth0/auth0-spa-js has source maps in their dist folder so I thought I would be able to debug it. I have enable JavaScript source maps checked in my chrome developer tools settings too.
Anyway, please let me know if you see anything wrong with what I'm doing.
@dunson062786 I recommend checking a couple things:
Hope this helps!
Dave
Hi @Taskle,
I followed your directions and I figured out what I did wrong. I was setting audience to my auth_audience which is "https://${domain}/api/v2/", but it should have been set to "default". I will update my github repo in a little bit with the fix.
Thank you so much for your help!
Glad you got it working!
Me too! If anyone needs a working example of logging in programmatically into Auth0 with cypress feel free to clone this repo: https://github.com/dunson062786/login-programmatically-into-auth0-with-cypress.
I was setting audience to my auth_audience which is "https://${domain}/api/v2/", but it should have been set to "default"
How can we find out if default is correct? On your example people are saying this isn't right for them?
In my case I was using a custom url as my audience and not default. Something I defined within auth0. And it's working for me as such. Admittedly it's been a while since I've looked at this, but the same code I have from above is working.
Additional calls to getUser or getTokenSilently do not seem to work this way. It appears to log me in, handleRedirectCallback works as expected, but getUser returns null and getTokenSilenty returns a 'login_required' error.
I am having trouble getting this to work. I got my generated a0.spajs.txs.* cookie to match exactly what I see when successfully manually logging in, but I still get redirected to the login page instead of my home page. I am getting a token correctly in the first part, what could be the problem?
@cwmrowe and @Taskle solution worked fine for me. But I cannot make it work with Refresh Tokens. Has anyone managed this?
@RipTheJacker i am getting the same issue for getTokenSilently, have you found a solution?
Fixed the issue in my test, when we call the getTokenSilently, we pare passing scope and audience. The scope in the test was set to "openid profile email" but in our code it was "openid profile". It seems to add "email" in the browser so we have added it to the getTokenSilently and this has resolved the problem
@ljones9142 Currently the SDK always sets default scopes of openid profile email regardless of your scope setting. However, we're working on a way you can specify default scopes which may make it more configurable for you.
Hi everyone, I struggle to stub the authentication process to make auth0-spa-js work with Cypress.
grant_type request that gives me a viable access token.a0.spajs.txs.${stateId} (similar to the one I have when inspecting auth0 cookie in a normal setup)./?code=test-code&state=${stateId}, the cookie gets deleted and I am sent back to the login popup.Why do you think the cookie would be deleted?
The cookie is deleted by the SDK as part of the transaction completion stage as it's no longer needed.
@stevehobbsdev Thanks for the explanation. Why am I no longer authenticated as soon as I cy.visit another page?
On localhost, whenever I refresh the page, auth0 automatically login without prompting the popup a second time.
Look at the plugin: https://github.com/lirantal/cypress-social-logins/
I cannot get the recommended solution to work with v1.12.0 - see this issue https://github.com/dunson062786/login-programmatically-into-auth0-with-cypress/issues/6
Perso finally I use the new possibility to store in localStorage the token and now my cypress test work.
Cypress.Commands.add(
"login",
(targetUrl = "/") => {
// cy.log("Logout first (if logged)");
// cy.getCookie("auth0.is.authenticated").then(($cookie) => {
// if ($cookie === "true") {
// cy.get('[data-cy="menu-user"]').click().wait(500)
// .get('ul[role="menu"]').find("li").contains("Logout").click();
// }
// });
const client_id = Cypress.env("auth_client_id");
const client_secret = Cypress.env("auth_client_secret");
const audience = Cypress.env("auth_audience");
const username = Cypress.env("auth_username");
const password = Cypress.env("auth_password");
const scope = "openid profile email";
cy.log(`Logging in as ${Cypress.env("auth_username")}`);
const options = {
method: "POST",
url: Cypress.env("auth_url"),
form: true,
body: {
grant_type: "password",
username,
password,
audience,
client_id,
client_secret,
scope,
},
};
cy.request(options).then(({ body }) => {
const { access_token, expires_in, id_token } = body;
const key = `@@auth0spajs@@::${client_id}::${audience}::${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,
};
cy.setLocalStorage(key, JSON.stringify(auth0Cache));
cy.visit(targetUrl);
});
},
);
Thanks @matinfo!
Still not working for anyone else? Remember to set cacheLocation: 'localstorage'! That tripped me up initially...
So Auth0 now has an official option to store the user's access token in local storage? :/
...All the while Auth0's documentation itself recommends against this:
Browser local storage (or session storage) is not secure. Any data stored there may be vulnerable to cross-site scripting. If an attacker steals a token, they can gain access to and make requests to your API. Treat tokens like credit card numbers or passwords: don鈥檛 store them in local storage.
@loremaps probably not relevant anymore, but
offline_access (const scope = "openid profile email offline_access") in scope did the trick with Refresh Tokens, somehow I found this info here: https://auth0.com/docs/api/authentication?javascript#authorization-code-flow-with-pkce under Remarks section:Include offline_access to the scope request parameter to get a Refresh Token from POST /oauth/token. Make sure that the Allow Offline Access field is enabled in the API Settings.
it was important to mirror exactly what normal localstorage key holds, especially decodedToken value, because getIdTokenClaims (which I'm using to extract jwt token for Authorization header) would return undefined otherwise.
audience in localstorage key had to be default instead of https://domain.eu.auth0.com/api/v2/ value passed to https://domain.eu.auth0.com/oauth/token request
My exact cypress login command:
import "cypress-localstorage-commands";
import jwt_decode from "jwt-decode";
Cypress.Commands.add(
"login",
({
username = Cypress.env("auth_username"),
password = Cypress.env("auth_password"),
targetUrl = "/dashboard"
} = {}) => {
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"; // important to include offline_access when using Refresh Tokens
cy.log(`Logging in as ${username}`);
const options = {
method: "POST",
url: Cypress.env("auth_url"),
form: true,
body: {
grant_type: "password",
username,
password,
audience,
client_id,
client_secret,
scope
}
};
cy.request(options).then(({ body }) => {
const { access_token, expires_in, id_token } = body;
const [header, payload, signature] = id_token.split(".");
const tokenData = jwt_decode(id_token);
const tokenDataHeader = jwt_decode(id_token, { header: true });
const key = `@@auth0spajs@@::${client_id}::default::${scope}`;
const auth0Cache = {
body: {
client_id,
access_token,
id_token,
scope,
expires_in,
decodedToken: {
encoded: { header, payload, signature },
header: tokenDataHeader,
// below is returned by getIdTokenClaims
claims: {
__raw: id_token,
...tokenData
},
user: tokenData
}
},
expiresAt: Math.floor(Date.now() / 1000) + expires_in
};
cy.setLocalStorage(key, JSON.stringify(auth0Cache));
cy.visit(targetUrl);
});
}
);
All and all linking cypress with auth0 was quite a task, lacked proper documentation for such a popular topic
Thanks for the explanation @jrozbicki, always helpful to provide such information for people in the future.
I wanted to chime in to drop this link in this issue as well, as I think it can be valuable for anyone looking to integrate the two: https://sandrino.dev/blog/writing-cypress-e2e-tests-with-auth0
That said, we are aware of the fact that our official documentation regarding Cypress is lacking and are working with the Cypress team to improve this so we can provide quality documentation, both in terms of the Auth0 side as the Cypress side.
I was able to use this in our test but just half way. Whenever we use click out to next subpage user is not logged in anymore. Is this expected or issue with this approach?
Most helpful comment
Hi all -- just a followup as someone else who has been hitting these same challenges and finally found a solution that worked...
I took what @cwmrowe provided above and proceeded a bit further as I encountered the same issues that @Nadya913 was hitting.
I was able to finally get this working by inspecting the actual cookies generated on localhost when loading the oauth screen. In the end, there were a few differences from what he provided above versus what I was observing in the live cookies.
Once I adjusted these items, I got it working... here's the working code:
Cheers,
Dave