In Blog #34, we tested Route Handlers at the HTTP boundary, including validation, status codes and authorization-sensitive endpoints. Authentication adds another layer: tests must prove not only that a user can sign in, but also that anonymous users stay out, authenticated users reach the correct resources, and identities cannot cross authorization boundaries.
Next.js authentication testing is strongest when each risk is tested at its natural level. Pure permission decisions belong in fast unit tests. Session integration belongs near the real server boundary. Login, logout, cookies and redirects need a browser. None of these layers alone proves complete security.
How Do You Test Authentication in Next.js?
Test pure permission and session-related business logic with Vitest, then use Playwright to verify real login, logout and protected-route flows in the browser. Include negative tests for anonymous users, incorrect roles and resource ownership. Use dedicated test accounts and environments, and never commit real session cookies, tokens or authentication state.
Authentication Testing at a Glance
A useful suite follows the same trust path as the product: identity is presented, verified, persisted as a session and checked again when a protected operation runs. Authorization must occur at the point of data access or mutation, not only in navigation or button visibility.
Authentication vs Authorization
Authentication asks who the caller is. Authorization asks what that caller may do. A page may redirect an anonymous browser to login instead of displaying a raw 401; an API usually expresses the distinction with response status codes.
| Question | Authentication | Authorization |
|---|---|---|
| Main question | Who are you? | What may you do? |
| Example | Sign in | Access admin page |
| Typical data | Session/user identity | Role, permission or ownership |
| Failure | Not authenticated | Not permitted |
| Common HTTP concept | 401 | 403 |

