Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.7 KiB
OIDC 🪪
This page describes how application roles consume the platform's OIDC provider, Keycloak.
The variable tree lives in 11_oidc.yml under defaults_oidc.
For the cross-cutting IAM principles, including the paired identifier rule shared with LDAP, see common.md.
For LDAP-side details see ldap.md.
Variable Tree 🌳
The OIDC tree is a lazy group_var: it resolves in the consuming role's
context, so the issuer family-aligns to that consumer (clearnet page →
clearnet issuer, onion page → onion issuer). Inventory overrides therefore
live under OIDC_OVERRIDE: — an inventory-level OIDC: block would shadow
the lazy resolution and is rejected by a constructor assert.
The keys consumers care about:
| Key | Meaning |
|---|---|
OIDC.URL |
Base URL of the Keycloak instance |
OIDC.CLIENT.ID |
Client identifier, defaults to SOFTWARE_DOMAIN |
OIDC.CLIENT.SECRET |
Client secret, MUST come from the inventory (OIDC_OVERRIDE.CLIENT.SECRET) |
OIDC.CLIENT.REALM |
Realm the client is registered in |
OIDC.CLIENT.ISSUER_URL |
Issuer URL, equal to discovery issuer |
OIDC.CLIENT.DISCOVERY_DOCUMENT |
.well-known/openid-configuration URL |
OIDC.CLIENT.AUTHORIZE_URL |
Authorization endpoint |
OIDC.CLIENT.TOKEN_URL |
Token endpoint |
OIDC.CLIENT.USER_INFO_URL |
Userinfo endpoint |
OIDC.CLIENT.LOGOUT_URL |
RP-initiated logout endpoint |
OIDC.CLIENT.CERTS |
JWKS endpoint, equal to discovery jwks_uri |
OIDC.CLIENT.ACCOUNT.URL |
User self-service console root |
OIDC.CLIENT.ACCOUNT.PROFILE_URL |
Personal-info section deep link |
OIDC.CLIENT.ACCOUNT.SECURITY_URL |
Login and security section deep link |
OIDC.CLIENT.CHANGE_CREDENTIALS |
Direct link to the credentials editor |
OIDC.CLIENT.RESET_CREDENTIALS |
Public password-reset entry point |
OIDC.BUTTON_TEXT |
Default label for SSO login buttons |
When a role needs an OIDC endpoint, it MUST template the corresponding OIDC.CLIENT.* value rather than build the URL from OIDC.URL itself.
The URL builders in 11_oidc.yml already handle realm and protocol concerns.
Claim Attributes 🏷️
OIDC.ATTRIBUTES lists the claim names that downstream applications read out of the userinfo response and ID token:
| Key | Default value | Meaning |
|---|---|---|
OIDC.ATTRIBUTES.USERNAME |
preferred_username |
Stable user identifier |
OIDC.ATTRIBUTES.GIVEN_NAME |
givenName |
First name |
OIDC.ATTRIBUTES.FAMILY_NAME |
surname |
Last name |
OIDC.ATTRIBUTES.EMAIL |
email |
Email address |
Rules:
- Application roles MUST identify users via
OIDC.ATTRIBUTES.USERNAME. They MUST NOT introduce a private claim name (for exampleuser_id,uid, oroauth_uid) whenpreferred_usernamealready satisfies the requirement. - Roles MUST NOT hardcode the literal value
preferred_username. Always template{{ OIDC.ATTRIBUTES.USERNAME }}. - The standard OIDC
subclaim is permitted as an alternative when the application explicitly requires a UUID-shaped identifier and stores its own mapping table. In that case the role MUST document the deviation in its README. - The OIDC username claim is paired with the LDAP login attribute by design. See ldap.md for the LDAP-side rule.
Keycloak Client Templates 📦
Keycloak client definitions are imported from clients. The default template in default.json.j2 is shared by every application client.
Rules:
- The shared
default.json.j2MUST stay generic. App-specific protocol mappers, claims, or scopes MUST NOT be added there because the change affects every other client. - An app that needs an extra mapper MUST add it through a per-client scope. See nextcloud.json.j2 for the precedent: a dedicated scope holds the Nextcloud-specific UID and quota mappers and is attached only to the Nextcloud client.
- New scope files MUST be referenced from the consuming app via its
KEYCLOAK_RBAC_GROUP_CLAIMstyle configuration or via theoptionalClientScopeslist in the client template.
Consumer Pattern 🧩
A typical OIDC-consuming role wires the values into its environment template, for example env.j2:
OIDC_ISSUER={{ OIDC.CLIENT.ISSUER_URL }}
OIDC_DISCOVERY=true
OIDC_SCOPE=openid,profile,email
OIDC_UID_FIELD={{ OIDC.ATTRIBUTES.USERNAME }}
OIDC_CLIENT_ID={{ OIDC.CLIENT.ID }}
OIDC_CLIENT_SECRET={{ OIDC.CLIENT.SECRET }}
Every application role SHOULD follow the same shape: discovery on, the OIDC.CLIENT.* endpoints templated, the user identifier read from OIDC.ATTRIBUTES.USERNAME, and the scopes limited to what the app actually consumes.
Related Files 📚
| File | Purpose |
|---|---|
| 11_oidc.yml | OIDC SPOT, defines OIDC.* |
| web-app-keycloak | OIDC provider role |
| default.json.j2 | Shared client template, MUST stay app-agnostic |
| scopes | Per-app client scopes for extra mappers |
| realm.json.j2 | Realm definition, including the preferred_username mapper |
| ldap.md | LDAP-side counterpart, paired user identifier |
| common.md | Cross-cutting IAM principles and integration checklist |