Currently facing a complex issue where i want to authenticate my end-to-end test in Cypress using Auth0 and Next-auth. Already took me quite some effort using various different tutorials like;
On localhost I’ve got the whole flow working now. But when I run the test via CI in a Github workflow using a custom domain/host I run into issues where instead of being logged in, I’m redirected to the Universal Login page of Auth0. You can see a compared flow of the Cypress tests in the following screenshot:
Here I’m facing the issue that as soon as I set the next-auth.session-token
on my test server and redirect to the platform, the app is redirected to the Auth0 Universal Login page instead. For logging into Auth0 I use a custom Cypress command creating tokens via the Auth0 passwordGrant:
// cypress/support/commands.ts
import hkdf from '@panva/hkdf';
import { EncryptJWT, JWTPayload, decodeJwt } from 'jose';
// Function from https://github.com/nextauthjs/next-auth/blob/5c1826a8d1f8d8c2d26959d12375704b0a693bfc/packages/next-auth/src/jwt/index.ts#L113-L121
async function getDerivedEncryptionKey(secret: string) {
return await hkdf('sha256', secret, '', 'NextAuth.js Generated Encryption Key', 32);
}
// Function from https://github.com/nextauthjs/next-auth/blob/5c1826a8d1f8d8c2d26959d12375704b0a693bfc/packages/next-auth/src/jwt/index.ts#L16-L25
export async function encode(token: JWTPayload, secret: string): Promise<string> {
const maxAge = 30 * 24 * 60 * 60; // 30 days
const encryptionSecret = await getDerivedEncryptionKey(secret);
return await new EncryptJWT(token)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuedAt()
.setExpirationTime(Math.round(Date.now() / 1000 + maxAge))
.setJti('test')
.encrypt(encryptionSecret);
}
Cypress.Commands.add('loginByAuth0Api', (username: string, password: string) => {
const log = Cypress.log({
displayName: 'AUTH0 LOGIN',
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
log.snapshot(`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');
log.snapshot('before');
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 user: any = decodeJwt(body.id_token);
const userObj = {
user: {
accessToken: body.access_token,
idToken: body.id_token,
sub: user.sub,
nickname: user.nickname,
picture: user.name,
email: user.email,
},
};
// Generate and set a valid cookie that next-auth can decrypt
cy.wrap(null)
.then(() => {
return encode(userObj.user, Cypress.env('NEXTAUTH_SECRET'));
})
.then((encryptedToken) => {
cy.setCookie('next-auth.session-token', encryptedToken);
// Gives error:
// cy.setCookie() had an unexpected error setting the requested cookie in Chrome.
// > Error: Sanitizing cookie failed
// cy.setCookie('next-auth.session-token', encryptedToken, {
// domain: `https://${Cypress.env('auth0_domain')}`,
// expiry: body.expires_in,
// secure: true,
// httpOnly: true,
// path: '/',
// });
Cypress.Cookies.defaults({
preserve: 'next-auth.session-token',
});
});
log.snapshot('after');
log.end();
});
});
As you can see I’m fetching the user on the supplied Auth0 issuer and fetching tokens. I can verify via jwt.io that the tokens fetched are valid on both localhost and the test server.
Furthermore the code is fairly simple. I use a next-auth middleware file in src/middleware.ts
to redirect to user when unauthenticated and using a straight forwards next-auth config as well.
// src/middleware.ts
export { default } from 'next-auth/middleware';
export const config = { matcher: ['/dashboard/:path*'] };
// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import type { NextAuthOptions } from 'next-auth';
import Auth0Provider from 'next-auth/providers/auth0';
const authOptions: NextAuthOptions = {
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
},
session: {
maxAge: 900, // 15 min
},
providers: [
Auth0Provider({
clientId: process.env.AUTH0_CLIENT_ID!,
issuer: process.env.AUTH0_ISSUER!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
token.idToken = account.id_token;
}
return token;
},
async session({ session, token }) {
session.userId = token.sub;
return session;
},
},
};
export default NextAuth(authOptions);
Also maybe helpful is the Cypress config:
// cypress.config.ts
import { defineConfig } from 'cypress';
// Populate process.env with values from .env file
require('dotenv').config({
path: '.env.development',
});
const env = process.env.NODE_ENV === 'production' ? 'PROD' : 'DEV';
const baseUrl = process.env[`${env}_CYPRESS_BASE_URL`] || 'http://localhost:3000';
export default defineConfig({
e2e: {
baseUrl,
chromeWebSecurity: false,
},
env: {
NEXTAUTH_URL: process.env[`${env}_NEXTAUTH_URL`] || process.env.NEXTAUTH_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
auth0_username: '',
auth0_password: '',
auth0_domain: (process.env?.[`${env}_AUTH0_ISSUER`] || process.env?.AUTH0_ISSUER!).replace(
'https://',
'',
),
auth0_audience: 'main-audience',
auth0_scope: 'openid profile email offline_access',
auth0_client_id: process.env[`${env}_AUTH0_CLIENT_ID`] || process.env.AUTH0_CLIENT_ID,
auth0_client_secret:
process.env[`${env}_AUTH0_CLIENT_SECRET`] || process.env.AUTH0_CLIENT_SECRET,
},
});
As seen in the Next Auth - Testing with Cypress tutorial I would expect that setting the next-auth.session-token
cookie would be enough to login via Auth0 with next-auth. I also would expect the same behaviour on localhost as well as the test server.
Only the settings of the cookie is not as in the tutorial:
// tutorial code
cy.setCookie(cookie.name, cookie.value, {
domain: cookie.domain,
expiry: cookie.expires,
httpOnly: cookie.httpOnly,
path: cookie.path,
secure: cookie.secure,
})
// my used code
cy.setCookie('next-auth.session-token', encryptedToken);
// also tried
cy.setCookie('next-auth.session-token', encryptedToken, {
domain: `https://${Cypress.env('auth0_domain')}`,
expiry: body.expires_in,
secure: true,
httpOnly: true,
path: '/',
});
But the latter results in a Cypress error: > Error: Sanitizing cookie failed
Although I’ve come a long way, I’m not sure what I’m missing or misunderstanding to fix the final step; so all the help is welcome. Thanks in advance.