Run tinyidp as a local OIDC provider, configure clients, define fixture users, and troubleshoot relying-party tests.
This guide explains how to use tinyidp as a local OpenID Connect provider for applications that act as relying parties. It focuses on operational use: choosing an issuer, configuring clients and redirects, defining deterministic fixture users, and diagnosing common failures.
Use tinyidp help developer-guide if you want to modify tinyidp itself. Use tinyidp help reference when you need a complete field or endpoint lookup.
tinyidp implements the OIDC authorization-code flow, OAuth device-code flow, and opt-in DPoP sender-constrained tokens for local development and integration tests. It serves discovery, JWKS, authorize, device authorization, token, userinfo, logout, health, and debug endpoints. It stores all runtime state in memory and generates signing keys at startup.
The default server is intentionally local:
tinyidp serve-dev
This starts an issuer at http://localhost:5556 and listens on 127.0.0.1:5556. The default client is dev-client, a public client with no secret and permissive local redirect URIs.
issuer and addr are different values.
| Field | Meaning | Example |
|---|---|---|
issuer | URL advertised in discovery and written into ID tokens as iss. | http://127.0.0.1:19087/realms/personal-inbox |
addr | TCP address the server binds to. | 127.0.0.1:19087 |
For a root issuer, they usually share the same host and port:
tinyidp serve-dev \
--issuer http://127.0.0.1:19087 \
--addr 127.0.0.1:19087
For a path-based issuer, only issuer contains the path:
tinyidp serve-dev \
--issuer http://127.0.0.1:19087/realms/personal-inbox \
--addr 127.0.0.1:19087
Path-based issuers are URL-shape compatibility. They make discovery advertise endpoints under the issuer path and make the server mount those routes. They do not change claim semantics or enable provider-specific behavior.
Every authorize request names a client and a redirect URI. tinyidp rejects unknown clients and redirects that are not allowlisted.
The built-in clients are:
| Client | Use | Secret | PKCE |
|---|---|---|---|
dev-client | First local tests and permissive development flows. | none | optional |
public-spa | Browser SPA tests that should require PKCE. | none | required |
web-app | Confidential web-app tests. | dev-secret | optional |
You can configure one client through flags or config files:
tinyidp serve-dev \
--client-id personal-inbox-local \
--redirect-uris http://127.0.0.1:19794/auth/callback
If the configured client ID matches a built-in client, tinyidp merges your redirect URIs into the built-in while preserving its defining behavior. For example, adding a redirect to public-spa does not remove its PKCE requirement.
Config files put OIDC settings under the oidc section:
oidc:
issuer: http://127.0.0.1:19087
addr: 127.0.0.1:19087
client-id: personal-inbox-local
redirect-uris:
- http://127.0.0.1:19794/auth/callback
users-file: examples/users/personal-inbox-users.yaml
Run with:
tinyidp serve-dev --config-file examples/configs/personal-inbox-root.yaml
Inspect without starting the server:
tinyidp print-config --config-file examples/configs/personal-inbox-root.yaml
Checked-in examples live under examples/configs/:
| File | Purpose |
|---|---|
dev-root.yaml | Basic root-issuer development setup. |
personal-inbox-root.yaml | xgoja personal-inbox setup with a root issuer. |
personal-inbox-realm.yaml | xgoja personal-inbox setup with a path-based issuer. |
public-spa-pkce.yaml | Built-in public SPA client with PKCE required. |
confidential-web-app.yaml | Built-in confidential web app with dev-secret. |
users-file paths are currently resolved relative to the process working directory. Run checked-in examples from the tinyidp repository root, or pass an absolute path.
Without a users file, any login name derives a synthetic user. That is enough for many tests. Use a users file when you need fixed subjects, fixed emails, fixture passwords, or predictable authorization claims.
users:
- login: alice
password: alice-password
sub: user-alice-fixed
email: alice@example.test
name: Alice Inbox
email_verified: true
groups: [inbox-users]
roles: [writer]
tenant: personal
preferred_username: alice
locale: en-US
Start with:
tinyidp serve-dev --users-file examples/users/personal-inbox-users.yaml
Seeded users override built-ins with the same login. If you define alice, logging in as alice uses your deterministic fixture, not the built-in synthetic Alice.
Passwords are optional local fixture values. If a seeded user has no password, any submitted password is accepted. If a seeded user has password: alice-password, authorize POST must submit that exact value.
Wrong or missing fixture passwords return:
HTTP 401 invalid login or password
No session and no authorization code are created. Built-in and fallback users remain permissive because they have no configured password.
These passwords are not production credentials. They exist so tutorial flows and negative tests can exercise credential-shaped behavior without introducing a real account system.
The top-level fields groups, roles, tenant, preferred_username, and locale expand into ordinary top-level ID token and userinfo claims. They are provider-neutral and are intended for common authorization fixtures.
Use the raw claims map for unusual shapes:
users:
- login: carol
claims:
feature_flags: [compact-inbox]
app_role: maintainer
If the same claim name appears in a generic field and in claims, the explicit claims value wins. omit_claims deletes claims from ID token and userinfo after claims are assembled.
DPoP lets a client bind an access token to a proof key. Include a valid DPoP proof JWT on the /token request to receive token_type: DPoP. The resulting access token must be used at /userinfo with Authorization: DPoP <token> and another proof signed by the same key.
The /userinfo proof must include ath, the base64url SHA-256 hash of the access token. tinyidp also rejects replayed proof jti values. Existing bearer flows continue to work when no DPoP header is present.
See tinyidp help tutorial-dpop for the full request shape and implementation limits.
Device authorization lets a non-browser client ask the user to approve in a browser. Start the flow with:
curl -sS -X POST http://localhost:5556/device_authorization \
-d client_id=dev-client \
-d 'scope=openid profile email offline_access' | jq .
Show the returned verification_uri and user_code to the user, or open verification_uri_complete directly. The approval page uses the same scenario registry and seeded-user fixture password behavior as browser login. For example, with examples/users/personal-inbox-users.yaml, approve as alice / alice-password.
The device polls the token endpoint with:
curl -sS -X POST http://localhost:5556/token \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d client_id=dev-client \
-d device_code="$DEVICE_CODE" | jq .
Before approval, the token endpoint returns authorization_pending. Too-fast polling returns slow_down. Approval returns bearer tokens; denial returns access_denied; expiry returns expired_token; reusing a consumed device code returns invalid_grant.
See tinyidp help tutorial-device-authorization for a complete walkthrough.
The debug endpoints are loopback-only. They show enough state to diagnose a test without exposing full secrets:
curl -s http://localhost:5556/debug | jq .
curl -s http://localhost:5556/debug/sessions | jq .
curl -s http://localhost:5556/debug/codes | jq .
curl -s http://localhost:5556/debug/tokens | jq .
curl -s http://localhost:5556/debug/device-grants | jq .
Use /debug/reset to clear in-memory state between tests:
curl -s -X POST http://localhost:5556/debug/reset
Use /debug/jwks-mode to simulate JWKS failures:
curl -s -X POST http://localhost:5556/debug/jwks-mode \
-H 'Content-Type: application/json' -d '{"mode":"500"}'
| Problem | Cause | Solution |
|---|---|---|
| Discovery issuer does not match the client configuration. | A stale server is still listening, or the RP is configured with a different issuer URL. | Check ss -ltnp, kill stale tinyidp processes, and fetch discovery manually. |
redirect_uri not allowed for this client. | The app callback URL is not in redirect-uris. | Add the exact callback URL, including scheme, host, port, and path. |
| Users file cannot be read. | The relative path was resolved against a different working directory. | Run from the tinyidp repo root or pass an absolute --users-file. |
| Password-backed user returns 401. | The form did not submit the configured fixture password. | Submit the exact fixture password or remove password from the seeded user. |
| PKCE client fails at authorize. | public-spa requires code_challenge. | Use a proper PKCE-capable RP or use dev-client for a first test. |
| Debug endpoint returns 403. | The request is not from loopback. | Call debug endpoints from the same host. |
tinyidp help getting-started — first run and first login.tinyidp help tutorial-first-rp-login — walk through the full browser login flow.tinyidp help tutorial-seeded-users-and-claims — build deterministic users and claims.tinyidp help tutorial-device-authorization — walk through device-code approval and polling.tinyidp help tutorial-dpop — obtain and use DPoP-bound access tokens.tinyidp help tutorial-xgoja-personal-inbox — use tinyidp with the xgoja personal-inbox examples.tinyidp help reference — complete lookup reference.