A fake OIDC provider for Playwright and CI end-to-end tests

Let your browser tests sign in as any of a fixed set of test users, with real OpenID Connect redirects and real signed tokens, without automating a third-party login page.

The problem

End-to-end tests that sign in through a real identity provider are slow and flaky. Login pages change, bot detection and MFA get in the way, test accounts get locked, and secrets have to live in CI. Mocking authentication inside the app avoids that, but then the tests no longer exercise your real OIDC integration.

Why HeyIdentity fits

Set it up

  1. Sign up and open your management link. Use a configuration dedicated to your tests.
  2. Add a client for the app under test, with the redirect URIs your test environment uses (for example http://localhost:3000/callback). Use PKCE for browser apps.
  3. Add the users your tests need, each with a stable key, for example admin with roles: ["Admin"] and reader with roles: ["Reader"].
  4. Point the app under test at your configuration in the test environment only, for example through an environment variable:
    OIDC_AUTHORITY=https://app.heyidentity.dev/t/{tenantRouteId}/c/{configurationId}
    OIDC_CLIENT_ID=my-app-e2e

Sign in from a Playwright test

HeyIdentity's user selection page has one button per user, with a stable test ID authorize-btn-{user key} (lowercase) and the user's display name as its text.

import { test, expect, type Page } from '@playwright/test';

async function signInAs(page: Page, userKey: string) {
  await page.goto('/');
  await page.getByRole('button', { name: 'Sign in' }).click();   // your app's login button
  await page.getByTestId(`authorize-btn-${userKey}`).click();    // HeyIdentity's user selection page
  await page.waitForURL('**/');                                   // back in your app
}

test('admins see the admin area', async ({ page }) => {
  await signInAs(page, 'admin');
  await page.goto('/admin');
  await expect(page.getByRole('heading', { name: 'Admin' })).toBeVisible();
});

test('readers are denied the admin area', async ({ page }) => {
  await signInAs(page, 'reader');
  await page.goto('/admin');
  await expect(page.getByText('Access denied')).toBeVisible();
});

To sign in once per user instead of once per test, run signInAs in a setup project and save storageState per user.

Refresh tokens and sign-out

Request offline_access (and allow it on the client) to get a refresh token, so tests can cover silent renewal. Each refresh rotates the refresh token, as many production providers do. Sign-out through the advertised end_session_endpoint redirects back to any registered URI with your state, so logout flows can be tested end to end.

Verify it

  1. Run a single test headed (npx playwright test --headed) and watch the redirect to HeyIdentity's user list and back.
  2. Decode the ID token in your app's session (or log the claims) and confirm sub, name and roles match the user you picked.
  3. See the same flow without writing code in demo case A.

Limitations

Make your sign-in tests boring

Sign up to get a tenant and a configuration for your test suite.

Sign up See demo case A