libredb / libredb/libredb-studio
docs/OIDC.md: Keycloak realm roles are not in the ID token by default, so a deployment that follows the doc has no admin
- Dominant language
- TypeScript
- Stars
- 726
- Forks
- 119
- Avg merge
- 7h 47m
- Merged PRs (30d)
- 265
Description
`docs/OIDC.md` tells Keycloak operators to set `OIDC_ROLE_CLAIM=realm_access.roles` and states that realm roles are in the ID token by default.
On Keycloak 26.4 they are not, and Studio reads ID token claims only, so every deployment that follows the doc ends up with no administrator: each user maps to `role: "user"`, and `/admin` is unreachable for everyone.
## What the doc says today
`docs/OIDC.md:148-154`:
```
Keycloak includes realm roles in the ID token by default:
OIDC_ROLE_CLAIM=realm_access.roles
```
## What actually happens
`exchangeCode()` in `src/lib/oidc.ts:129-150` calls `client.authorizationCodeGrant(...)` and then `tokens.claims()`.
In `openid-client` v6 that is the **ID token** and nothing else: `node_modules/openid-client/build/index.d.ts:1176` declares `claims(): oauth.IDToken | undefined`.
`mapOIDCRole()` (`src/lib/oidc.ts:158-183`) walks the dot path over those claims, and returns `"user"` when the path is absent.
Keycloak's `realm roles` protocol mapper, in the built-in `roles` client scope, ships configured for the access token and not the ID token.
Read back from a fresh Keycloak 26.4 realm through the admin REST API, its config is:
```json
{"introspection.token.claim": "true", "access.token.claim": "true", "claim.name": "realm_access.roles"}
```
There is no `id.token.claim` key at all, which Keycloak treats as off.
The mapper class is documented as able to serve both tokens (`UserRealmRoleMappingMapper`, "Allows mapping of user realm role mappings to an ID and Access Token claim"), so this is a per-mapper switch and not a capability gap: https://www.keycloak.org/docs-api/latest/javadocs/org/keycloak/protocol/oidc/mappers/UserRealmRoleMappingMapper.html
## Reproduction, with a control arm
Environment: Keycloak 26.4 (`quay.io/keycloak/keycloak:26.4`, `start-dev` with TLS), realm `libredb`, confidential client `studio`, three users. `alice` holds realm role `admin`; `bob` and `carol` hold realm role `analyst`.
Studio on `main` at 2703ddc6, `NEXT_PUBLIC_AUTH_PROVIDER=oidc`, `OIDC_ROLE_CLAIM=realm_access.roles`, `OIDC_ADMIN_ROLES=admin`.
**Arm 1, Keycloak defaults untouched.** Full authorization-code login for each user:
| User | Realm role | `/api/auth/me` | Redirect |
|---|---|---|---|
| alice | admin | `{"role":"user","username":"alice@probe.local"}` | `/` |
| bob | analyst | `{"role":"user","username":"bob@probe.local"}` | `/` |
| carol | analyst | `{"role":"user","username":"carol@probe.local"}` | `/` |
The decoded ID token for alice carries `sub`, `email` and `preferred_username`, and no `realm_access` key.
**Arm 2, one variable changed.** Set `id.token.claim: "true"` on that one mapper through the admin REST API. Nothing else changed: same realm, same users, same client, same Studio process, same env.
| User | Realm role | `/api/auth/me` | Redirect |
|---|---|---|---|
| alice | admin | `{"role":"admin","username":"alice@probe.local"}` | `/admin` |
| bob | analyst | `{"role":"user","username":"bob@probe.local"}` | `/` |
| carol | analyst | `{"role":"user","username":"carol@probe.local"}` | `/` |
alice's ID token in arm 2 carries `"realm_access": {"roles": ["offline_access", "admin", "uma_authorization", "default-roles-libredb"]}`.
So the doc's premise is the only thing standing between a working Keycloak setup and one with no admin, and the failure is silent: login succeeds, the app works, the operator simply never sees `/admin`.
## What to change
This is a documentation fix. Do not change `mapOIDCRole()` or the claim path handling: reading the ID token is correct, and every other provider section in the doc is fine.
1. In `docs/OIDC.md`, replace the "includes realm roles in the ID token by default" sentence in the Keycloak section (around `:148`) with the required setup step. Realm roles reach the ID token only when the `realm roles` mapper in the `roles` client scope has **Add to ID token** switched on. Give the click path: Client scopes, `roles`, Mappers, `realm roles`, toggle **Add to ID token**, Save.
2. State the version the instruction was verified against, the way the rest of the repo's docs pin measured facts. Verified on Keycloak 26.4.
3. Add the symptom to the Troubleshooting section: "Role is always user even for admins" already exists around `docs/OIDC.md:335`. Add this as the first thing to check for Keycloak, above the `OIDC_ADMIN_ROLES` spelling check, because it is the more common cause and it cannot be diagnosed from the app.
4. Same treatment for client roles if the doc recommends `resource_access.${client_id}.roles` anywhere: that mapper ships with the same default. Check before writing; do not assert it without looking.
5. Consider a one-line note, in the Role Mapping section rather than per provider, that Studio reads ID token claims and never the access token. That is the fact every provider section depends on, and it is currently implicit.
## Done when
- `docs/OIDC.md` no longer claims Keycloak puts realm roles in the ID token by default.
- The Keycloak section carries the exact toggle to flip, with the click path and the verified version.
- Troubleshooting names this as the first Keycloak check for a missing admin role.
- `bun run readme:check` and the docs tests pass. No source file changes, so no coverage impact.
## Not in scope
Adding an access-token fallback to `mapOIDCRole()`. The ID token is the right place for identity and role claims here, the access token is for the issuer's resource servers, and a fallback would make the role depend on which token happened to carry the claim. If someone wants to argue for it, that is a separate issue with its own measurement.
Contributor guide
Research direction
Update docs/OIDC.md in the Keycloak section, Role Mapping section, and Troubleshooting section around the cited lines. Verify the Keycloak 26.4 mapper behavior, including client roles if documented, then run bun run readme:check and the docs tests. Done means the ID-token toggle, click path, version, and missing-admin symptom are documented without source changes.
Written by the indexing model from the issue text.
Assessment
- Domain
- authentication, documentation
- Issue type
- Documentation
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 85/100