Fake OAuth 2.0 authorization server · for CI
Your pipeline can't reach your real identity provider. TokenDock issues real RS256-signed JWTs from fake credentials — so the app under test validates tokens with its normal middleware, and your JWT-protected flows, browser logins included, finally run in CI.
The problem
Every service you test in CI expects a bearer token signed by your identity provider — which lives behind a VPN, rate-limits you, or simply shouldn't be handed to a build runner. So teams stub out auth, mark those tests skip, and ship JWT-handling code that has never actually been exercised.
TokenDock replaces the identity provider, not your auth code. It speaks enough real OAuth 2.0 — client credentials, authorization code and refresh token grants, JWKS, userinfo, OIDC discovery — that your app validates its tokens, and runs its login, exactly as it would in production. No test flags. No mocked middleware. No code changes.
Bill of lading
Add TokenDock to your workflow's services: block or run it anywhere Docker runs. It boots in milliseconds with a built-in demo client, or takes your clients via env vars or YAML.
Your service or test calls POST /token with its client ID and secret — the standard client credentials grant, Basic or form-body auth, RFC 6749 errors and all.
The app under test discovers signing keys from /.well-known/jwks.json via OIDC discovery and verifies the RS256 signature — the same code path it runs in production.
Boarding pass · new in v1.6
Point your app's OIDC login at TokenDock like any identity provider. It approves every sign-in on the spot and sends the browser straight back, so Playwright and Cypress tests have nothing to click.
The browser lands on /authorize, as it would at your real provider. Send login_hint to choose who signs in.
No login screen, nothing to click: the browser returns to your callback with a code and your state.
A normal POST /token. PKCE verifiers are checked, and each code works once.
An access token, plus an OIDC ID token when you ask for openid. Both verify against the JWKS.
When a test should pick the user on screen, set TOKENDOCK_INTERACTIVE_LOGIN=true and fill in one field:
await page.getByLabel("Subject").fill("alice");
await page
.getByRole("button", { name: "Sign in" })
.click();
Quickstart
# zero config: demo client included
docker run -p 8080:8080 ghcr.io/iamluisj/tokendock:latest
# fetch a signed JWT
curl -u tokendock:tokendock-secret \
-d grant_type=client_credentials \
http://localhost:8080/token
services:
tokendock:
image: ghcr.io/iamluisj/tokendock:latest
ports: ["8080:8080"]
env:
TOKENDOCK_CLIENT_ID: my-service
TOKENDOCK_CLIENT_SECRET: ci-secret
TOKENDOCK_SCOPES: read,write
TOKENDOCK_AUDIENCE: my-api
Prefer a one-liner? The composite action starts the container, waits for health, and hands your next step the issuer, token-endpoint, and jwks-uri as outputs.
Cargo manifest
Starts with a demo client and a loud warning. Real clients come later; a working token comes in 30 seconds.
Defaults, then YAML file, then env vars — later wins. Single-client setups need no file at all.
Audience, subject, lifetime, and arbitrary claims like roles — so you can test authorization logic, not just authentication.
The authorization code flow with PKCE and OIDC ID tokens. Sign-ins are approved instantly, or turn on a one-field login page to pick the user.
Client credentials, authorization code (PKCE, OIDC ID tokens), rotating refresh tokens, RFC 8693 token exchange, and RFC 7523 JWT bearer grants, JWT client assertions, OIDC userinfo, RFC 9068 access tokens (typ: at+jwt, switchable), OIDC discovery, JWKS with proper kid, RFC 6749 error responses. Your JWT library already understands it.
A fresh RSA-2048 keypair per start — nothing to leak, nothing to rotate. Mount a PEM if you need reproducibility.
A static Go binary in a bare container — no shell, no OS packages. amd64 + arm64, Docker HEALTHCHECK built into the binary. Works in GitHub Actions, GitLab CI, and docker-compose alike.
Harbor traffic
The established vessel here is navikt/mock-oauth2-server — mature, capable, and the right choice for plenty of crews. Here's an honest manifest of the differences.
| Line item | TokenDock | mock-oauth2-server |
|---|---|---|
| Runtime & image | ~4 MB static Go binary no OS layer | ~200 MB JVM image Kotlin |
| Cold start | Milliseconds | Seconds JVM startup |
| Grant types | Client credentials, authorization code, refresh token, token exchange, JWT bearer PKCE · OIDC ID tokens · RFC 8693 · RFC 7523 | Authorization code, token exchange, JWT bearer, refresh & more |
| Interactive login page | Opt-in by default /authorize approves instantly | Yes for browser-driven E2E tests |
| Embed in test code | Container only | JVM library JUnit-friendly |
| Issuers | One per container | Multiple per instance |
| Configuration | Zero-config default, then env vars or YAML | JSON or programmatic API |
| CI wrapper | Composite GitHub Action health-wait + outputs | — |