|
| 1 | +# Keycloak Realm Setup |
| 2 | + |
| 3 | +This document describes the roles, groups, and claims you need to configure in your Keycloak realm to run this application. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Realm |
| 8 | + |
| 9 | +Name your realm `skill-matrix-llm` (must match `AUTH_KEYCLOAK_ISSUER` and the backend `Realm` config value). |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Clients |
| 14 | + |
| 15 | +You need three clients: |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +### `skill-matrix-llm-frontend` |
| 20 | + |
| 21 | +Confidential client used by the Next.js backend (NextAuth). |
| 22 | + |
| 23 | +| Setting | Value | |
| 24 | +|---------|-------| |
| 25 | +| Client authentication | **On** | |
| 26 | +| Authentication flow | Standard flow only | |
| 27 | +| Valid redirect URIs | `<frontend-url>/*` e.g. `http://localhost:3000/*` | |
| 28 | +| Web origins | `<frontend-url>` e.g. `http://localhost:3000` | |
| 29 | + |
| 30 | +Add an **Audience** mapper so that access tokens include `skill-matrix-llm-api` in the `aud` claim. The backend validates this on every request — without it you get an audience validation error. |
| 31 | + |
| 32 | +- Clients → `skill-matrix-llm-frontend` → Client scopes → `skill-matrix-llm-frontend-dedicated` → Add mapper → **Audience** |
| 33 | +- Included client audience: `skill-matrix-llm-api` — Add to access token: **On** |
| 34 | + |
| 35 | +Copy the client secret from the **Credentials** tab into `AUTH_KEYCLOAK_SECRET` (`.env.local` locally, Key Vault in Azure). |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +### `skill-matrix-llm-api` |
| 40 | + |
| 41 | +Confidential client representing the backend API — used as the JWT audience and for Admin API machine-to-machine calls. |
| 42 | + |
| 43 | +| Setting | Value | |
| 44 | +|---------|-------| |
| 45 | +| Client authentication | **On** | |
| 46 | +| Authentication flow | Standard flow only | |
| 47 | + |
| 48 | +Copy the client secret into `Keycloak__Secret` in App Service config (or Key Vault reference). |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +### `skill-matrix-llm-public` |
| 53 | + |
| 54 | +Public client for developer API docs via Scalar — **local development only**, do not create in production. |
| 55 | + |
| 56 | +| Setting | Value | |
| 57 | +|---------|-------| |
| 58 | +| Client authentication | **Off** (public client) | |
| 59 | +| Authentication flow | Standard flow only | |
| 60 | +| Valid redirect URIs | `http://localhost:5000/*` | |
| 61 | +| Web origins | `http://localhost:5000` | |
| 62 | + |
| 63 | +No secret required. PKCE (SHA-256) is enforced by the Scalar configuration. |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +After creating the clients, update `AUTH_KEYCLOAK_ISSUER` in `frontend/.env.example` and App Service config, and update the Keycloak URLs in the `infra/main.*.bicepparam` files. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## Roles |
| 72 | + |
| 73 | +The seeder creates these as **realm roles**, which is fine — this is the only app on this realm so there is no namespace collision concern. The claims transformer reads from both `realm_access.roles` and `resource_access.{clientId}.roles` in the JWT, so either approach works. |
| 74 | + |
| 75 | +### Roles to create |
| 76 | + |
| 77 | +| Role name | What it grants | |
| 78 | +|-----------|----------------| |
| 79 | +| `CreateUsers` | Create new users in Keycloak via the API | |
| 80 | +| `UpdateUsers` | Update user details and assign roles to other users | |
| 81 | +| `DeleteUsers` | Delete users | |
| 82 | +| `ViewUsers` | Read the user list and individual user profiles | |
| 83 | +| `ManageSkills` | Create, rename, and delete skills in the catalogue | |
| 84 | +| `ManageProjects` | Create and manage projects; trigger LLM recommendations | |
| 85 | + |
| 86 | +> **`SendHealthCheckEmails`** and **`ViewContent`** are defined in code but not enforced as distinct policies — the health check endpoint actually requires `ViewUsers`, and `ViewContent` is currently unused. You do not need to create these in Keycloak unless you plan to use them in future. |
| 87 | +
|
| 88 | +--- |
| 89 | + |
| 90 | +## Groups |
| 91 | + |
| 92 | +Create the following groups in the realm: |
| 93 | + |
| 94 | +| Group name | Roles assigned | Intended members | |
| 95 | +|------------|---------------|-----------------| |
| 96 | +| `Admin` | All roles | Administrators | |
| 97 | +| `Users` | `ViewContent`, `ManageProjects`, `ManageSkills` | Regular authenticated users | |
| 98 | +| `Guest` | `ViewContent` | Read-only viewers | |
| 99 | + |
| 100 | +Assign the realm roles to each group using **Group Role Mappings** rather than assigning roles directly to individual users. This makes access management much easier. |
| 101 | + |
| 102 | +### Default group for self-registration |
| 103 | + |
| 104 | +The app uses Keycloak's own registration UI (via `prompt=create`), so new users who sign up themselves are not created through the backend — no code runs to assign them a group. Without a default group they would land in the app authenticated but with no roles, and every policy-gated endpoint would return 403. |
| 105 | + |
| 106 | +Set the default group to `Users` in **Realm Settings → General → Default Groups**. Every self-registered user is then automatically placed in `Users` after verifying their email, which is the right level of access for anyone signing up. |
| 107 | + |
| 108 | +> Email verification is already enabled, which is sufficient access control for this app — invite-only was considered but rejected because anyone who would be invited would get standard `Users` access anyway, so the extra admin overhead buys nothing. |
| 109 | +
|
| 110 | +> **Ownership enforcement — current state:** |
| 111 | +> |
| 112 | +> - **User skills:** enforced. `UsersController` runs an `IsSelfOrAdmin` check before add/update/remove skill endpoints — the request is allowed only if the caller holds the `UpdateUsers` Keycloak role (admin) or their `sub` claim resolves to the same user as the path parameter. |
| 113 | +> - **Projects:** not yet enforced. `Update`, `TransitionStatus`, `Close`, and all team management endpoints (`CreateTeam`, `AddTeamMember`, `RemoveTeamMember`, `ConfirmTeam`, `RejectTeam`) execute without any caller-vs-owner check. Any user with the `ManageProjects` Keycloak role can currently mutate any project. |
| 114 | +> |
| 115 | +> Project ownership enforcement is planned but not yet implemented. |
| 116 | +
|
| 117 | +--- |
| 118 | + |
| 119 | +## Standard claims (no configuration needed) |
| 120 | + |
| 121 | +The following claims are read from the JWT by the backend but are standard OIDC claims that Keycloak includes automatically — you do not need to add custom mappers for them: |
| 122 | + |
| 123 | +| Claim | Source | Used for | |
| 124 | +|-------|--------|---------| |
| 125 | +| `sub` | Always present | Keycloak user ID, stored as the primary key in the app DB | |
| 126 | +| `name` | `profile` scope | Display name shown in the UI | |
| 127 | +| `preferred_username` | `profile` scope | Fallback display name if `name` is absent | |
| 128 | +| `email` | `email` scope | Shown on profile; used when sending health-check emails | |
| 129 | + |
| 130 | +Ensure the `profile` and `email` scopes are added to the `skill-matrix-llm-frontend` client (they are included by default in new Keycloak clients). |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Token mapper for client roles |
| 135 | + |
| 136 | +Keycloak does **not** include `resource_access` in access tokens by default for all client configurations. Verify this is working: |
| 137 | + |
| 138 | +1. In your realm, go to **Clients → skill-matrix-llm-api → Client scopes**. |
| 139 | +2. Open the dedicated scope (`skill-matrix-llm-api-dedicated`). |
| 140 | +3. Confirm a **"roles"** mapper of type **"User Client Role"** exists and has **"Add to access token"** enabled. |
| 141 | + |
| 142 | +If it is missing, add it manually: |
| 143 | +- Mapper type: `User Client Role` |
| 144 | +- Client ID: `skill-matrix-llm-api` |
| 145 | +- Token claim name: `resource_access.${client_id}.roles` |
| 146 | +- Multivalued: on |
| 147 | +- Add to access token: on |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## Summary checklist |
| 152 | + |
| 153 | +- [ ] Realm named `skill-matrix-llm` |
| 154 | +- [ ] Client `skill-matrix-llm-frontend` created with redirect URIs and audience mapper |
| 155 | +- [ ] Client `skill-matrix-llm-api` created (confidential, client credentials enabled) |
| 156 | +- [ ] Client `skill-matrix-llm-public` created (local dev only) |
| 157 | +- [ ] Six realm roles created: `CreateUsers`, `UpdateUsers`, `DeleteUsers`, `ViewUsers`, `ManageSkills`, `ManageProjects` |
| 158 | +- [ ] Groups `Admin`, `Users`, and `Guest` created with appropriate role mappings |
| 159 | +- [ ] Default group set to `Users` (Realm Settings → General → Default Groups) |
| 160 | +- [ ] `profile` and `email` scopes on the frontend client |
| 161 | +- [ ] `resource_access` included in access tokens (verify mapper above) |
0 commit comments