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
- Your app runs its normal OIDC code against a real discovery URL. Nothing is mocked inside the app.
- "Signing in" is one click on a test user, so it's fast and stable to automate. There is no password to store in CI.
- Users, roles and claims are fixed in the configuration, so every run sees the same identities.
- Tokens are signed and published through JWKS, so your token validation runs exactly as it does in production.
Set it up
- Sign up and open your management link. Use a configuration dedicated to your tests.
- 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. - Add the users your tests need, each with a stable key, for example
adminwithroles: ["Admin"]andreaderwithroles: ["Reader"]. - 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
- Run a single test headed (
npx playwright test --headed) and watch the redirect to HeyIdentity's user list and back. - Decode the ID token in your app's session (or log the claims) and confirm
sub,nameandrolesmatch the user you picked. - See the same flow without writing code in demo case A.
Limitations
- With several users, signing in needs one click on the user selection page. Pinning a user in your own browser skips it, but that is a Pro feature, a per-browser convenience and not an API, and its cookie format is not a contract. For CI, use a configuration with a single user: it never shows a picker, on every plan.
- The public demo configurations are shared and only accept the demo site's redirect URIs. Use your own tenant for your tests.
- Requests are rate limited per source and per tenant, and nothing retries a rejected sign-in automatically: a browser sign-in that gets a 429 shows an error page, and MSAL and Microsoft.Identity.Web throw instead of waiting. How your suite gets tokens decides which plan it fits:
- One sign-in per test user, reused for the whole run (Playwright storage state, or a cached token): Free.
- One sign-in per test class or fixture: Pro, within its burst.
- A fresh sign-in or token request per test: not recommended on any plan. From one runner it is stopped by the per-source limits, which no plan raises.
- For development and testing only. Never use HeyIdentity to protect production identities.
Make your sign-in tests boring
Sign up to get a tenant and a configuration for your test suite.