Ryan Gard
Posted on February 28, 2023
One of the biggest headaches around end-to-end testing is dealing with authentication/authorization. With the rapid adoption of Next.js there has also been a corresponding increase in the use of NextAuth which is the preferred method for doing authentication/authorization with Next.js.
The recommended approaches (e.g. NextAuth, Cypress) for authenticating a Cypress test session usually involves going through a full login flow with a given authorization provider. The full login flow approach significantly increases the time to execute a test run and is still subject to failure when the underlying provider changes elements in the login flow interface.
However, if you're using NextAuth with the JWT session strategy you can skip the login flow completely for ANY provider by directly writing the next-auth.session-token
cookie directly which will grant the session full authentication! This approach simplifies end-to-end testing with Cypress as well as reduces test time significantly regardless of the chosen authorization provider.
This article will cover how to configure Cypress to perform end-to-end testing on Next.js web app that has all pages and routes secured by NextAuth.
Example Web App
The full source code of the examples shown in this article can be found here. The example web app is based on the react-note-taking-app built on top of the T3 Stack. The Cypress tests and configuration are based on the best practices documented in the cypress-realworld-app.
This article is only going to cover a narrow portion of source code found in the example repo. If you're looking for more detailed information about some of the source code in the example repo then I suggest referring to the links above.
Configure NextAuth
In the [...nextauth].ts
file enable the JWT session strategy for your chosen provider:
import NextAuth, { type NextAuthOptions } from "next-auth";
import DiscordProvider from "next-auth/providers/discord";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { env } from "../../../env/server.mjs";
import { prisma } from "../../../server/db";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
DiscordProvider({
clientId: env.DISCORD_CLIENT_ID,
clientSecret: env.DISCORD_CLIENT_SECRET,
}),
],
pages: {
signIn: "/auth/login",
signOut: "/",
error: "/auth/unauthorized",
},
session: {
strategy: "jwt",
},
};
export default NextAuth(authOptions);
Configure Cypress
In the cypress.config.ts
file expose the NEXTAUTH_SECRET
to the Cypress.env command and disable chromeWebSecurity
which will allow writing insecure cookies:
import { defineConfig } from "cypress";
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd());
export default defineConfig({
env: {
database_url: process.env.DATABASE_URL,
nextauth_secret: process.env.NEXTAUTH_SECRET,
mobileViewportWidthBreakpoint: 414,
},
e2e: {
baseUrl: "http://localhost:3000",
specPattern: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}",
supportFile: "cypress/support/e2e.ts",
chromeWebSecurity: false,
viewportHeight: 1000,
viewportWidth: 1280,
},
});
Create a Custom Command for NextAuth Login
Create a custom command that will write a properly encoded next-auth.session-token
cookie using the provided NEXTAUTH_SECRET
. (nextAuth.ts
)
import { v4 as uuidv4 } from "uuid";
import { encode } from "next-auth/jwt";
import type { JWT } from "next-auth/jwt";
// Custom command for automagically authenticating using next-auth cookies.
// Note: this function leaves you on a blank page, so you must call cy.visit()
// afterwards, before continuing with your test.
Cypress.Commands.add("loginNextAuth", ({ userId, name, email, provider }: loginNextAuthParams) => {
Cypress.log({
displayName: "NEXT-AUTH LOGIN",
message: [`🔐 Authenticating | ${name}`],
});
const dateTimeNow = Math.floor(Date.now() / 1000);
const expiry = dateTimeNow + 30 * 24 * 60 * 60; // 30 days
const cookieName = "next-auth.session-token";
const cookieValue: JWT = {
sub: userId,
name: name,
email: email,
provider: provider,
picture: `https://via.placeholder.com/200/7732bb/c0392b.png?text=${name}`,
tokenType: "Bearer",
accessToken: "dummy",
iat: dateTimeNow,
exp: expiry,
jti: uuidv4(),
};
// https://docs.cypress.io/api/utilities/promise#Waiting-for-Promises
cy.wrap(null, { log: false }).then(() => {
return new Cypress.Promise(async (resolve, reject) => {
try {
const encryptedCookieValue = await encode({ token: cookieValue, secret: Cypress.env("nextauth_secret") });
cy.setCookie(cookieName, encryptedCookieValue, {
log: false,
httpOnly: true,
path: "/",
expiry: expiry,
});
resolve();
} catch (err) {
console.error(err);
reject();
}
});
});
});
Use the NextAuth Login Command in a Test
Now we can use the custom command to login! (auth.spec.ts
)
import { testCreds } from "../support/testData";
describe("given an unauthenticated session", () => {
before(() => {
cy.resetDatabase();
});
beforeEach(() => {
cy.loginNextAuth(testCreds);
cy.visit("/");
});
describe("when generating a valid JWT and visiting the site", () => {
it("should show the home page of an authenticated user", () => {
cy.findByLabelText("logout-button").should("be.visible");
});
});
});
Conclusion
This method of login is a great way to speed up Cypress tests that are currently using a full login flow. I don't think this method completely replaces the need for testing a full login flow, but for the vast majority of tests it would be best to save time using the method outlined in the article.
Check out the example repo for this article and run the Cypress test suite yourself to get a feel for how fast this login technique can be for your project(s).
Posted on February 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.