Configuration, clients, endpoints, and behaviors — the lookup reference for everything tinyidp does.
This is the lookup reference for tinyidp: how it is configured, which
clients it ships, which endpoints it exposes, and the behaviors those
endpoints implement. It is organized for finding a specific fact, not for
reading top to bottom. For a guided introduction, start with
tinyidp help getting-started; for the scenario catalog, see
tinyidp help scenarios.
tinyidp is configured through the Glazed command framework. A reusable
oidc field section defines the provider settings; it is composed into
the serve-dev and print-config commands so flags, environment variables,
and config-file schema never drift between them.
| Flag | Env | Default | Purpose |
|---|---|---|---|
--issuer | TINYIDP_ISSUER | http://localhost:5556 | Issuer URL; endpoints are derived from it. Path-based issuers such as http://localhost:5556/realms/demo are supported. |
--addr | TINYIDP_ADDR | 127.0.0.1:5556 | Listen address (loopback by default; set 0.0.0.0:5556 for LAN). |
--client-id | TINYIDP_CLIENT_ID | dev-client | Client ID. If it matches a builtin, the config is merged into it. |
--client-secret | TINYIDP_CLIENT_SECRET | (empty) | If set, /token enforces it; if empty, the client is public. |
--redirect-uris | TINYIDP_REDIRECT_URIS | http://localhost:3000/callback,http://127.0.0.1:3000/callback | Allowlist of redirect URIs (repeat the flag or pass a list). |
--users-file | TINYIDP_USERS_FILE | (empty) | Optional YAML/JSON file with seeded users and claims. |
From lowest to highest:
--config-file)TINYIDP_*)A local override always wins. Use tinyidp serve-dev --print-parsed-fields
to see which source won each field, or tinyidp print-config to print
the resolved configuration without starting the server.
A YAML config file layers above profiles and below env/flags. The fields
live under the oidc section slug:
oidc:
issuer: http://localhost:5556
addr: 127.0.0.1:5556
client-id: dev-client
client-secret: dev-secret
redirect-uris:
- http://localhost:8080/callback
users-file: ./users.yaml
Pass it with tinyidp serve-dev --config-file config.yaml.
Portable examples are checked in under examples/configs/:
| File | Purpose |
|---|---|
dev-root.yaml | Basic root-issuer setup for local development. |
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 URL. |
public-spa-pkce.yaml | Builtin public SPA client while preserving PKCE-required behavior. |
confidential-web-app.yaml | Builtin confidential web-app client with local dev-secret. |
oidc.users-file is resolved relative to the process working directory. For the checked-in examples, run from the tinyidp repository root or pass an absolute users-file path.
Profiles are named bundles of overrides stored in a YAML file, selected
with --profile. They sit above defaults and below config/env/flags, so
they are a convenient baseline that local overrides always win against.
The file is a YAML map: profile name, then section slug, then fields.
dev:
oidc:
client-id: dev-profile-client
addr: 127.0.0.1:6600
ci:
oidc:
client-id: ci-runner
addr: 127.0.0.1:6601
tinyidp looks for a default file at ~/.config/tinyidp/profiles.yaml
($XDG_CONFIG_HOME/tinyidp/profiles.yaml on Linux). If the default file
is missing and the requested profile is default, loading is skipped
silently — tinyidp works out of the box with no profiles.yaml. A
non-default profile with no file is an error, never a silent fallback.
tinyidp serve-dev --profile dev
TINYIDP_PROFILE=ci tinyidp serve-dev
By default, tinyidp derives a stable synthetic user from whatever login is typed. For integration tests that need fixed subjects or app-specific claims, pass --users-file (or oidc.users-file in config). The file may be YAML or JSON:
users:
- login: alice
sub: user-alice-fixed
email: alice@example.test
name: Alice Inbox
password: alice-password
email_verified: true
groups: [inbox-users]
roles: [writer]
tenant: personal
preferred_username: alice
locale: en-US
- login: bob
sub: user-bob-fixed
email: bob@example.test
name: Bob Inbox
password: bob-password
email-verified: true
groups: [inbox-users]
roles: [reader]
tenant: personal
claims:
feature_flags: [compact-inbox]
Seeded users are registered as normal scenarios. They override builtins with the same login and appear on the login page under "Seeded users" by default.
password is optional. If it is omitted or empty, the seeded user remains permissive and any submitted password is accepted. If it is set, authorize POST must submit the exact fixture password; wrong or missing passwords return 401 invalid login or password and no session or authorization code is created. Passwords are plain local test fixture values, not production credentials.
Generic top-level claim helpers are available for common authorization fixtures: groups, roles, tenant, preferred_username, and locale. These expand into ordinary top-level claims in both the ID token and userinfo response. The raw claims map remains available for provider-specific or unusual shapes; explicit claims entries override generic helper fields with the same claim name. Use omit_claims when a seeded user should deliberately omit a base claim such as email.
| Field | Type | Meaning |
|---|---|---|
login | string | Required login/scenario name. Normalized before registration. |
sub | string | Optional fixed subject. Defaults to deterministic synthetic subject derived from login. |
email | string | Optional fixed email. Defaults to login or login@example.test. |
name | string | Optional display name. Defaults to login local part. |
password | string | Optional fixture password. Empty means any submitted password is accepted. |
email_verified / email-verified | bool | Optional email verification claim. Both spellings are accepted. |
groups | list[string] | Generic top-level groups claim. Whitespace-only entries are dropped. |
roles | list[string] | Generic top-level roles claim. Whitespace-only entries are dropped. |
tenant | string | Generic top-level tenant claim. |
preferred_username | string | Generic top-level preferred username claim. |
locale | string | Generic top-level locale claim. |
claims | object | Raw extra claims. Values here override generic helper fields with the same name. |
omit_claims | list[string] | Claims to delete from ID token and userinfo after extra claims are merged. |
category | string | Login-page quick-pick category. Defaults to Seeded users. |
tinyidp ships a client registry with three builtins, so one running instance can serve a public SPA, a confidential web app, and a permissive dev client simultaneously.
| Client | Type | PKCE | Secret | Post-logout redirect |
|---|---|---|---|---|
dev-client | public | optional | (none) | http://localhost:3000 |
public-spa | public | required | (none) | http://localhost:8080 |
web-app | confidential | optional | dev-secret | http://localhost:8080 |
Each client owns its own redirect-URI allowlist, PKCE requirement, scope allowlist, and post-logout redirect allowlist. A redirect URI is valid for a specific client, not globally, and an authorization code issued to one client cannot be redeemed by another.
When the configured --client-id matches a builtin, the configuration is
merged into the builtin rather than replacing it: the builtin's
class-defining properties (RequirePKCE, Secret, AllowedScopes) are
preserved, and redirect URIs are unioned. A non-matching --client-id
registers a new permissive client.
| Endpoint | Method | Purpose |
|---|---|---|
/.well-known/openid-configuration | GET | Discovery; advertises every endpoint and supported grant/scope/claim. |
/jwks | GET | Public signing keys (three kids). See JWKS rotation below. |
/authorize | GET/POST | Authorization endpoint; GET decides silent vs interactive login, POST submits credentials. |
/device_authorization | POST | OAuth Device Authorization Grant start endpoint; returns a device_code, user_code, verification URIs, expiry, and polling interval. |
/device | GET/POST | Browser approval form for device grants; approves or denies a user_code using normal scenario/seeded-user login semantics. |
/token | POST | Token endpoint; authorization_code, refresh_token, and urn:ietf:params:oauth:grant-type:device_code grants. |
/userinfo | GET | UserInfo; bearer-token-protected claims. |
/end-session | GET | RP-initiated logout. See Logout below. |
/healthz | GET | Liveness (ok). |
/debug/* | GET/POST | Loopback-only introspection. See Debug UI below. |
If --issuer includes a path component, tinyidp registers the same endpoints under that prefix as well as at the root. For example, issuer http://localhost:5556/realms/demo serves discovery at /realms/demo/.well-known/openid-configuration and advertises endpoint URLs under /realms/demo. This includes /device_authorization and /device for device-flow tests. Path-based issuers are URL-shape compatibility only; seeded-user claims remain generic and provider-neutral.
tinyidp implements opt-in DPoP sender-constrained access tokens. If a token request includes a valid DPoP proof JWT, tinyidp computes the proof JWK thumbprint, stores it beside the opaque access token, and returns token_type: DPoP. If the request omits DPoP, tinyidp returns the existing bearer response.
Discovery advertises:
"dpop_signing_alg_values_supported": ["ES256", "RS256"]
DPoP behavior:
typ: dpop+jwt, alg: ES256 or RS256, a public JWK, jti, htm, htu, and fresh iat./userinfo calls for DPoP-bound tokens must use Authorization: DPoP <token> and a fresh DPoP proof whose key matches the token binding./userinfo proofs must include ath, the base64url SHA-256 hash of the access token.jti with the same key is rejected by the in-memory replay cache.For a guided explanation, see tinyidp help tutorial-dpop.
tinyidp implements the OAuth 2.0 Device Authorization Grant for local and integration-test clients. A device starts with POST /device_authorization, then polls /token with grant_type=urn:ietf:params:oauth:grant-type:device_code while the user approves or denies the request in a browser at /device.
Important behavior:
client_id is required and must identify a registered client.openid; client scope allowlists are enforced when configured.XXXX-XXXX; approval normalizes case, spaces, and hyphens.authorization_pending.interval returns slow_down.access_denied.expired_token.invalid_grant.openid, and refresh tokens for offline_access.For a runnable walkthrough, see tinyidp help tutorial-device-authorization.
After a login, tinyidp sets an IdP session cookie and remembers the authentication time. The authorize GET endpoint implements the OIDC session rules:
prompt=none forbids any UI. If a valid, fresh-enough session exists,
a code is issued silently; otherwise the RP receives login_required.prompt=login forces the login form even when a session exists.max_age requires the session's auth_time to be within the given
seconds; otherwise re-authentication is forced.login_hint prefills the login form.The auth_time claim in the ID token is the original login time, not
the token-issuance time. This is what makes max_age checks honest: a
silent re-issue minutes after login still reports the true age of the
session. The cookie is HttpOnly and SameSite=Lax but not Secure,
because tinyidp serve-devs plain HTTP on loopback and a Secure cookie
would never be sent.
When the RP requests the offline_access scope, the token response
includes a refresh_token. Refresh tokens rotate: each use deletes the
presented token and issues a new one. Reusing a rotated token fails
with invalid_grant — the standard reuse signal. A refresh token cannot
be redeemed by a different client than the one it was issued to.
/jwks publishes three keys: dev-key-1 (the active signing key),
rotated-key-2, and bad-sig-key. The SignKey scenario field selects
which key signs the ID token (see tinyidp help scenarios). Because
/jwks is global rather than tied to a login, its failure modes are
server-level, toggled through the debug UI:
curl -X POST http://localhost:5556/debug/jwks-mode \
-H 'Content-Type: application/json' -d '{"mode":"500"}'
Valid modes are normal (default), 500, slow (sleeps 10s), and
empty (returns {"keys":[]}). The debug reset restores normal.
/end-session implements RP-initiated logout. Parameters:
id_token_hint — a previously issued ID token. Its payload is decoded
(signature not re-verified) to find the subject, and any session for
that subject is deleted — even from a client that holds no cookie.post_logout_redirect_uri — a URI to redirect to after logout. It must
be registered for the client; when client_id is given, it is checked
against that client only, otherwise against all clients.state — forwarded on the redirect as a query parameter.The session cookie is always cleared. Without a post_logout_redirect_uri,
tinyidp returns a logged-out page.
The /debug/* endpoints are read-only views of in-memory state plus a
reset, guarded to loopback so a LAN bind does not leak state.
| Endpoint | Method | Returns |
|---|---|---|
/debug | GET | Counts and the current JWKS mode. |
/debug/sessions | GET | Active sessions (login, sub, auth_time, expires). |
/debug/codes | GET | Outstanding authorization codes. |
/debug/tokens | GET | Issued access tokens. |
/debug/device-grants | GET | Pending/approved/denied device grants with redacted device-code prefixes. |
/debug/jwks-mode | GET/POST | Read or set the JWKS failure mode. |
/debug/reset | POST | Clear all sessions, codes, tokens, refresh tokens, and device grants. |
Secrets are shown as 8-character prefixes — enough to correlate a flow against a log without exposing the full token in a listing.
| Problem | Cause | Solution |
|---|---|---|
prompt=none always returns login_required. | The session cookie is not being sent. | tinyidp serve-devs plain HTTP; ensure the cookie is not marked Secure by a proxy, and that the RP and IdP share a context. |
| Config flag has no effect. | A higher-precedence source overrode it. | Run tinyidp serve-dev --print-parsed-fields to see which source won. |
--redirect-uris replaced the builtin's URIs. | It does not — it unions them. | The merge preserves builtin properties; the configured URI is added, not swapped. |
| Debug endpoints return 403. | The request is not from loopback. | Call them from the same host; they are loopback-only by design. |
| Users file cannot be read. | oidc.users-file was relative to a different working directory. | Run from the tinyidp repository root or pass an absolute users-file path. |
| xgoja login redirects but callback fails. | The app base URL does not match the configured redirect URI. | Match TINYIDP_BASE_URL and oidc.redirect-uris, for example http://127.0.0.1:19794/auth/callback. |
tinyidp help getting-started — install and first login.tinyidp help user-guide — operational guide for running tinyidp against relying parties.tinyidp help developer-guide — implementation guide for extending tinyidp.tinyidp help tutorial-first-rp-login — first relying-party login walkthrough.tinyidp help tutorial-seeded-users-and-claims — deterministic users and claim fixtures.tinyidp help tutorial-xgoja-personal-inbox — xgoja personal-inbox smoke workflow.tinyidp help tutorial — a guided walkthrough of scenarios.tinyidp help scenarios — the full scenario catalog and model.