Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mox-oauth2

A tiny, multi-issuer mock OAuth 2.0 / OpenID Connect provider for local development and integration tests — written in Rust, shipping as a few-MB FROM scratch container.

Point your app's OIDC client at it instead of a real identity provider and run complete login flows offline: authorization code + PKCE, RS256-signed ID & access tokens, a real JWKS endpoint, /userinfo, and DPoP sender-constrained tokens — no accounts, no passwords, no setup.

Why another mock IdP?

Existing lightweight mocks are single-issuer; the multi-issuer ones are heavyweight runtimes (hundreds of MB). mox-oauth2 is the small and multi-issuer and Rust option:

  • Multi-issuer by path/issuers/<name>/...; each issuer has its own discovery doc, keys, and clients. Test an app that talks to several IdPs at once, with isolation.
  • Tiny — a few-MB static binary and scratch image, starts instantly. Pure-Rust crypto (no OpenSSL/ring), so the container is just the static binary.
  • Spec-correct enough for real client libraries — absolute discovery URLs, matching iss, RFC-compliant error responses, published JWKS. Conformant libraries accept it.

Notes & limitations

This is a mock — deliberately not a real IdP:

  • No credential verification: any email "logs in". sub = FNV1a(lowercased email) mod 1e6.
  • State is in-memory (auth sessions, codes, DPoP jtis) and not persisted or evicted — fine for tests, restart to reset.
  • One RSA signing key per instance, generated on first run and cached to mox_oauth2.prv. Mount a volume to persist it across restarts.
  • Refresh tokens are not implemented yet.

Quick start

Docker

docker build -t mox-oauth2 .
docker run --rm -p 8090:8090 mox-oauth2

From source

cargo run                       # listens on 0.0.0.0:8090

Then fetch a discovery document:

curl http://localhost:8090/issuers/google/.well-known/openid-configuration

Configuration

Env var Default Purpose
BASE_URL http://localhost:8090 Public origin used to build absolute discovery URLs and iss.
ISSUERS_CONFIG static/builtin_issuers.toml Path to the issuers definition (bind-mount to customize).
HEADLESS unset When truthy (1/true), /authorize skips the login page and auto-approves — for non-interactive e2e tests.

The server listens on 0.0.0.0:8090.

Headless mode (CI / e2e)

With HEADLESS=1, GET /authorize skips the HTML login form and immediately redirects back with a code — no page scraping needed. The user identity is taken from the login_hint query parameter (default headless@mock.local):

GET /issuers/google/authorize?client_id=...&redirect_uri=...&response_type=code
    &scope=openid%20email&state=xyz&login_hint=alice@example.com
=> 303 redirect to redirect_uri?code=...&state=xyz

Then exchange the code at /token as usual. Your test driver never has to render a page.

Issuers

Issuers are declared in a TOML file — the table name becomes the issuer id in the URL path:

[google]
client_ids = ["my-web-app", "my-mobile-app"]
claims_supported = ["sub", "email", "email_verified", "name"]

[acme]
client_ids = ["internal-dashboard"]
claims_supported = ["sub", "email"]

This exposes /issuers/google/... and /issuers/acme/..., each as an independent IdP.

Endpoints

All endpoints are scoped per issuer under /issuers/<issuer>:

Path Method Description
/.well-known/openid-configuration GET OIDC discovery document
/.well-known/jwks.json GET Signing public key (JWK Set)
/authorize GET/POST Login page; issues the authorization code
/token POST Exchanges code → ID + access token
/userinfo GET/POST Claims for the access token

End-to-end example (authorization code)

BASE=http://localhost:8090
ISS=$BASE/issuers/google

# 1. Open /authorize in a browser (or script the form POST). Submit any email —
#    no password is checked; the email deterministically maps to a stable `sub`.

# 2. After login you're redirected to redirect_uri?code=<CODE>&state=...

# 3. Exchange the code:
curl -s -X POST $ISS/token \
  -d grant_type=authorization_code \
  -d code=<CODE> \
  -d redirect_uri=https://your-app/callback \
  -d client_id=my-web-app
# => { "access_token": "...", "id_token": "...", "token_type": "Bearer", ... }

# 4. Call userinfo:
curl -s $ISS/userinfo -H "Authorization: Bearer <ACCESS_TOKEN>"
# => { "sub": "656198", "email": "you@example.com", "email_verified": true, ... }

The same email always yields the same sub (case/whitespace-insensitive), so tests are reproducible across runs and restarts.

PKCE

If the client sends code_challenge/code_challenge_method at /authorize, the matching code_verifier is required and verified at /token (S256 and plain, RFC 7636).

DPoP (RFC 9449)

Send a DPoP proof header on the token request and the issued access token is sender-constrained via a cnf.jkt claim and returned as token_type: "DPoP". /userinfo then requires a valid DPoP proof whose key matches the binding and whose ath matches the token.

HTTPS / production

The server speaks plain HTTP and keeps no TLS stack (that's what keeps it tiny). Most OAuth2 libraries require an https issuer except for localhost, so:

  • Local use: the default http://localhost:8090 is accepted as-is.
  • Anywhere else: run behind a TLS-terminating reverse proxy and set BASE_URL=https://your-host so the discovery URLs and iss are HTTPS.

License

0BSD

About

A tiny, multi-issuer mock OAuth 2.0 / OpenID Connect provider for local development and integration tests — written in Rust, shipping as a few-MB FROM scratch container.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages