Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Registering clients

A client is an application that asks rIDM for tokens. This page covers registering and maintaining clients: every field, the type-driven defaults, secrets and their rotation, private_key_jwt, and dynamic client registration. For the concepts, see Clients.

Client routes are under /admin/tenants/{slug}/clients and need ridm:clients:read or ridm:clients:write. {client} is either the internal id or the public client_id.

Where to register one

WhereHow
ConsoleClients (/console/clients/) → New client: a four-step wizard (kind of client; grants and authentication; URIs; scopes and audiences) with the type’s defaults preselected. The result shows the client ID and any secret once
CLIridm --tenant acme client create --name "Acme SPA" --type spa --redirect-uri https://app.acme.example/callback
APIPOST /admin/tenants/{slug}/clients
Configuration as codeA clients entry in the tenant document
The client itselfDynamic client registration, when the tenant allows it

The console’s client detail page (?client=<id>) saves every field as you go, and the playground (/console/playground/) runs the client’s flow for real and shows the tokens it gets back. Its sign-in opens in a popup (allow pop-ups for the console); the console tab keeps the run in memory, a pasted client secret and the tokens included, and writes none of it to browser storage.

Client types and their defaults

client_type picks the defaults for everything not given. Anything given explicitly wins.

TypeForAuth methodGrantsPKCEScopes
spaBrowser appsnone (public)authorization_code, refresh_tokenrequiredstandard scopes
web (default)Server-side web appsclient_secret_basicauthorization_code, refresh_tokenrequiredstandard scopes
nativeMobile and desktop appsnone (public)authorization_code, refresh_tokenrequiredstandard scopes
machineService-to-serviceclient_secret_basicclient_credentialsnot requirednone
deviceTVs, CLIs, kiosksnone (public)device code, refresh_tokenrequiredstandard scopes

The standard scopes are openid, profile, email, phone, address and offline_access. PKCE (S256) is always required of a public client, whatever require_pkce says.

Fields

POST /admin/tenants/acme/clients
Content-Type: application/json

{
  "name": "Acme Portal",
  "client_type": "web",
  "redirect_uris": ["https://portal.acme.example/auth/callback"],
  "post_logout_redirect_uris": ["https://portal.acme.example/"],
  "allowed_scopes": ["openid", "profile", "email", "offline_access"],
  "allowed_audiences": ["https://api.acme.example"],
  "backchannel_logout_uri": "https://portal.acme.example/auth/backchannel-logout",
  "require_consent": false
}

Identity and display

FieldDefaultNotes
client_idgenerated1–128 characters of A-Z a-z 0-9 . _ : -, starting alphanumeric; cannot change later
namerequired1–255 characters; shown on the consent screen
client_typewebSee the table above
descriptionnullFor administrators
logo_uri, client_uri, tos_uri, policy_urinullShown on the consent screen and in the account console’s application list
statusactivePATCH only: disabled stops the client from getting tokens

Authentication

FieldDefaultNotes
token_endpoint_auth_methodby typenone, client_secret_basic, client_secret_post, private_key_jwt, tls_client_auth, self_signed_tls_client_auth. The client must authenticate with exactly the method registered
tls_client_auth_subject_dn, tls_client_auth_san_dns, tls_client_auth_san_uri, tls_client_auth_san_ip, tls_client_auth_san_emailnullFor tls_client_auth, exactly one: the name its certificate must carry. See Mutual TLS
jwksnullInline JWK set ({"keys": [...]}) for private_key_jwt, signed request objects and ID token encryption
jwks_urinullThe same, fetched over HTTPS

client_credentials needs client authentication, so it cannot be combined with none. private_key_jwt needs jwks or jwks_uri; self_signed_tls_client_auth needs its certificate in jwks (a key’s x5c) or a jwks_uri.

Redirects, logout and CORS

