Contract
Registering an app
An app exists for the IdP when its descriptor, reviewed in a pull request, lands in clients/ of urupema/idp. Format, validation rules, examples and steps.
realm.json is the realm’s posture and names no app. Each app is one descriptor, clients/<clientId>.json, in the urupema/idp repository. The same code validates descriptors in CI (node reconcile.ts --check) and applies them on the host.
The descriptor
Section titled “The descriptor”| Key | Required | Rule |
|---|---|---|
clientId | yes | 2 to 63 characters of a-z, 0-9 and -, starting with a letter or digit, equal to the file name. Not one of Keycloak’s own clients: account, account-console, admin-cli, broker, realm-management, security-admin-console |
name | no | display name; defaults to the clientId |
enabled | no (true) | false disables the app without deleting anything |
profile | yes | confidential (has a secret, runs on a server) or public (native, mobile or browser-only app: no secret, PKCE only) |
redirectUris | yes, at least 1 | exact URIs: https://host/path (a path, even if only /), or http://localhost[:port]/… and http://127.0.0.1[:port]/…. No *, no # |
webOrigins | yes (may be []) | exact origins, https://host[:port], no path or trailing slash; http:// only on localhost and 127.0.0.1. Needed only when a browser talks to the IdP directly; a BFF uses [] |
postLogoutRedirectUris | no | a list, same rule as the redirects: where the IdP may send the person after logout |
pkce | no (S256) | only S256; no exception |
secretSsmPath | if confidential | the path of the secret, under /idp/prod/ (conventionally /idp/prod/clients/<clientId>); forbidden on a public client |
notes | no | free text for the reviewers: what the app is and why it needs these addresses |
- Unknown keys are refused. No
scopes,rolesorclaimskey: the claims contract is one, for every app. - One client per environment. A descriptor never mixes
https://addresses withhttp://localhost(redirects, post-logout and origins alike). Local development has its own descriptor, e.g.my-app-dev, and its own secret.
The secret never goes in the descriptor; the descriptor only points to where it is kept. A secret has at least 32 characters.
Examples
Section titled “Examples”All three pass the validator; names and domains are examples.
{ "clientId": "my-app", "name": "My App", "profile": "confidential", "redirectUris": ["https://my-app.institutourupema.com.br/callback"], "webOrigins": [], "postLogoutRedirectUris": ["https://my-app.institutourupema.com.br/"], "secretSsmPath": "/idp/prod/clients/my-app", "notes": "Server-side app (BFF): the server holds the tokens, the browser only a session cookie, so no web origins."}{ "clientId": "my-app-dev", "name": "My App (local development)", "profile": "confidential", "redirectUris": ["http://localhost:3000/callback"], "webOrigins": [], "postLogoutRedirectUris": ["http://localhost:3000/"], "secretSsmPath": "/idp/prod/clients/my-app-dev", "notes": "One client per environment: localhost never shares a descriptor with production."}{ "clientId": "my-mobile", "name": "My Mobile App", "profile": "public", "redirectUris": ["https://my-app.institutourupema.com.br/mobile/callback"], "webOrigins": [], "postLogoutRedirectUris": ["https://my-app.institutourupema.com.br/mobile/"], "notes": "Public client: no secret, PKCE only. Use it only when there is no server to hold tokens."}-
A pull request adds
clients/<clientId>.jsontourupema/idp. CI runs the validator; the reviewer decides whether the app gets in. Without access to the repository, send the descriptor to the IdP administration at suporte@institutourupema.com.br, which opens the pull request. -
For a confidential client, the IdP administration creates the secret as a
SecureStringparameter at the descriptor’ssecretSsmPath, generated withopenssl rand -hex 32. The value reaches the app’s team over a secure channel, never by email or chat in clear text. -
After the merge, the administration applies the clients on the host:
sudo bash /opt/idp/bootstrap.sh --clientsTakes seconds, restarts nothing, does not touch the realm or the claims contract.
-
The app uses its
clientIdand secret with the integration guide.
Changing a descriptor (a new redirect, disabling the app) follows the same path. Rotating a secret: a new value in SSM, then --clients; only the new value is valid afterwards.
Guarantees of apply
Section titled “Guarantees of apply”Idempotent, per client. Applying again changes nothing. With no descriptors, the IdP runs with zero apps.
A bad descriptor does not take down the others. An invalid descriptor or a missing secret skips only that client, with a named error and without printing the secret; the rest are applied. A client whose contract fails halfway is disabled, not left half-configured; if disabling also fails, the deploy names it for the administration to disable by hand.
Nothing is deleted by omission. The IdP never deletes a client or user it does not manage; removing the file from clients/ does not delete the client. Disabling is declarative ("enabled": false); deleting is an explicit administration act.
The contract is reimposed. Every managed client gets only the contract scopes and its own aud. Scopes, protocol mappers and grant switches added through the console are undone at the next apply.
Isolated rotation. A new secret affects only its client.
Review
Section titled “Review”A registered client receives the identity of any person with an open session at the IdP, without a new password prompt: a request with prompt=none is enough. One extra redirect or a wrong domain exposes every Institute account. Registration happens only by reviewed pull request, behind the validator: never in the console, never through a credential the app holds to write to the IdP.