Understand Your Authentication Architecture
Start with Blog #13 for the core Next.js authentication architecture, then inspect the implementation rather than assuming the tutorial stack. Before writing a test, inventory the installed provider and version, session strategy, login methods, cookie configuration, user model, OAuth callbacks, proxy.ts or existing middleware, protected layouts and pages, Server Actions, Route Handlers, role helpers and ownership rules. Read the installed lockfile and source; never select an article example and retrofit the application around it.
Current Next.js guidance separates authentication, session management and authorization. It recommends centralizing secure checks near data access, while Proxy can perform optional optimistic checks. In Next.js 16 documentation the file convention is proxy.ts; do not create middleware.ts blindly or treat a layout-only check as the final security boundary.
What Should Be Unit, Integration and E2E Tested?
import { describe, expect, it } from 'vitest'
import { canAccessAdmin } from './permissions'
describe('canAccessAdmin', () => {
it('allows an admin', () => expect(canAccessAdmin('admin')).toBe(true))
it('denies a normal user', () => expect(canAccessAdmin('user')).toBe(false))
})Test Roles, Permissions and Resource Ownership
Role labels are only useful when connected to actual capabilities. Build the matrix from production policy, then test every meaningful transition. Ownership deserves its own cases because a logged-in user may still be forbidden from reading or changing another user's record.
| User | Resource owner | Expected |
|---|---|---|
| User A | User A | Allow |
| User B | User A | Deny |
| Anonymous | User A | Deny |
Testing Login, Failure and Signup
A login E2E test should verify the page, required accessible fields, submission with a dedicated test identity, intended redirect and visible authenticated outcome. It should not assert a raw session-token value. Failure cases cover missing input, wrong test password, unknown synthetic user and a safely controlled provider failure without revealing whether a real production account exists.
Cover signup only when the application actually has signup. Test validation, duplicate-account behavior, verification requirements and post-signup navigation in an isolated environment. Never send real email unless a provider sandbox is part of the approved test architecture.
Testing Logout
Logout is a state transition, not merely a click assertion. Start authenticated, invoke the supported logout control, confirm the logged-out UI, then navigate directly to a protected URL. The former session must no longer grant access, including after reload or in a new request using the same browser context.
Testing Sessions, Expiration and Cookies
Session tests should prove the authenticated identity is recognized, invalid or expired state is rejected, logout invalidates state and authorization data is interpreted correctly. Control time through the provider's documented mechanism or a test clock; do not use long sleeps and never forge production tokens.
Where cookies carry session state or an opaque session identifier, test configured properties such as HttpOnly, environment-appropriate Secure, SameSite, path and expiry. Assert names or attributes only where they are part of the contract. Do not print cookie values into assertion messages, traces or reports.
Testing Protected Pages and Server Components
For every protected page, cover anonymous access and authenticated access. If roles matter, add the lowest-privilege authenticated identity that must be denied. For a Server Component that reads a session, prefer a pure policy test plus integration or Playwright coverage of the rendered page; Blog #33 explains why async server boundaries need different evidence from client components.
Testing Protected Server Actions
Hiding a form or button is not authorization. A protected Server Action must validate input, authenticate, authorize the specific operation, mutate only after approval and then revalidate or redirect. Tests should exercise anonymous, allowed, unauthorized and ownership cases while verifying that denied attempts leave state unchanged.
Testing Protected Route Handlers
At the API boundary, missing or unacceptable authentication generally maps to 401, while an authenticated identity without permission generally maps to 403. Follow the application's real contract; it may deliberately use 404 to conceal resource existence. Test the handler with no identity, valid allowed identity and valid disallowed identity, then assert both response and protected state.
Multiple Roles and Admin Routes
Never use a production admin account. Create a least-privilege normal test user and a dedicated admin identity in the isolated test environment. Verify anonymous and normal users are denied before proving the test admin's intended access.
Playwright Authentication and storageState
Use UI login in tests whose purpose is to verify login. For tests that begin after login, Playwright recommends authenticating once in a setup project and reusing saved state when tests can safely share one account. The state file may contain sensitive cookies or headers, so place it in a git-ignored directory and never attach its contents to reports.
import { test as setup, expect } from '@playwright/test'
import path from 'node:path'
const authFile = path.join(import.meta.dirname, '../playwright/.auth/user.json')
setup('authenticate test user', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill(process.env.E2E_USER_EMAIL!)
await page.getByLabel('Password').fill(process.env.E2E_USER_PASSWORD!)
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page).toHaveURL(/dashboard/)
await page.context().storageState({ path: authFile })
})
OAuth and Provider Boundaries
OAuth E2E testing is often less stable than local credentials because another organization controls the UI, anti-bot policy and account lifecycle. Prefer the provider's supported sandbox or a controlled boundary for most tests, with a small contract test for callback, account linking and safe failure behavior. Never capture authorization codes, access tokens or refresh tokens in screenshots or logs.
Test Redirects and CSRF-Sensitive Flows
Test the expected post-login destination, an internal return path and a malicious external destination. The application should reject or normalize untrusted destinations instead of creating an open redirect. For cookie-authenticated mutations, connect the suite to the protections described in Blog #27 on CSRF protection: test the actual SameSite policy, origin validation or anti-CSRF mechanism without weakening production settings.
Mock Authentication Without Mocking Away Security
A component test may replace the identity boundary to render signed-in and signed-out UI. A Server Action test may replace the session lookup to isolate a policy branch. But a mocked identity proves only collaboration with that mock. Retain integration and browser coverage that exercises the real provider or supported test mechanism.
Test Users, Data Isolation and Error States
Give each test worker unique users or unique resources when tests mutate shared state. Seed only the roles and records a case needs, clean up within a scoped disposable environment, and never let cleanup target production. Error tests should cover unavailable identity services, expired state and malformed input only through supported, controllable mechanisms.
Authentication Tests in CI
Run cheap deterministic checks first, then build and start the application with isolated services before the browser suite. Inject test-only credentials through encrypted CI secrets. Generate authentication state during the job, use it only for dependent tests and remove the environment with the job. A passing suite is evidence about tested boundaries, not proof of complete application security.
Common Authentication Testing Mistakes
- Testing successful login but no anonymous or unauthorized denial.
- Assuming authenticated means authorized.
- Checking hidden UI without protecting the server operation.
- Inventing Auth.js, Clerk or custom JWT behavior before inspecting dependencies.
- Creating
middleware.tswithout checking the currentproxy.tsarchitecture. - Hard-coding passwords, tokens, cookies or OAuth secrets.
- Committing Playwright storage state.
- Using production users, admin accounts or customer data.
- Sharing mutable users across parallel workers.
- Logging complete response headers that contain Set-Cookie.
- Waiting in real time for session expiry.
- Mocking every auth boundary and claiming end-to-end security.
Next.js Authentication Testing Best Practices
- Inventory the real provider, version and session strategy first.
- Separate authentication, authorization and ownership cases.
- Keep secure checks close to protected data and mutations.
- Unit-test deterministic permission and redirect rules.
- Test protected pages, Server Actions and Route Handlers at their real boundaries.
- Use UI login only when login is the behavior under test.
- Reuse isolated, ignored storageState for appropriate dependent tests.
- Use dedicated test users and non-production infrastructure.
- Assert denied attempts leave protected state unchanged.
- Protect traces, videos, screenshots and CI logs.
- Re-read current Next.js, Playwright and provider documentation during upgrades.
Testing & Quality Hub
#31 Vitest & React Testing LibraryPublished
#32 Playwright E2E TestingPublished
#33 Server Components, Client Components & Server Actions TestingPublished
#34 Route Handlers & API TestingPublished
#35 Authentication TestingPublished
#36 Database TestingPlanned
#37 Mocking APIs & External ServicesPlanned
#38 Playwright Authentication & User FlowsPlanned
#39 Testing with GitHub ActionsPlanned
#40 Production Testing StrategyPlanned


