--- title: "Identity Provider Setup" audience: [admin, developer] summary: "Configure an OIDC identity provider (Keycloak-focused) so the console can authenticate users and assign the operator role." --- # Identity Provider Setup This guide covers configuring your OIDC identity provider to work with the member console. The examples use Keycloak but the concepts apply to any OIDC-compliant provider. ## Prerequisites - An OIDC identity provider with a client configured for Authorization Code + PKCE flow - The client ID and secret configured in `mc-config.yaml` (or equivalent flags) - Refresh tokens issued to the client (Keycloak's default). Two things use them: every five minutes a signed-in session re-derives its identity and roles from a fresh ID token, so a role removed, an account disabled, or a session ended at the provider takes effect here within that bound; and sign-out mints a fresh ID token for a valid `id_token_hint`, so the provider ends its session without asking. Without a refresh token a session keeps its sign-in roles for its lifetime and the provider prompts at sign-out. The roles claim must be present in the ID token a refresh returns, not only the one sign-in returns (Keycloak's mappers do both). - `end_session_endpoint` published in the provider's discovery document (OpenID Connect RP-Initiated Logout 1.0). The console refuses to start without it. - `revocation_endpoint` published in the discovery document (RFC 7009), so the console can revoke the refresh token at sign-out. Optional; without it the console warns once at startup and skips revocation. ## Standard OIDC Claims The member console reads the following standard claims from the **ID token**: | Claim | Used for | |-------|----------| | `sub` | Unique user identifier (OIDC subject) | | `email` | User's primary email | | `email_verified` | Whether the email is verified | | `name` | Display name | | `preferred_username` | Username (used for org slug derivation) | On first login, the member console auto-provisions a person, organization, and workspace from these claims. ## Role-Based Access: Operator Role The member console uses a single application-level role to gate access to the operator panel: - **Role name:** `operator-member` - **Where it's checked:** `internal/server/operator.go` and `operator_partials.go` ### How roles are extracted The member console extracts roles from the **ID token**, the token OIDC defines for the client to consume, after it has passed signature and nonce verification. The access token is not read: OAuth2 leaves its format opaque, so parsing it would only work with providers that happen to issue JWT access tokens. Within the ID token the console reads every claim location an identity provider might use to carry roles, rather than trusting only one: 1. `roles` (top-level array) 2. `groups` (top-level array) 3. `realm_access.roles` (Keycloak realm roles) 4. `resource_access..roles` (Keycloak client-scoped roles, read only for this app's configured client ID; roles belonging to other clients are never included) The member console reads all four locations and merges their roles into one deduplicated list: a role that appears in more than one location is kept once, at the position where it first appears (the locations are read in the order listed above, and within a location, in the order the claim lists them). Because every location is read, a deployment can put `operator-member` in whichever claim its identity provider produces and the member console still recognizes it. If the ID token carries **none** of the four locations at all, the identity provider is not mapping roles into the ID token. The console logs a warning naming the signed-in email and this document on every such login; without it, the only symptom would be an operator who cannot get into the operator panel. A location that is present but empty is an ordinary member with no roles and is not logged. ### Keycloak setup There are two ways to make the `operator-member` role visible in the ID token: #### Option A: Client role with a protocol mapper (recommended) 1. Go to **Clients > member-console > Roles** 2. Create a role named `operator-member` 3. Assign it to the desired user(s) under **Users > (user) > Role Mappings > member-console** 4. Ensure there is a protocol mapper that emits client roles into a top-level `roles` claim in the ID token: - **Mapper type:** User Client Role - **Client ID:** `member-console` - **Token Claim Name:** `roles` - **Claim JSON Type:** String - **Multivalued:** On - **Add to ID token:** On (**Add to access token** is not required) This is the approach used by the test stack's Keycloak seed (`test/seed/keycloak/seed-keycloak.sh`). #### Option B: Realm role 1. Go to **Realm Roles** 2. Create a role named `operator-member` 3. Assign it to the desired user(s) 4. Enable **Add to ID token** on the realm roles mapper (**Client scopes > roles > Mappers > realm roles**); it is off by default, so without this step the role reaches only the access token, which the console does not read. The role then appears in `realm_access.roles` in the ID token. This is simpler but mixes application roles with realm-level roles. ### Other identity providers For providers other than Keycloak, ensure that the ID token includes `operator-member` in one of the four checked claim locations (`roles`, `groups`, `realm_access.roles`, or `resource_access..roles`). Most providers support custom claims or group mappings that can achieve this. ## Temporal Authorization Temporal requires its own role setup for JWT-based authorization. See [Temporal Authorization Setup](temporal-authorization-setup.md) for the full Temporal permissions configuration. ## Test Environment The test stack's Keycloak seed (`test/seed/keycloak/seed-keycloak.sh`) pre-configures: - A `member-console` client with the `operator-member` client role defined - The admin user mapped to the `operator-member` role - Protocol mappers that emit roles into the ID token (and the access token) To use the test environment: ```bash cd test docker compose up -d member-console start ``` Login with `admin` / `admin` at `http://localhost:8081` — this is Keycloak's own admin console, a separate system from member-console itself. The console's operator login (`alice` / `password`) is covered in the [README quickstart](../README.md#quickstart-local-development). > **Run app OIDC on a dedicated realm, not Keycloak's `master` admin realm.** The member-facing "Identity and Access" link opens Keycloak's account console; on the `master` admin realm that console errors (*"Something went wrong"*) because the minted token carries no `aud: account` claim. The test stack therefore creates and runs on a dedicated `test` realm, where the account console works out of the box — do the same in production. See `status/issues.md`. > **Note:** The first `member-console start` after a fresh `docker compose up -d` used to fail with a Temporal auth error. This is fixed — compose now gates Temporal on its database and Keycloak being healthy, and member-console retries the initial Temporal connection. See `status/issues.md`.