FieldDefaultNotes
redirect_uris[]At least one when authorization_code is allowed. Matched as exact strings, except that a native client’s http loopback redirect may use any port (RFC 8252 §7.3)
post_logout_redirect_uris[]Where RP-initiated logout may return to
cors_origins[]Browser origins (scheme://host[:port], no path) allowed to call the token, userinfo and other endpoints from JavaScript
backchannel_logout_urinullReceives a signed logout token when a session the client took part in ends
frontchannel_logout_urinullLoaded in an iframe on the logout page when a session ends. Browsers that block third-party cookies (Safari, Firefox, private windows in every browser, Android WebView) do not send the application’s own session cookie to that iframe, so it cannot end the application’s session there: use backchannel_logout_uri wherever the application can receive it
initiate_login_urinullWhere a third party may start a login at the client

Redirect and post-logout URIs must be https, http only on a loopback host (localhost, 127.0.0.1, [::1]), or, for native clients only, a custom scheme (RFC 8252). Fragments are refused.

Grants, scopes and audiences

FieldDefaultNotes
allowed_grantsby typeAny of authorization_code, refresh_token, client_credentials, urn:ietf:params:oauth:grant-type:device_code, urn:ietf:params:oauth:grant-type:token-exchange, urn:openid:params:grant-type:ciba (confidential clients only; see Backchannel sign-in). An empty list means no grant at all
backchannel_token_delivery_modepoll with the CIBA grantpoll or ping; only with the CIBA grant
backchannel_client_notification_endpointnullWhere a ping client is told; required in ping mode
allowed_scopesby typeScopes the client may ask for; each must exist in the tenant
allowed_audiences[]Resource servers (by identifier) the client may get access tokens for; each must exist. Empty means any resource server except the built-in ones. urn:ridm:admin makes admin API tokens possible (see Administrator access)

A scope or audience that does not exist is refused at registration, rather than surfacing later as invalid_scope or invalid_target at the token endpoint.

A token request without a resource parameter gets the client’s allowed_audiences as its audience; a client with an empty list then gets a token whose audience is the client itself. A request that names resource may only name audiences the client is allowed. The built-in resource servers (the admin and account APIs) are never implied by an empty list: a client must list them explicitly. A scope bound to a resource server adds that server’s audience when it is requested, and is refused with invalid_scope if the client may not target it. See Resource servers, scopes and permissions.

FieldDefaultNotes
require_consenttrueShow the consent screen. Set false for first-party applications
require_pkceby typeOnly meaningful for confidential clients; public clients always need PKCE

Token lifetimes and format

FieldDefaultNotes
access_token_ttl_secsnullOverrides the tenant’s session.access_token_ttl_secs (default 300)
refresh_token_ttl_secsnullOverrides session.refresh_token_ttl_secs (default 30 days)
id_token_ttl_secsnullOverrides session.id_token_ttl_secs (default 300)
access_token_formatjwtjwt, or opaque: see Opaque access tokens. The built-in console clients refuse opaque
subject_typepublicpairwise gives each client (or sector) its own sub for a user
sector_identifier_urinullGroups clients that should share pairwise subjects
id_token_scope_claimsfalseAlso put the claims released by the granted scopes (each scope’s claims list, profile, email, address and phone included) in the ID token; by default they are only at /userinfo (OIDC Core §5.4)
id_token_encryptionnull{"alg": "RSA-OAEP-256" | "RSA-OAEP", "enc": "A256GCM" | "A128GCM"}; needs the client’s jwks or jwks_uri

A resource server may cap the lifetime of access tokens issued for it, choose the algorithm they are signed with, and refuse offline_access; see Resource servers, scopes and permissions. What the tokens contain is in Token claims.

Opaque access tokens

With access_token_format: "opaque" the client receives access tokens of the form at_<random> instead of JWTs. The claims a JWT would have carried are kept in Valkey until the token expires; the token itself carries nothing.

  • A resource server learns what the token stands for from POST /t/{slug}/introspect. It cannot validate the token locally, so ridm-auth, which validates JWTs only, does not accept opaque tokens.
  • rIDM’s own endpoints accept them: /userinfo, /introspect, /revoke, token exchange (as urn:ietf:params:oauth:token-type:access_token), and the account and admin APIs.
  • Losing Valkey’s data ends every opaque token early; the client refreshes or signs in again. See Postgres and Valkey.
  • Refresh tokens and ID tokens are unchanged.

Choose opaque tokens when the audience must not be able to read the token’s contents, or when you want every use checked centrally (revocation takes effect at the next introspection), and accept the introspection round trip that costs.

PAR, JAR, DPoP, mTLS and FAPI 2.0

FieldDefaultNotes
dpop_bound_access_tokensfalse (true under FAPI 2.0 unless certificate-bound)Every token request must carry a DPoP proof, and the tokens are bound to its key
tls_client_certificate_bound_access_tokensfalseEvery token request must come over mutual TLS, and the access tokens are bound to the client certificate (RFC 8705 §3). See Mutual TLS
require_pushed_authorization_requestsfalse/authorize refuses the client’s requests unless they came through PAR (RFC 9126 §6)
security_profilenonefapi2 holds the client to the FAPI 2.0 Security Profile: see Backchannel sign-in and FAPI 2.0

Pushed authorization requests (POST /t/{slug}/par) and JWT-secured authorization requests (a request parameter signed with one of the client’s jwks keys) are available to every client; require_pushed_authorization_requests (or the FAPI 2.0 profile) makes PAR the only way in. Any client may also present DPoP proofs voluntarily; dpop_bound_access_tokens makes them compulsory. See Tokens.

Changing a client

PATCH /admin/tenants/{slug}/clients/{client} is a merge patch over the client’s metadata: send only what changes. null clears an optional field or resets a defaulted one to its type’s default. client_id cannot change. Switching to a secret-based auth method mints a secret and returns it once in client_secret.

DELETE …/clients/{client} removes the client with its client-scoped roles, refresh tokens and consents.

The built-in ridm-admin-console and ridm-account-console clients cannot be deleted and are left out of tenant exports. Their redirect URIs follow UI_URL and are restored at start-up; other fields, such as token lifetimes and CORS origins, may be changed.

Secrets

A confidential client using client_secret_basic or client_secret_post has a secret (cs_…, 256 random bits) generated at creation. It is returned once, in the 201 response’s client_secret, and stored only as a SHA-256 hash. GET shows each secret’s id, creation time and expiry, never the value.

Rotate with POST …/clients/{client}/secrets:

{ "grace_secs": 86400 }

The response carries the new secret, once. The previous secret keeps working for grace_secs (default 86 400, one day; 0 retires it at once; at most 30 days), so deployments can move to the new one without an outage. Any older secret is dropped. In the console: client detail → Secrets → Rotate, then revoke the retiring secret once everything has moved.

Revoke one secret early with DELETE …/clients/{client}/secrets/{secret_id}. The last secret of a confidential client cannot be revoked; rotate instead.

private_key_jwt

With token_endpoint_auth_method: "private_key_jwt", the client proves itself with a JWT signed by its own private key (RFC 7523) instead of a shared secret. Register the public half as jwks or jwks_uri:

{
  "name": "Acme Billing",
  "client_type": "machine",
  "token_endpoint_auth_method": "private_key_jwt",
  "jwks_uri": "https://billing.acme.example/.well-known/jwks.json",
  "allowed_audiences": ["https://api.acme.example"]
}

At the token endpoint, send client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and client_assertion, a JWT that:

  • is signed with an asymmetric algorithm, by a key in the registered set;
  • has iss and sub equal to the client_id;
  • has aud equal to the token endpoint URL or the tenant’s issuer;
  • carries exp and a jti, lives no more than 600 seconds, and is not replayed (each jti is accepted once).

A jwks_uri is fetched over HTTPS and cached for an hour. An unknown kid triggers one re-fetch, at most once a minute per client, so a rotated client key is picked up without an administrator.

Service accounts

A client with the client_credentials grant can have a service account: a user named svc-<client_id> that its tokens are issued for, so it can hold roles and groups like anyone else. PUT …/clients/{client}/service-account creates it (or returns the existing one), DELETE removes it. See Administrator access for giving a service account admin permissions, and Machine-to-machine access.

Dynamic client registration

Applications can register themselves at POST /t/{slug}/register (RFC 7591) and manage their registration at GET, PUT and DELETE /t/{slug}/register/{client_id} (RFC 7592) with the registration access token they received. The tenant’s settings.dcr decides whether that is allowed. In the console: Settings → Keys, discovery & audit.

KeyDefaultMeaning
modedisableddisabled: registration answers 403. open: anyone may register, within the tenant’s rate limits. initial_access_token: registration needs a bearer initial access token
allowed_grants[]Grant types a registered client may ask for; empty allows every supported grant
require_pkcetrueWhether registered confidential clients must use PKCE (public clients always must). The OpenID Connect basic profile registers confidential clients without PKCE, so conformance testing needs it off
{ "settings": { "dcr": { "mode": "open", "allowed_grants": ["authorization_code", "refresh_token"] } } }

How registered metadata maps onto a client:

  • token_endpoint_auth_method defaults to client_secret_basic; none, client_secret_post and private_key_jwt are accepted.
  • grant_types defaults to ["authorization_code"]; only the code response type is supported.
  • The type is native for application_type: "native", else derived from the grants and auth method (device, machine, spa or web).
  • scope sets the allowed scopes; audiences, CORS origins and token lifetimes cannot be registered.
  • Every registered client gets require_consent: true.
  • require_pushed_authorization_requests is stored and enforced.
  • backchannel_token_delivery_mode (poll or ping) and backchannel_client_notification_endpoint register a CIBA client; signed backchannel requests and user codes are refused. security_profile cannot be registered: an administrator sets it.

The response carries the client_id, any client_secret (with client_secret_expires_at: 0), and a registration_access_token for the management endpoints. An administrator can issue a new registration access token for any client with POST /admin/tenants/{slug}/clients/{client}/registration-token (or on the console’s client detail page), which replaces the previous one; it is shown once.

Registrations are recorded in the audit log with the system as actor, and are ordinary clients afterwards: administrators see, edit and delete them like any other.

Initial access tokens

With dcr.mode set to initial_access_token, POST /t/{slug}/register demands a bearer token issued by an administrator (RFC 7591 §1.2). A missing token is answered 401 with invalid_token; an unknown, revoked, expired or used-up one the same way. Each successful registration spends one use; the check and the count are one statement, so two registrations racing for a token’s last use cannot both succeed.

POST /t/acme/register
Authorization: Bearer iat_Vt4cN9…
Content-Type: application/json

{ "client_name": "Build agent", "redirect_uris": ["https://ci.acme.example/cb"] }

Tokens are managed under the client permissions, since a token lets its holder create clients:

RoutePermissionDoes
GET /admin/tenants/{slug}/dcr/initial-access-tokensridm:clients:readList the tenant’s tokens, newest first, never their values
POST /admin/tenants/{slug}/dcr/initial-access-tokensridm:clients:writeIssue a token; answers 201 with the record and token, shown this once
DELETE /admin/tenants/{slug}/dcr/initial-access-tokens/{id}ridm:clients:writeRevoke it (204)

The request body is optional in every field:

FieldDefaultRules
descriptionnullWhat the token is for; at most 200 characters
expires_in_secsno expiry1 second to ten years
max_usesno limitRegistrations it allows in all; at least 1
curl -X POST https://id.example.com/admin/tenants/acme/dcr/initial-access-tokens \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"description": "CI agents", "expires_in_secs": 86400, "max_uses": 5}'

A listed token shows id, description, max_uses, uses, expires_at, last_used_at, revoked_at and created_at. Tokens are stored in Postgres as SHA-256 hashes; the value (iat_…) exists only in the issuing response. Issuing and revoking raise dcr_token.created and dcr_token.revoked (see Webhooks and the audit log).

The same is available as ridm client iat create | list | revoke (see The ridm command line) and in the console under Settings → Keys, discovery & audit, where an Initial access tokens block appears while the mode is initial_access_token: issue a token with a description, lifetime and use limit, see each token’s state (active, expired, used up, revoked), and revoke it.