ProductHow it worksPricingBlogDocsLoginFind Your First Bug
Diagram showing a NextAuth session cookie being minted and injected into a Playwright storageState fixture, bypassing the OAuth round-trip on a dark background
TestingAuthenticationNext.js+1

How to Test NextAuth (Auth.js) Login Flows

Tom Piaggio
Tom PiaggioCo-Founder at Autonoma

NextAuth (now Auth.js in v5) testing skips the OAuth round-trip in CI by minting the session cookie directly and injecting it via Playwright's storageState. The session token lives in a cookie named next-auth.session-token (v4) or authjs.session-token (v5), and that name change is itself the clearest example of how minted-cookie fixtures can silently break between versions.

Running the full OAuth dance in CI is a trap. You depend on a third-party provider you don't control, you get rate-limited or blocked by bot detection, and the redirect round-trip adds seconds to every test that needs an authenticated session. For a team with 50 tests that each sign in fresh, that adds up fast. There is a better path.

The right approach for NextAuth and Auth.js is to mint the session on your side and hand it to Playwright as a storageState fixture. Your tests start authenticated without touching an identity provider. For the one case where you actually want to drive a sign-in form (the credentials provider), you can do it end-to-end in CI because credentials don't redirect anywhere. This post covers both patterns, and then the part nobody talks about: where they rot.

For the broader picture of Playwright auth patterns across multiple providers, see the guide to Playwright authentication testing. For Next.js-specific Playwright configuration, the Next.js Playwright testing guide has the project setup.

How NextAuth Stores a Session

NextAuth supports two session strategies: JWT (the default) and database.

With the JWT strategy, NextAuth encodes the session object as an encrypted JSON Web Encryption token (JWE). This is not a plain, readable JWT. It uses the NEXTAUTH_SECRET environment variable (or AUTH_SECRET in v5) as the encryption key. The resulting token is stored in a cookie. On every request, NextAuth decrypts the cookie server-side to read the session. No database lookup is required.

With the database strategy, NextAuth stores a session row in your database keyed by a sessionToken value. The cookie holds only that token string. On each request, NextAuth queries the database to look up the session. The cookie is shorter and opaque, but the session data lives in your DB rather than in the token itself.

The cookie name changed between major versions. In NextAuth v4, the cookie is next-auth.session-token. In Auth.js v5, it became authjs.session-token. On HTTPS, both versions add a __Secure- prefix, so the production cookie name is __Secure-next-auth.session-token or __Secure-authjs.session-token respectively. In local development over HTTP, the prefix is absent. This naming difference is exactly the kind of version drift that silently breaks a minted-cookie fixture when you upgrade.

Diagram comparing the NextAuth JWT session strategy, where an encrypted JWE cookie holds the session with no database lookup, against the database strategy, where the cookie holds only a sessionToken and the session row is looked up per request, including the v4 next-auth.session-token versus v5 authjs.session-token cookie names

The JWT strategy keeps the session inside an encrypted cookie, while the database strategy stores only a token that points to a row your app looks up on every request.

Minting the Session Cookie for Tests

For the JWT strategy, minting means encoding a session object using the same secret your app uses, then writing the result into the right cookie name.

A small minting helper does the work. It encodes the session with the encode helper from the next-auth/jwt package (or the Auth.js equivalent), using your NEXTAUTH_SECRET (or AUTH_SECRET in v5) as the key. The session object you encode needs a user field (name, email, image) and an expires field matching what NextAuth sets on the real token. The helper writes the resulting encrypted JWE into the cookie (next-auth.session-token for v4 or authjs.session-token for v5) inside a Playwright storageState object, then saves it to disk so tests reuse it across the suite without re-generating on every run.

For the database strategy, the approach is different: insert a session row directly in your test database with a known sessionToken, then write only that token string into the cookie. The actual session data lives in the DB row you inserted. Use the same expiry you'd set for a real session, and clean up the row in your test teardown.

