Authenticating programmatically for Cypress testing using nextjs-auth0

Hi,

Currently setting up a testing framework using Cypress for our NextJS application. We handle authentication using the nextjs-auth0 library as recommended.

Using this library poses a problem authenticating when under test as we need to exercise the protected routes via logging in our test account etc. For obvious reasons, I do not want to login to the application via the Auth0 UI which is triggered when using the /api/auth/login, callback etc etc, although this does work as expected.

There doesn’t seem to be a great deal of information on this. I have managed to retrieve the accessToken for the user through the <domain>/oauth/token endpoint but I am struggling to understand how I go about creating a session, encrypting this, and persisting it as the appSession.0 cookie.

I found this library cypress-nextjs-auth0 which claims to do what I need but when trying to use it I get errors due to it being unmaintained and last updated a few years ago. I have repurposed this library to hopefully get this functionality to work. The encryption algorithm is as follow:

import hkdf from 'futoin-hkdf';
import * as jose from 'jose';

const BYTE_LENGTH = 32;
const ENCRYPTION_INFO = 'JWE CEK';
const options = { hash: 'SHA-256' };

const deriveKey = (secret: any) => {
	return hkdf(secret, BYTE_LENGTH, { info: ENCRYPTION_INFO, ...options });
};

const encrypt = async (arg: any) => {
	const { secret, ...thingToEncrypt } = arg;
	const key = jose.JWK.asKey(deriveKey(secret));
	const epochNow = (Date.now() / 1000) | 0;
	return Promise.resolve(
	  jose.JWE.encrypt(JSON.stringify(thingToEncrypt), key, {
			alg: 'dir',
			enc: 'A256GCM',
			uat: epochNow,
			iat: epochNow,
			exp: epochNow + 7 * 24 * 60 * 60,
	  }),
	);
};

export default encrypt;

I have managed to fetch the user from /oauth/token endpoint, and encrypt a payload to store to the cookie appSession, but when I then attempt to call /api/auth/me to confirm the user is authenticated I get back a 401.

This is the main login command:

let cachedUsername: string;

Cypress.Commands.add('login', (credentials = {}) => {
	const { username, password } = credentials;
	const cachedUserIsCurrentUser = cachedUsername && cachedUsername === username;
	const _credentials = {
		username: username || Cypress.env('auth0Username'),
		password: password || Cypress.env('auth0Password'),
	};

	const sessionCookieName = Cypress.env('auth0SessionCookieName');

	try {
		cy.getCookie(sessionCookieName).then(cookieValue => {
			/* Skip logging in again if session already exists */

			if (cookieValue && cachedUserIsCurrentUser) {
				return true;
			} else {
				cy.clearCookies();

				cy.getUserTokens(_credentials).then((response: any) => {
					const { access_token, expires_in, id_token, scope } = response.body;

					cy.getUserInfo(access_token).then((user: any) => {
						const payload = {
							secret: Cypress.env('auth0CookieSecret'),
							user,
							idToken: id_token,
							accessToken: access_token,
							accessTokenScope: scope,
							accessTokenExpiresAt: Date.now() + expires_in,
							createdAt: Date.now(),
						};

						cy.task('encrypt', payload).then((encryptedSession: any) => {
							cy._setAuth0Cookie(encryptedSession);
						});
					});
				});
			}
		});
	} catch (error) {
		// throw new Error(error);
	}
});

And finally the setting of the cookie:

import { serialize } from 'cookie';

const MAX_COOKIE_SIZE = 4096;

const COOKIE_OPTIONS = {
	domain: Cypress.env('auth0CookieDomain'),
	path: Cypress.env('auth0CookiePath'),
	httpOnly: Cypress.env('auth0CookieHttpOnly'),
	sameSite: Cypress.env('auth0CookieCookieSameSite'),
	secure: Cypress.env('auth0CookieCookieSecure'),
	transient: Cypress.env('auth0CookieTransient'),
};

const SESSION_COOKIE_NAME = Cypress.env('auth0SessionCookieName');

Cypress.Commands.add('_setAuth0Cookie', encryptedSession => {
	const emptyCookie = serialize(`${SESSION_COOKIE_NAME}.0`, '', COOKIE_OPTIONS);
	const chunkSize = MAX_COOKIE_SIZE - emptyCookie.length;

	const value = encryptedSession;
	const chunkCount = Math.ceil(value.length / chunkSize);

	if (chunkCount > 1) {
		for (let i = 0; i < chunkCount; i++) {
			const chunkValue = value.slice(i * chunkSize, (i + 1) * chunkSize);
			const chunkCookieName = `${SESSION_COOKIE_NAME}.${i}`;

			cy.setCookie(chunkCookieName, chunkValue, COOKIE_OPTIONS);
		}

		/**
     * Delete the cookie that is not splitted to ensure that auth0 uses only
     * the splitted ones.
     */
		cy._clearAuth0Cookie();
	} else {
		cy._clearAuth0SplittedCookies().then(() => {
			/**
			 * Set one main cookie because its value does not exceed the max size,
			 * and nextjs-auth0 needs at least one cookie to be set.
			 */
			cy.setCookie(SESSION_COOKIE_NAME, value, COOKIE_OPTIONS);
		});
	}
});

I have been stuck on this for a few days now and there doesn’t seem to be much support online for testing using nextjs-auth0 without hitting the endpoints.

1 Like