Skip to content

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.

KeyRequiredRule
clientIdyes2 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
namenodisplay name; defaults to the clientId
enabledno (true)false disables the app without deleting anything
profileyesconfidential (has a secret, runs on a server) or public (native, mobile or browser-only app: no secret, PKCE only)
redirectUrisyes, at least 1exact URIs: https://host/path (a path, even if only /), or http://localhost[:port]/… and http://127.0.0.1[:port]/…. No *, no #
webOriginsyes (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 []
postLogoutRedirectUrisnoa list, same rule as the redirects: where the IdP may send the person after logout
pkceno (S256)only S256; no exception
secretSsmPathif confidentialthe path of the secret, under /idp/prod/ (conventionally /idp/prod/clients/<clientId>); forbidden on a public client
notesnofree text for the reviewers: what the app is and why it needs these addresses
  • Unknown keys are refused. No scopes, roles or claims key: the claims contract is one, for every app.
  • One client per environment. A descriptor never mixes https:// addresses with http://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.

All three pass the validator; names and domains are examples.

clients/my-app.json
{
"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."
}
  1. A pull request adds clients/<clientId>.json to urupema/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.

  2. For a confidential client, the IdP administration creates the secret as a SecureString parameter at the descriptor’s secretSsmPath, generated with openssl rand -hex 32. The value reaches the app’s team over a secure channel, never by email or chat in clear text.

  3. After the merge, the administration applies the clients on the host:

    sudo bash /opt/idp/bootstrap.sh --clients

    Takes seconds, restarts nothing, does not touch the realm or the claims contract.

  4. The app uses its clientId and 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.

The IdP guarantees

Idempotent, per client. Applying again changes nothing. With no descriptors, the IdP runs with zero apps.

The IdP guarantees

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.

The IdP guarantees

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 IdP guarantees

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.

The IdP guarantees

Isolated rotation. A new secret affects only its client.

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.