Autonoma can use the same app-side SDK/data factory pattern for stateful auth scenarios. You connect your own create and teardown functions through @autonoma-ai/sdk factories, create users with the exact characteristics a test needs, authenticate scenarios with those users, and tear them down at the end of the run. The SDK pattern uses defineFactory, createHandler, create, and teardown that calls userRepo.delete(user.id).

Once you have the cookie, pass the storageState to Playwright's browser context before navigating to any protected route. Tests that use the fixture skip authentication entirely. For a suite with multiple roles (admin, regular user, read-only), generate one fixture per role and load the appropriate one per test. Save each fixture to a dedicated file so they can be reused across workers without regenerating.

A reusable fixture built with test.extend loads the saved per-role state file (for example admin.json or user.json) and sets it on the browser context. Each test then declares which role it needs and starts already signed in as that user, with no login step in the test body.

Diagram of the mint-and-inject flow: encode the session with your secret, write the session-token cookie, save it into a Playwright storageState file per role, and start tests already authenticated while skipping the OAuth round-trip

Minting encodes a session once and saves it as a reusable storageState file, so every test starts authenticated without driving the login flow.

Testing the Credentials Provider Path

The credentials provider is the only NextAuth provider you can drive fully end-to-end in CI without involving a third party. It accepts email and password directly and returns a session without any redirect to an external identity provider.

A Playwright spec drives this path end-to-end. It navigates to /api/auth/signin, fills the email and password fields from environment variables, submits, and asserts that the next-auth.session-token (or authjs.session-token) cookie is set afterward. You can verify the cookie is present in the browser context, assert that the response redirected to the expected post-login destination, and check that a protected route now renders the authenticated content rather than bouncing back to the sign-in page.

Keep the test account credentials in environment variables, not hardcoded. If your credentials provider validates against a real database, run the test against a seeded test database. If it validates against a static lookup (common in examples), ensure the lookup is consistent between environments.

This end-to-end path is worth running in CI even if you use the minted-cookie approach for the bulk of your tests. It confirms that the sign-in form, the credentials handler, and the session creation all work together. The minted fixture skips all of that. If your credentials handler breaks, the fixture tests stay green while real users can't sign in.

