A tiny OpenID Connect provider for local development and end-to-end tests.
524
A tiny OpenID Connect provider for local development and end-to-end tests: the authorization-code flow with PKCE, signed RS256 ID tokens, and a one-click sign-in page.
Do not use in production. Anyone who can reach oauth2-mock can sign in as any user without a password, and with the default KEY_SEED anyone can forge its ID tokens. It is for testing and development only. The examples publish it on 127.0.0.1 so only your machine can reach it.
With love from the curroya.es team: a simple way to find temporary jobs near you.
| oauth2-mock | Dex | mock-oauth2-server (JVM) | |
|---|---|---|---|
| Image | ~52 MB | ~100 MB | ~350 MB |
| Idle memory | ~3 MiB | ~25 MB | ~150–300 MB |
| Sign in | Click a user, type any email, or pass login_hint | Email and password form | Form or login_hint |
| Users | Optional list; any email works | Static list with bcrypt hashes | Any username |
| Signing key | Stable across restarts | New on every boot | New on every boot |
docker run --rm -p 127.0.0.1:5556:5556 -e [email protected],[email protected] curroya/oauth2-mock
Point your OIDC client at the issuer http://localhost:5556, with client id local-client and secret local-secret.
| Tag | Points to |
|---|---|
latest | The newest release |
vX.Y.Z | One release, never moved; pin this in CI |
Every tag is a multi-platform image for linux/amd64 and linux/arm64.
OIDC clients compare iss byte for byte. When your backend reaches the mock as http://oidc:5556 inside Docker and the browser as http://localhost:5556, keep one issuer and move only the browser-facing endpoint:
services:
oidc:
image: curroya/oauth2-mock
ports: ["127.0.0.1:5556:5556"]
environment:
ISSUER: http://oidc:5556
BROWSER_BASE_URL: http://localhost:5556
CLIENT_ID: my-app
CLIENT_SECRET: my-app-secret
USERS_FILE: /users.json
volumes:
- ./users.json:/users.json:ro
| Variable | Default | Purpose |
|---|---|---|
ISSUER | http://localhost:5556 | The exact iss claim and the base of the server-to-server endpoints; its path is where the routes are mounted |
BROWSER_BASE_URL | ISSUER | Base of authorization_endpoint, for when the browser reaches the mock under another host than your backend |
CLIENT_ID | local-client | The one accepted client |
CLIENT_SECRET | local-secret | Its secret, sent as client_secret_basic or client_secret_post |
USERS | none | Comma-separated emails to list on the sign-in page |
USERS_FILE | none | Path inside the container to a JSON list of users |
KEY_SEED | oauth2-mock | Seed of the deterministic RSA signing key |
TOKEN_LIFETIME_SECONDS | 3600 | Lifetime of ID and access tokens |
PORT | 5556 | Listen port |
The container exits at startup, naming the variable, when a value is invalid or USERS_FILE cannot be read.
Mount a JSON list and point USERS_FILE at it. name and id are optional:
[
{
"name": "Candidate",
"email": "[email protected]",
"id": "6f8d3c1e-0b2a-4f5c-9d7e-1a2b3c4d5e6a"
},
{ "email": "[email protected]" }
]
| Claim | From the list | Fallback |
|---|---|---|
sub | id | SHA-256 of the trimmed, lowercased email: first 32 hex digits in UUID layout |
name | name | The part of the email before @ |
email | email, trimmed and lowercased | — |
email_verified | always true | — |
USERS and USERS_FILE combine; the file wins when an email is in both.sub is the same on every restart and every machine, so an email always maps to the same account.| Request | Result |
|---|---|
GET /auth?… without login_hint | The sign-in page: one button per configured user, plus an email field |
GET /auth?…&[email protected] | Straight back to redirect_uri with code and state, no page |
POST /auth | The sign-in page's submit; login_hint carries the email |
S256 is mandatory; a request without it gets 400.code_verifier, a reused code, or a different redirect_uri gets invalid_grant.| Endpoint | Purpose |
|---|---|
GET {ISSUER}/.well-known/openid-configuration | Discovery document |
GET {ISSUER}/keys | JWKS with the one RS256 key |
GET/POST {BROWSER_BASE_URL}/auth | Authorization endpoint and sign-in page |
POST {ISSUER}/token | authorization_code grant |
GET {ISSUER}/healthz | ok |
access_token (random, opaque), token_type: Bearer, expires_in and id_token.iss, sub, aud, iat, exp, nonce when one was sent, email, email_verified and name.Click a configured user on the sign-in page:
await page.getByRole('button', { name: /candidate@example\.test/ }).click()
Or type any email, which needs no configuration:
await page.getByLabel('Email').fill('[email protected]')
await page.getByRole('button', { name: 'Sign in' }).click()
login_hint to the provider skip the page entirely.client_credentials, userinfo and logout.plain PKCE or flows without PKCE.Content type
Image
Digest
sha256:3842cdbfa…
Size
9.8 MB
Last updated
about 3 hours ago
docker pull curroya/oauth2-mock