Sign inSign up

curroya/oauth2-mock

By curroya

•Updated about 3 hours ago

A tiny OpenID Connect provider for local development and end-to-end tests.

Image
Security
Developer tools
0

524

curroya/oauth2-mock repository overview

⁠oauth2-mock

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-mockDexmock-oauth2-server (JVM)
Image~52 MB~100 MB~350 MB
Idle memory~3 MiB~25 MB~150–300 MB
Sign inClick a user, type any email, or pass login_hintEmail and password formForm or login_hint
UsersOptional list; any email worksStatic list with bcrypt hashesAny username
Signing keyStable across restartsNew on every bootNew on every boot

⁠Quick start

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.

⁠Tags

TagPoints to
latestThe newest release
vX.Y.ZOne release, never moved; pin this in CI

Every tag is a multi-platform image for linux/amd64 and linux/arm64.

⁠Docker Compose

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

⁠Environment variables

VariableDefaultPurpose
ISSUERhttp://localhost:5556The exact iss claim and the base of the server-to-server endpoints; its path is where the routes are mounted
BROWSER_BASE_URLISSUERBase of authorization_endpoint, for when the browser reaches the mock under another host than your backend
CLIENT_IDlocal-clientThe one accepted client
CLIENT_SECRETlocal-secretIts secret, sent as client_secret_basic or client_secret_post
USERSnoneComma-separated emails to list on the sign-in page
USERS_FILEnonePath inside the container to a JSON list of users
KEY_SEEDoauth2-mockSeed of the deterministic RSA signing key
TOKEN_LIFETIME_SECONDS3600Lifetime of ID and access tokens
PORT5556Listen port

The container exits at startup, naming the variable, when a value is invalid or USERS_FILE cannot be read.

⁠Users file

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]" }
]
ClaimFrom the listFallback
subidSHA-256 of the trimmed, lowercased email: first 32 hex digits in UUID layout
namenameThe part of the email before @
emailemail, trimmed and lowercased—
email_verifiedalways true—
  • USERS and USERS_FILE combine; the file wins when an email is in both.
  • The fallback sub is the same on every restart and every machine, so an email always maps to the same account.

⁠Signing in

RequestResult
GET /auth?… without login_hintThe 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 /authThe sign-in page's submit; login_hint carries the email
  • Any email signs in, listed or not.
  • Codes are single-use and live in memory.
  • PKCE with S256 is mandatory; a request without it gets 400.
  • A wrong code_verifier, a reused code, or a different redirect_uri gets invalid_grant.

⁠Endpoints

EndpointPurpose
GET {ISSUER}/.well-known/openid-configurationDiscovery document
GET {ISSUER}/keysJWKS with the one RS256 key
GET/POST {BROWSER_BASE_URL}/authAuthorization endpoint and sign-in page
POST {ISSUER}/tokenauthorization_code grant
GET {ISSUER}/healthzok
  • The token response carries access_token (random, opaque), token_type: Bearer, expires_in and id_token.
  • The ID token carries iss, sub, aud, iat, exp, nonce when one was sent, email, email_verified and name.

⁠End-to-end tests

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()
  • Each email is a stable identity, so give every parallel worker its own address.
  • Backends that forward login_hint to the provider skip the page entirely.

⁠Not supported

  • Refresh tokens, client_credentials, userinfo and logout.
  • More than one client.
  • plain PKCE or flows without PKCE.

Tag summary

Content type

Image

Digest

sha256:3842cdbfa…

Size

9.8 MB

Last updated

about 3 hours ago

docker pull curroya/oauth2-mock