Testing
Run the WorkOS API locally with the emulator and learn what to cover when testing your AuthKit integration — sign-in flows, sessions, webhooks, and failure handling.
Introduction
Your authentication code deserves the same test coverage as the rest of your app. Pointing tests at the live WorkOS API is a poor fit for that: it requires network access and real credentials in CI, accumulates state between runs, and can’t be forced to fail on demand.
WorkOS Emulate solves this. It’s an open source, in-memory emulator of the WorkOS API that runs on your machine, implements the full AuthKit login story – hosted authorize, code exchange, sessions, organization selection, signed webhooks – and lets you inject failures to exercise your error handling. Event names and payload shapes are generated from the WorkOS API specification, so what your tests see matches production.
This guide covers running the emulator, then – just as important – what to test.
The emulator is a development and testing tool. It keeps all data in memory, performs no real authentication, and should never be exposed to production traffic or seeded with production secrets.
The emulator is a plain HTTP server, so it works with every WorkOS SDK – point the SDK’s base URL at the emulator instead of https://api.workos.com.
Self-contained binaries for macOS, Linux, and Windows are also attached to each GitHub release.
The emulator listens on http://localhost:4100 and accepts the API key sk_test_default by default. Use GET /health as a readiness check when starting it in the background:
| Flag | Description |
|---|---|
--port <port> |
Port to listen on (default 4100) |
--seed <path> |
Seed file with users, organizations, roles, webhook endpoints, and more |
--interactive |
Serve real login pages for browser-based end-to-end tests |
--signing-key <path> |
Pin the RSA signing key so tokens and JWKS stay stable across restarts |
--issuer <url> |
Pin the iss claim on minted tokens |
--json |
Machine-readable output for scripts and CI |
Override the SDK’s base URL in your test configuration and use the SDK as normal, so that requests hit the emulator instead of the real API.
The same pattern works for any language with a WorkOS SDK, as all of them expose a base URL override.
Tests should not depend on state left behind by earlier runs. Declare the users, organizations, roles, and permissions your tests need in a seed file, and the emulator recreates that exact world on every boot:
Both organizations and users accept an optional id. Pin ids to match what your real WorkOS environment emits, so a backend whose database already references a real organization or user id lines up with the emulator – and stays stable across restarts.
For JavaScript test suites, createEmulator starts an emulator in-process on a random port, so parallel test files never collide:
Error hooks force the emulator to return non-200 responses so you can test how your app handles any rare WorkOS API failures. Register them in the seed file, over HTTP at runtime, or programmatically:
Hooks match a method and path (exact, prefix wildcard like /user_management/*, or *), return a status of your choosing with an optional custom body, and can auto-remove after count uses.
Register a webhook endpoint and every resource creation and authentication outcome fires a signed webhook, exactly like production – WorkOS-Signature: t=<timestamp>,v1=<hmac>, verifiable with the official SDKs’ constructEvent unchanged.
Codes that WorkOS would deliver by email arrive in the webhook payload instead: magic_auth.created carries the Magic Auth code, password_reset.created the reset token, and email_verification.created the verification code. Your test can drive an entire login flow from webhooks alone, with no email provider in the loop.
Delivery is fire-and-forget with no retries, so poll your receiver in tests rather than asserting immediately. All events can also be queried at GET /events (filter with ?events[]=user.created).
By default the authorize endpoints immediately redirect back to your callback with a code – ideal for API-level tests. For browser tests, pass --interactive and the emulator serves a real login page instead:
This works in headless browsers and requires no dashboard configuration or real identity provider.
WorkOS tests AuthKit itself – the hosted UI, the token issuance, the protocol plumbing. Your job is the integration seam: the routes, session handling, and authorization logic you wrote. Focus your coverage there.
| Area | What to verify |
|---|---|
| Callback route | Code exchange creates a session; error redirects are handled |
| Session lifecycle | Expired tokens refresh; rotated refresh tokens are stored; logout revokes |
| Protected routes | Unauthenticated requests are rejected; authenticated ones pass |
| Authorization | Role and permission claims gate access; multi-organization users work |
| Webhook handlers | Invalid signatures are rejected; duplicate deliveries are idempotent |
| Failure handling | API errors and timeouts degrade gracefully instead of signing users out |
The OAuth callback is the front door of your integration. Verify that a valid code is exchanged for a session and the user lands where you expect:
- WorkOS can redirect back with an
errorparameter instead of acode(for example, when a user cancels). Your callback should show something sensible, not a stack trace. - If you pass
state, assert that a missing or tampered value is rejected.
Sessions fail in ways that only show up over time, so simulate time passing instead of waiting for it:
- An expired access token triggers a refresh and the request succeeds transparently.
- Your app stores the new refresh token after every refresh. Refresh tokens may rotate in production; the emulator always rotates them and invalidates the old one, so a client that keeps using a stale token fails locally instead of in production.
- Logout clears your session state and revokes the WorkOS session, not just one of the two.
Test your middleware or route guards from both sides: an unauthenticated request to a protected route is redirected or rejected, an authenticated request passes, and public routes stay public. These tests are cheap and catch the most embarrassing class of bug – a route that silently lost its protection.
If your app reads role, permissions, or custom claims from the access token, test the decisions your code makes with them:
- A user with the right permission gets through; one without it is denied.
- Users who belong to multiple organizations receive the
organization_selection_requiredresponse – verify your app completes the selection flow instead of failing. The emulator reproduces this exactly as production does. - If you use JWT templates, seed the same template into the emulator and assert your code reads the custom claims correctly.
Webhook endpoints are publicly reachable, so their tests are security tests:
- A request with a missing, malformed, or wrongly-signed
WorkOS-Signatureheader is rejected. - The same event delivered twice does not double-apply – deliveries are at-least-once, so handlers must be idempotent.
- Events arriving out of order (an
organization_membership.updatedbefore theuser.createdyour handler expects) don’t crash or corrupt state.
The WorkOS API will occasionally be slow or unavailable, and your app’s behavior at that moment is a product decision worth pinning down in a test. Use error hooks to force each case:
- A
5xxor timeout during token refresh must not destroy the session – treat it as transient and retry, reserving sign-out for a terminalinvalid_grant. See session resilience for the full pattern. - Rate limits (
429) and outages (503) are retried with backoff where you expect them to be. - Validation errors (
422) surface actionable feedback to the user rather than a generic failure.
The emulator covers unit, integration, and local end-to-end tests. For a final staging pass against the real API, you can set up a dedicated environment:
- Create a separate WorkOS environment for staging or CI so test data can’t leak into production and API keys stay scoped.
- Seed it from a declarative YAML file with
workos seedin the WorkOS CLI, which can also tear everything down cleanly afterwards. - Keep credentials in your CI secret store – sandbox API keys are still secrets.