For a comparison with how other providers approach this (including Auth0's Resource Owner Password grant pattern), the guide on how to test Auth0 login covers the equivalent approach for Auth0.

How Autonoma Handles NextAuth Testing Without the Drift Risk

The minting approach solves one real problem (speed) and creates another (version sensitivity). When NextAuth or Auth.js changes the session token format, the JWE encoding parameters, or the cookie name, every minted fixture in your test suite produces a cookie the app can no longer decode. The app sees an invalid session and redirects to sign-in. Every test that relied on the fixture fails. The failure looks like a test infrastructure problem, not an auth regression.

Autonoma takes a different path. Our Executor agent drives the real browser login flow the way a user would. It navigates to the sign-in page, fills credentials, submits, and lets NextAuth set the session cookie naturally. There is no minted cookie to drift out of sync. The session is always a real one, produced by the same code path your users hit.

Our Planner agent reads your Next.js routes and components, identifies which paths require authentication, and generates test cases that exercise both the authenticated and unauthenticated states. The Reviewer agent evaluates results, separating real auth failures from agent errors. The Diffs Agent maintains the test suite as your Next.js or Auth.js version changes, catching the cookie-name rename or the token-format update at the diff level before the tests start failing. The result is auth coverage that doesn't require maintaining a minting helper aligned to your library's internals.

Where This Breaks: Version Drift

Minting is fast and reliable until you upgrade. The failure modes are specific and worth knowing before they hit you.

Diagram of how version drift breaks tests: a v4 to v5 upgrade renames the cookie or changes the JWE format, the minted cookie no longer matches, the app ignores it, and every authenticated test redirects to signin and fails

A single version upgrade can invalidate every minted fixture at once, turning an auth regression into what looks like a test infrastructure failure.

The cookie-name change between v4 and v5 is the most visible one. If your fixture hardcodes next-auth.session-token and you upgrade to Auth.js v5, the app now looks for authjs.session-token. Your fixture writes a cookie the app ignores. Every authenticated test fails. Nothing in the test output tells you it's a cookie-name mismatch. You see "redirected to /api/auth/signin" and have to trace backward.

The JWE encoding format is the subtler one. NextAuth uses the jose library for JWE. If NextAuth changes the algorithm, the key derivation, or the payload structure between versions, a token encoded with the old parameters fails to decrypt on the new version. The app treats it as an invalid token and starts a fresh session flow. This is harder to detect because the error surface is inside NextAuth's decryption logic, not in your test assertions.

The session object shape can also shift. In v5, the default JWT payload structure changed. Fields moved. If your minting helper encodes a v4-shaped payload against a v5 app, the session callback receives unexpected data and may produce a degraded or empty session.

Detecting drift before it breaks your suite requires either locking your NextAuth version and testing upgrades deliberately, or running one end-to-end sign-in test (the credentials provider test from the section above) on every PR. That test doesn't mint anything. It exercises the real sign-in path. If the minting approach breaks after an upgrade, the credentials-provider test still passes, and you at least know the app's auth stack is functional.

The broader guide to Playwright authentication testing covers the maintenance story for auth test fixtures in more detail, including the session-validation fixture pattern for detecting token expiry.

FAQ

The fastest approach is to mint the session token directly and inject it into Playwright via storageState, bypassing the OAuth round-trip entirely. For the credentials provider, you can drive the sign-in form end-to-end in CI without involving a third-party provider. For protected routes, assert that unauthenticated requests redirect to the sign-in page and that an authenticated session reaches the expected destination.

Yes. NextAuth.js rebranded to Auth.js starting with v5. The library now supports multiple frameworks beyond Next.js. Version 4 (still widely deployed) is still called NextAuth.js; v5 and later use the Auth.js name. The cookie name changed between versions: next-auth.session-token in v4, authjs.session-token in v5.

With the JWT session strategy (the default), you encode a session object using the same secret NextAuth uses and write the result into the correct cookie name: next-auth.session-token for v4 or authjs.session-token for v5. With the database strategy, you insert a session row and write the sessionToken value into the cookie. Either way, you then pass the cookie via Playwright's storageState so tests start already authenticated.

Yes, two ways. First, mint the session cookie and inject it via storageState so no login flow runs at all. Second, use the credentials provider, which accepts email and password directly without redirecting to a third-party identity provider. The credentials provider is the only NextAuth provider you can drive end-to-end in CI without a third party.

Load the storageState fixture containing your minted session before navigating to the protected route. Playwright will send the session cookie with the request. Assert that the page renders the authenticated content rather than redirecting to the sign-in page. For the unauthenticated case, navigate without a storageState fixture and assert the redirect to your configured sign-in URL.

Related articles

Diagram showing a Playwright auth setup flow: a login-once setup test saves storageState to a JSON file, multiple parallel worker tests load the state and skip login entirely, cutting per-test auth time from seconds to milliseconds

Playwright Authentication: Cut Login Time by 80% with storageState

Playwright authentication with storageState: log in once, reuse auth state, and cut test login time by 60-80%. Covers multi-role, API login, Clerk, Auth0, and Supabase.

Next.js and Playwright testing framework integration showing end-to-end testing setup

Next.js Playwright Testing: Full Guide

Learn how to test Next.js apps with Playwright. App Router, Server Components, API routes, authentication flows, and best practices with TypeScript.

Checklist of login page test cases covering password authentication, OAuth, SSO, OTP, and magic link flows with pass/fail indicators on a dark background

Test Cases for a Login Page: The 2026 Checklist (Including OAuth, SSO, and Magic Links)

Complete login page test cases for 2026: positive, negative, security, OAuth, SSO, OTP, and magic link scenarios. Includes a copyable test-case table and a maintenance guide.

Diagram showing three OTP testing patterns: provider bypass code, test phone number, and API interception, arranged as branching paths on a dark background

How to Test OTP Login Flows Without Reading the SMS

How to test OTP login flows: use a provider bypass code, a test phone number, or API interception. Assert on expiry, replay, and rate limits. A practical guide.