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
Frequently Asked Questions
How do I test authentication in Next.js 16?
Unit-test pure access rules, integration-test session and server boundaries, and use Playwright for real login, logout and protected-route journeys.
Should I use Vitest or Playwright for authentication?
Use both where the risk justifies it: Vitest gives fast feedback on rules, while Playwright proves browser, cookie, redirect and routing behavior.
How do I test a Next.js login page?
Use a dedicated test account in an isolated environment, submit the real form, verify its user-visible result and confirm a protected page becomes accessible.
How do I test logout?
Log in, sign out through the supported UI, revisit a protected URL and verify the application redirects or denies access according to its contract.
How do I test protected routes?
Cover anonymous denial, authenticated access and unauthorized-role denial when roles exist. Assert the final safe destination and protected content visibility.
How do I test protected Server Components?
Test pure authorization helpers separately and use integration or browser coverage for the rendered server boundary instead of forcing unsupported component rendering.
How do I test authentication in Server Actions?
Call the action through a controlled server test or browser flow and cover allowed, anonymous, unauthorized, invalid-input and ownership cases that exist.
How do I test authenticated Route Handlers?
Send requests with missing and valid test identity, then verify 401, 403 or success according to the documented API contract and unchanged state after denial.
What is the difference between authentication and authorization?
Authentication establishes identity. Authorization determines whether that identity may access a route, action or resource.
What is the difference between 401 and 403?
401 normally means acceptable authentication is missing; 403 means the caller is identified but lacks permission. Page routes often redirect instead.
How do I test user roles?
Build a role-to-capability matrix from production policy, create isolated test identities and test every meaningful allow and deny boundary.
How do I test admin routes?
Prove anonymous users and normal test users are denied, then verify a dedicated non-production admin test user can access only the intended surface.
How do I test resource ownership?
Create a resource for user A, request it as user A and user B, and verify that the cross-user attempt is denied without changing or revealing protected data.
What is Playwright storageState?
It is serialized browser context state that can include cookies and local storage, allowing selected tests to begin authenticated.
Is Playwright storageState safe to commit?
No. Treat it as sensitive, generate it for the test environment, keep it outside source control and delete or rotate it appropriately.
Should every Playwright test log in through the UI?
No. Use UI login for login-specific tests; reuse isolated authenticated state for tests whose purpose begins after login.
How do I test OAuth authentication?
Test your callback, account-linking and failure behavior with provider-supported sandboxes or controlled boundaries. Do not automate real personal accounts.
Should I use production accounts for E2E tests?
No. Use dedicated accounts and disposable data in an isolated environment with no access to real customer information.
How do I store test login credentials in CI?
Use the CI platform's encrypted secret store, limit access and lifetime, and prevent traces, screenshots and logs from exposing values.
Should authentication tests run in CI?
Yes. Run fast policy tests on each change and selected isolated browser flows after the application is built and started safely.
Current Official References
- Next.js authentication guide
- Next.js testing guides
- Next.js Playwright guide
- Next.js cookies reference
- Next.js Proxy guide
- Playwright authentication guide
- Vitest guide
Next Steps
Audit the real application and choose one representative protected surface from each layer it actually uses. Begin with an anonymous denial, an allowed test user and the highest-risk role or ownership boundary. Coming next: Next.js 16 Database Testing with PostgreSQL, Test Databases & Integration Tests.
