Stop 13 · Testing the Full StackTopics · 6/6

Testing Authentication Flows

Part of the Full Stack Developer Roadmap

In short

Auth bugs are the scary kind: a logged-out user sees someone's dashboard, or nobody can log in after a refactor. A few focused tests catch both before your users do.

Specifically testing login, signup, protected routes and token expiry — since auth bugs are high-impact and easy to accidentally break while refactoring either side of the stack.

The blob character tests a fan of coloured pass cards on a card reader at a society boom barrier.

Think of it like this

Auditing the society gate

A good security audit doesn't just check that residents get in. It tries an expired pass, no pass at all, and a visitor pass trying to reach the terrace. Testing auth is the same: test the happy path, then every way someone should be stopped.

Watch it happen

Three auth tests, three outcomes

Playwright
Browser
App + API

goto('/dashboard')

1/7Test 1: a logged-out visitor opens a protected page.

The big ideas

What you need to know

Test the stops, not just the doors

For each protected route, test logged out (401 or redirect), wrong role (403) and the right user (200).

Expire sessions on purpose

Use a short expiry in the test environment or fake timers to check refresh works and expired sessions are rejected.

Log in once, reuse it

Playwright's storageState saves cookies after one login, so other tests start signed in without retyping.

Seed test users

Create known users and roles in a test database before the run. Never test against real accounts.

See it in code

Walk through the code

ts
1import { test, expect } from "@playwright/test";2 3test("logged-out visitors are sent to login", async ({ page }) => {4  await page.goto("/dashboard");5  await expect(page).toHaveURL(/\/login/);6});7 8test("a user can log in and reach the dashboard", async ({ page }) => {9  await page.goto("/login");10  await page.getByLabel("Email").fill("asha@test.local");11  await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD!);12  await page.getByRole("button", { name: "Log in" }).click();13  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();14});15 16// API check, as a signed-in normal user: expect((await request.get("/api/admin/users")).status()).toBe(403);

1/3The 'stopped at the gate' test: no cookie, so the protected page must redirect to login.

Set baseURL in playwright.config.ts so goto('/login') hits your local app. The test password comes from an env variable, never from the code.

Save yourself the pain

Common mistakes

Oops

Only testing that login works.

Do this instead

Also test the failures: wrong password, logged out, expired session and wrong role. That's where data leaks hide.

Oops

Logging in through the UI at the start of every single test.

Do this instead

Log in once in a setup step, save storageState, and reuse it. Keep one UI test for the login form itself.

Your mission

Guard the gate with tests

~40 min

Quick check

A signed-in normal user calls DELETE /api/users/5, which is admin-only. What should your test expect?

Go deeper

  1. 1Write an E2E test that logs in and confirms access to a protected page.
  2. 2Write a test confirming an expired or invalid token correctly redirects to login.
  3. 3Test that a logged-out user genuinely cannot reach protected API endpoints, not just protected pages.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge