Skip to content
Migrating from NextAuth.js v4? Read our migration guide.
GuidesTesting

Testing

Repeated and consistent testing of authentication has always been tricky. OAuth providers in particular are especially difficult to test in an automated fashion, because they often introduce additional verification steps that will trigger if you’re logging in from a new geographic location, a datacenter IP address, or from a new user-agent, etc.

To get around these limitations, we recommend you use one of the following strategies to run successful E2E tests against Auth.js applications.

  1. Run your own OAuth provider using software like Keycloak
  2. Enable an authentication method like the Credentials provider in development mode

Below are one example of each strategy, leveraging @playwright/test for the automated E2E tests.

Keycloak

First, set up a Keycloak instance. Then you have to add the Keycloak provider to your Auth.js configuration.

This test requires two environment variables to be set. These credentials should be for a test user who can authenticate against your newly created Keycloak instance.

.env
TEST_KEYCLOAK_USERNAME=abc
TEST_KEYCLOAK_PASSWORD=123

Then we can use @playwright/test to execute two test steps.

  1. Which will visit the signin URL, enter the authentication credentials, and then click the “Sign In” button. It ensures the session is then set correctly.
  2. Which will click the “Sign Out” button and ensure the session is then null.
tests/e2e/basic-auth.spec.ts
import { test, expect, type Page } from "@playwright/test"
 
test("Basic auth", async ({ page, browser }) => {
  if (
    !process.env.TEST_KEYCLOAK_USERNAME ||
    !process.env.TEST_KEYCLOAK_PASSWORD
  )
    throw new TypeError("Incorrect TEST_KEYCLOAK_{USERNAME,PASSWORD}")
 
  await test.step("should login", async () => {
    await page.goto("http://localhost:3000/auth/signin")
    await page.getByText("Keycloak").click()
    await page.getByText("Username or email").waitFor()
    await page
      .getByLabel("Username or email")
      .fill(process.env.TEST_KEYCLOAK_USERNAME)
    await page.locator("#password").fill(process.env.TEST_KEYCLOAK_PASSWORD)
    await page.getByRole("button", { name: "Sign In" }).click()
    await page.waitForURL("http://localhost:3000")
    const session = await page.locator("pre").textContent()
 
    expect(JSON.parse(session ?? "{}")).toEqual({
      user: {
        email: "bob@alice.com",
        name: "Bob Alice",
        image: "https://avatars.githubusercontent.com/u/67470890?s=200&v=4",
      },
      expires: expect.any(String),
    })
  })
 
  await test.step("should logout", async () => {
    await page.getByText("Sign out").click()
    await page
      .locator("header")
      .getByRole("button", { name: "Sign in", exact: true })
      .waitFor()
    await page.goto("http://localhost:3000/auth/session")
 
    expect(await page.locator("html").textContent()).toBe("null")
  })
})

Credentials Provider in Development

This method requires less initial setup and maintenance as you do not need to maintain a separate OAuth provider (like Keycloak), but you also must be extremely careful that you do not leave insecure authentication methods available in production. For example, in this example we will be adding a Credentials provider which accepts the password password.

auth.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import Credentials from "next-auth/providers/credentials"
 
const providers = [GitHub]
 
if (process.env.NODE_ENV === "development") {
  providers.push(
    Credentials({
      id: "password",
      name: "Password",
      credentials: {
        password: { label: "Password", type: "password" },
      },
      authorize: (credentials) => {
        if (credentials.password === "password") {
          return {
            email: "bob@alice.com",
            name: "Bob Alice",
            image: "https://avatars.githubusercontent.com/u/67470890?s=200&v=4",
          }
        }
      },
    })
  )
}
 
export const { handlers, auth } = NextAuth({
  providers,
})

The above configuration example will add the GitHub provider all the time, and the Credentials provider only in development. After making that configuration tweak, we can write our @playwright/test tests just like above.

tests/e2e/basic-auth.spec.ts
import { test, expect, type Page } from "@playwright/test"
 
test("Basic auth", async ({ page, browser }) => {
  if (!process.env.TEST_PASSWORD) throw new TypeError("Missing TEST_PASSWORD")
 
  await test.step("should login", async () => {
    await page.goto("http://localhost:3000/auth/signin")
    await page.getByLabel("Password").fill(process.env.TEST_PASSWORD)
    await page.getByRole("button", { name: "Sign In" }).click()
    await page.waitForURL("http://localhost:3000")
    const session = await page.locator("pre").textContent()
 
    expect(JSON.parse(session ?? "{}")).toEqual({
      user: {
        email: "bob@alice.com",
        name: "Bob Alice",
        image: "https://avatars.githubusercontent.com/u/67470890?s=200&v=4",
      },
      expires: expect.any(String),
    })
  })
 
  await test.step("should logout", async () => {
    await page.getByText("Sign out").click()
    await page
      .locator("header")
      .getByRole("button", { name: "Sign in", exact: true })
      .waitFor()
    await page.goto("http://localhost:3000/auth/session")
 
    expect(await page.locator("html").textContent()).toBe("null")
  })
})
Auth.js © Balázs Orbán and Team - 2024