mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-26 22:04:46 -06:00
62d2a0fe6a
* fix: remove non-auth support from bootstrap wizard Auth is now mandatory for all deployments. Remove the TURNSTONE_AUTH_ENABLED toggle and make JWT_SECRET and AUTH_TOKEN required in the wizard's system prompt. * fix: remove auth disable support from runtime and infra Remove AuthConfig.enabled field — auth is always on. Drop TURNSTONE_AUTH_ENABLED env var, config toggle, and the check_request bypass. Update compose.yaml, Helm chart, Terraform, docs, and tests to match. * feat: deprecate config tokens, require JWT secret, prefer JWT auth Phase 1 of config-token removal: - load_jwt_secret() now exits with error if no secret is configured (was: silently auto-generated ephemeral secret) - _authenticate_token() logs deprecation warning on config token use - CLI /cluster commands use ServiceTokenManager when JWT secret is set - turnstone-admin tls-list uses ServiceTokenManager when JWT secret is set - Update bootstrap wizard, docker.md, security.md to mark TURNSTONE_AUTH_TOKEN as deprecated and JWT_SECRET as required - Console test fixtures use auth token + headers (auth always enforced) * feat: add service scope for inter-service JWT auth Add "service" to VALID_SCOPES and SCOPE_HIERARCHY. Service tokens bypass require_permission() RBAC checks, replacing the old empty-user-id bypass that config tokens relied on. All ServiceTokenManager instances that need admin access now include "service" in their scopes (console proxy, channel gateway, CLI, admin CLI). Read-only services (collector, notification) unchanged. * feat: phase 2 config token deprecation - SDK doc examples now show API tokens (ts_) instead of config tokens - Remove _get_config_token() from admin CLI (dead code) - Block config token exchange in handle_auth_login — only password and API token login allowed - Update login tests to use password-based auth instead of config token exchange * feat: phase 3 — remove config tokens entirely Complete removal of config-file token authentication: - Delete AuthConfig.tokens, check(), _ROLE_TO_SCOPES, hmac dispatch branch, and config token loading from load_auth_config() - Remove auth_config parameter from _authenticate_token() and check_request() — callers updated throughout - Remove TURNSTONE_AUTH_TOKEN from compose.yaml, Helm charts, Terraform, turnstone.example.toml - Remove --auth-token CLI flags from turnstone, turnstone-admin, and turnstone-console - Simplify console main() — always use ServiceTokenManager (no fallback to static tokens) - Delete config-token-specific tests, rewrite check_request and integration tests to use JWT auth with proper audience claims - Remove all config token references from docs (security.md, docker.md, sdk.md, console.md, architecture.md, bootstrap prompt) * fix: address code review findings - Fix 33 broken tests: add JWT auth to test_api_versioning, test_console_routing_proxy, test_tls_admin, test_tls_manager, test_server_live (jwt_secret + audience-scoped auth headers) - Add TestRequirePermissionServiceScope: 4 tests covering the service scope RBAC bypass path - Remove stale comments referencing config tokens in auth.py and console/server.py - Remove dead proxy_auth_token parameter from console create_app() and static token fallback in _proxy_auth_headers() - Remove TURNSTONE_AUTH_TOKEN from env.py scrub list * fix: address Copilot review — JWT audience, compose require secret - CLI /cluster: add audience=JWT_AUD_CONSOLE to ServiceTokenManager (console validates audience, JWTs without it were rejected) - Admin CLI tls-list: same audience fix - compose.yaml: TURNSTONE_JWT_SECRET now uses :? to fail fast if unset - SDK console: fix default port from 8081 to 8090 * test: add auth enforcement tests for TLS admin endpoints 5 new tests: unauthenticated requests return 401 (list, renew, delete), read-only-scoped requests return 403 (renew, delete). Closes the TLS auth enforcement test gap noted in PROGRESS.md. * fix: address remaining Copilot review feedback - Fix token_source="config" → "test" in TLS test fixtures - Fix AuthResult.token_source docstring to include service origins - Require TURNSTONE_JWT_SECRET in cluster compose profile (:?) - Helm: add auth.jwtSecret + auth.existingSecret values, wire TURNSTONE_JWT_SECRET into secret.yaml and both deployments - Terraform: replace auth_token with jwt_secret variable + secret, remove orphaned auth_token resources and IAM reference - Remove [[auth.tokens]] from security.md config example * fix: address full code review — 10 findings Critical: - Terraform: replace concat(common_env, auth_env) with common_env (auth_env local was removed but still referenced) - Channel gateway: remove hmac static token auth from _check_auth(), use JWT-only validation. Remove --auth-token CLI arg from channel - Rebalancer: add token_manager support so migration requests carry JWT auth (was sending unauthenticated POST to /internal/migrate) Major: - Guard _permissions_to_scopes() against "service" privilege escalation from DB role permissions - Remove dead AuthConfig class, load_auth_config(), and all auth_config parameters from create_app() signatures - Helm: inject JWT secret for both inline and existingSecret paths Minor: - Remove dead auth_token param from ClusterCollector - Remove empty TestLoadAuthConfig class - Short JWT secret now exits instead of warning - Compose: add generation command comment above JWT_SECRET - Clean stale config token references from 6 doc files - Clean stale AUTH_TOKEN reference from bootstrap wizard prompt * fix: remove remaining stale config token references from docs - channels.md: remove --auth-token from options table - oidc.md: remove "config-file tokens still work" claim - security.md: remove config token section, fix JWT secret docs (now required/exits, no ephemeral fallback), remove hmac from ASCII diagram, remove --auth-token reference
430 lines
15 KiB
Markdown
430 lines
15 KiB
Markdown
# OpenID Connect (OIDC) Single Sign-On
|
|
|
|
Turnstone supports OpenID Connect for federated authentication, allowing
|
|
users to log in with their existing corporate identity provider instead of
|
|
managing a separate password. OIDC is opt-in: when configured, the login
|
|
screen shows a "Continue with SSO" button alongside the existing
|
|
username/password form. When not configured, the login experience is
|
|
unchanged.
|
|
|
|
Any OIDC-compliant provider works: Google, Okta, Azure AD, Keycloak,
|
|
Auth0, OneLogin, and others that publish a
|
|
`.well-known/openid-configuration` discovery document.
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
1. A registered **confidential** OIDC client at your identity provider
|
|
2. The client's redirect URI must include:
|
|
`https://your-turnstone-host/v1/api/auth/oidc/callback`
|
|
3. A local admin user must exist in Turnstone (complete the initial setup
|
|
wizard before enabling OIDC)
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
OIDC is configured via environment variables (preferred) or the `[oidc]`
|
|
section of `config.toml`. Environment variables take precedence when both
|
|
are set.
|
|
|
|
| Variable | Required | Default | Description |
|
|
|----------|----------|---------|-------------|
|
|
| `TURNSTONE_OIDC_ISSUER` | Yes | — | Issuer URL (e.g. `https://accounts.google.com`). Must serve `/.well-known/openid-configuration`. |
|
|
| `TURNSTONE_OIDC_CLIENT_ID` | Yes | — | OAuth 2.0 client ID from your provider |
|
|
| `TURNSTONE_OIDC_CLIENT_SECRET` | Yes | — | OAuth 2.0 client secret (confidential client) |
|
|
| `TURNSTONE_OIDC_SCOPES` | No | `openid email profile` | Space-separated OAuth scopes to request |
|
|
| `TURNSTONE_OIDC_PROVIDER_NAME` | No | `SSO` | Display name for the login button (e.g. "Google", "Okta") |
|
|
| `TURNSTONE_OIDC_ROLE_CLAIM` | No | — | ID token claim containing role/group values (see [Role Mapping](#role-mapping)) |
|
|
| `TURNSTONE_OIDC_ROLE_MAP` | No | — | Mapping from claim values to Turnstone role IDs (see [Role Mapping](#role-mapping)) |
|
|
| `TURNSTONE_OIDC_PASSWORD_ENABLED` | No | `true` | Set to `false` to hide the password form and block all username/password logins (including admin). API tokens continue to work. |
|
|
| `TURNSTONE_OIDC_REDIRECT_BASE` | No | — | Externally-reachable origin for the OIDC redirect URI (e.g. `https://app.example.com`). Recommended when running behind a reverse proxy. When unset, derived from the request Host header. |
|
|
|
|
OIDC is enabled when all three required fields (issuer, client ID, client
|
|
secret) are non-empty. If any is missing, OIDC is silently disabled and
|
|
the login screen shows only the password form.
|
|
|
|
### Reverse Proxy / Load Balancer
|
|
|
|
When Turnstone runs behind a reverse proxy, the internal `Host` header may
|
|
not match the externally-reachable URL. Set `TURNSTONE_OIDC_REDIRECT_BASE`
|
|
to the public origin so the redirect URI sent to the identity provider is
|
|
correct:
|
|
|
|
```bash
|
|
TURNSTONE_OIDC_REDIRECT_BASE=https://app.example.com
|
|
```
|
|
|
|
The resulting callback URL will be
|
|
`https://app.example.com/v1/api/auth/oidc/callback` — register this as the
|
|
authorized redirect URI in your identity provider.
|
|
|
|
### config.toml alternative
|
|
|
|
```toml
|
|
[oidc]
|
|
issuer = "https://accounts.google.com"
|
|
client_id = "your-client-id"
|
|
client_secret = "your-client-secret"
|
|
scopes = "openid email profile"
|
|
provider_name = "Google"
|
|
role_claim = "groups"
|
|
password_enabled = true
|
|
redirect_base = "https://app.example.com"
|
|
|
|
[oidc.role_map]
|
|
admin = "builtin-admin"
|
|
engineering = "builtin-operator"
|
|
```
|
|
|
|
---
|
|
|
|
## Provider-Specific Setup
|
|
|
|
### Google
|
|
|
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/) >
|
|
**APIs & Services** > **Credentials**
|
|
2. Click **Create Credentials** > **OAuth 2.0 Client ID**
|
|
3. Application type: **Web application**
|
|
4. Add authorized redirect URI:
|
|
`https://your-turnstone-host/v1/api/auth/oidc/callback`
|
|
5. Copy the **Client ID** and **Client secret**
|
|
|
|
```bash
|
|
TURNSTONE_OIDC_ISSUER=https://accounts.google.com
|
|
TURNSTONE_OIDC_CLIENT_ID=123456789.apps.googleusercontent.com
|
|
TURNSTONE_OIDC_CLIENT_SECRET=GOCSPX-...
|
|
TURNSTONE_OIDC_PROVIDER_NAME=Google
|
|
```
|
|
|
|
### Okta
|
|
|
|
1. In the Okta Admin Console, go to **Applications** > **Create App
|
|
Integration**
|
|
2. Sign-in method: **OIDC - OpenID Connect**
|
|
3. Application type: **Web Application**
|
|
4. Add sign-in redirect URI:
|
|
`https://your-turnstone-host/v1/api/auth/oidc/callback`
|
|
5. Note the **Issuer** (your Okta domain, e.g.
|
|
`https://dev-123456.okta.com`)
|
|
|
|
```bash
|
|
TURNSTONE_OIDC_ISSUER=https://dev-123456.okta.com
|
|
TURNSTONE_OIDC_CLIENT_ID=0oaXXXXXXXXXXXXX
|
|
TURNSTONE_OIDC_CLIENT_SECRET=...
|
|
TURNSTONE_OIDC_PROVIDER_NAME=Okta
|
|
TURNSTONE_OIDC_ROLE_CLAIM=groups
|
|
TURNSTONE_OIDC_ROLE_MAP="admin:builtin-admin,everyone:builtin-operator"
|
|
```
|
|
|
|
### Azure AD (Entra ID)
|
|
|
|
1. In the Azure Portal, go to **App registrations** > **New registration**
|
|
2. Redirect URI: **Web** >
|
|
`https://your-turnstone-host/v1/api/auth/oidc/callback`
|
|
3. Under **Certificates & secrets**, create a new **Client secret** and
|
|
copy the value immediately
|
|
4. The issuer URL is
|
|
`https://login.microsoftonline.com/{tenant-id}/v2.0`
|
|
|
|
```bash
|
|
TURNSTONE_OIDC_ISSUER=https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0
|
|
TURNSTONE_OIDC_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
TURNSTONE_OIDC_CLIENT_SECRET=...
|
|
TURNSTONE_OIDC_PROVIDER_NAME="Azure AD"
|
|
TURNSTONE_OIDC_ROLE_CLAIM=roles
|
|
TURNSTONE_OIDC_ROLE_MAP="Admin:builtin-admin,User:builtin-operator"
|
|
```
|
|
|
|
### Keycloak
|
|
|
|
1. In the Keycloak Admin Console, select your **Realm**
|
|
2. Go to **Clients** > **Create client**
|
|
3. Client type: **OpenID Connect**
|
|
4. Set **Client authentication** to **On** (confidential)
|
|
5. Add valid redirect URI:
|
|
`https://your-turnstone-host/v1/api/auth/oidc/callback`
|
|
6. The issuer URL is
|
|
`https://keycloak.example.com/realms/your-realm`
|
|
|
|
```bash
|
|
TURNSTONE_OIDC_ISSUER=https://keycloak.example.com/realms/your-realm
|
|
TURNSTONE_OIDC_CLIENT_ID=turnstone
|
|
TURNSTONE_OIDC_CLIENT_SECRET=...
|
|
TURNSTONE_OIDC_PROVIDER_NAME=Keycloak
|
|
TURNSTONE_OIDC_ROLE_CLAIM=realm_access.roles
|
|
TURNSTONE_OIDC_ROLE_MAP="admin:builtin-admin,operator:builtin-operator"
|
|
```
|
|
|
|
---
|
|
|
|
## Role Mapping
|
|
|
|
OIDC role mapping assigns Turnstone roles to users based on claims in the
|
|
ID token. This is optional — without it, OIDC users are provisioned with
|
|
the `builtin-viewer` role (read-only access) by default.
|
|
|
|
### Configuration
|
|
|
|
Set `TURNSTONE_OIDC_ROLE_CLAIM` to the name of the claim in the ID token
|
|
that contains the user's group or role memberships. Then set
|
|
`TURNSTONE_OIDC_ROLE_MAP` to map claim values to Turnstone role IDs.
|
|
|
|
The role map is a comma-separated list of `claim_value:turnstone_role`
|
|
pairs:
|
|
|
|
```bash
|
|
TURNSTONE_OIDC_ROLE_CLAIM=groups
|
|
TURNSTONE_OIDC_ROLE_MAP="admin:builtin-admin,engineering:builtin-operator,viewer:builtin-viewer"
|
|
```
|
|
|
|
### Behavior
|
|
|
|
- **Synced on every login**: roles are added when new claim values appear,
|
|
and OIDC-assigned roles are revoked when the corresponding claim value
|
|
is no longer present. Roles assigned manually (or by other sources) are
|
|
never touched — only roles with `assigned_by="oidc"` are subject to
|
|
revocation.
|
|
- **List or string**: the claim value can be a JSON array
|
|
(`["admin", "engineering"]`) or a single string (`"admin"`). Both are
|
|
handled correctly.
|
|
- **Unknown values**: claim values not present in the role map are silently
|
|
ignored.
|
|
- **Missing roles**: if the role map references a Turnstone role ID that
|
|
does not exist in the database, the assignment is skipped (no error).
|
|
- **Evaluated on every login**: roles are checked and applied each time
|
|
the user authenticates via OIDC, so new group memberships are picked
|
|
up on the next login.
|
|
|
|
### Built-in Roles
|
|
|
|
| Role ID | Permissions |
|
|
|---------|-------------|
|
|
| `builtin-admin` | All permissions |
|
|
| `builtin-operator` | read, write, workstreams.create, workstreams.close |
|
|
| `builtin-viewer` | read |
|
|
|
|
---
|
|
|
|
## User Provisioning
|
|
|
|
When a user logs in via OIDC for the first time, Turnstone automatically
|
|
creates a local user account:
|
|
|
|
1. The OIDC identity (`issuer` + `sub` claim) is stored in the
|
|
`oidc_identities` table and linked to the new user
|
|
2. The **username** is derived from the `preferred_username` claim,
|
|
falling back to the email local part, with deduplication if needed
|
|
3. The **display name** comes from the `name` claim, falling back to
|
|
`preferred_username` or email
|
|
4. The user's password hash is set to a sentinel value (`!oidc`) — OIDC
|
|
users cannot log in with a password
|
|
|
|
On subsequent logins, the existing user is matched by `(issuer, sub)` and
|
|
the `last_login` timestamp is updated. Role mapping is re-evaluated on
|
|
every login.
|
|
|
|
---
|
|
|
|
## OIDC-Only Mode
|
|
|
|
To enforce OIDC for all logins and hide the password form, set:
|
|
|
|
```bash
|
|
TURNSTONE_OIDC_PASSWORD_ENABLED=false
|
|
```
|
|
|
|
In this mode the login screen shows only the "Continue with SSO" button.
|
|
The password form, token toggle, and sign-in button are all hidden.
|
|
All username/password logins are blocked at the API level, including
|
|
admin accounts.
|
|
|
|
The first admin account must be created via the setup wizard (with a
|
|
password) before OIDC is enabled. The setup wizard always works
|
|
regardless of this setting because it is only available when zero users
|
|
exist in the database.
|
|
|
|
API token login (`POST /v1/api/auth/login` with a `ts_` token)
|
|
continues to work regardless of this setting. JWTs and API tokens are
|
|
the supported authentication methods. OIDC-only mode affects
|
|
password-based authentication only.
|
|
|
|
---
|
|
|
|
## Login Flow
|
|
|
|
Both the server and console support OIDC login. The flow is identical:
|
|
|
|
1. The browser fetches `GET /v1/api/auth/status` at page load
|
|
2. If the response includes `oidc_enabled: true`, the login screen shows
|
|
a "Continue with {provider_name}" button
|
|
3. Clicking the button navigates to `GET /v1/api/auth/oidc/authorize`
|
|
4. Turnstone generates a state token, nonce, and PKCE verifier, stores
|
|
them in the database, and redirects the browser to the identity
|
|
provider's authorization endpoint
|
|
5. The user authenticates at the identity provider
|
|
6. The IdP redirects back to
|
|
`GET /v1/api/auth/oidc/callback?code=...&state=...`
|
|
7. Turnstone validates the state, exchanges the authorization code for
|
|
tokens using the PKCE verifier, validates the ID token against the
|
|
provider's JWKS public keys, provisions or matches the user, and
|
|
issues a Turnstone JWT
|
|
8. The browser is redirected to `/?oidc_success=1` with the JWT set in
|
|
an `HttpOnly` session cookie
|
|
9. The browser JavaScript detects the `oidc_success` query parameter,
|
|
strips it from the URL, hides the login overlay, and calls
|
|
`onLoginSuccess()` to initialize the application
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| GET | `/v1/api/auth/oidc/authorize` | Public | Redirects to identity provider |
|
|
| GET | `/v1/api/auth/oidc/callback` | Public | Handles IdP callback, issues JWT |
|
|
|
|
Both endpoints are public (no authentication required) because they are
|
|
part of the login flow itself.
|
|
|
|
### Auth status response
|
|
|
|
When OIDC is enabled, `GET /v1/api/auth/status` includes additional
|
|
fields:
|
|
|
|
```json
|
|
{
|
|
"auth_enabled": true,
|
|
"has_users": true,
|
|
"setup_required": false,
|
|
"oidc_enabled": true,
|
|
"oidc_provider_name": "Google",
|
|
"password_enabled": true
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Database Schema
|
|
|
|
Migration 018 creates two tables:
|
|
|
|
```sql
|
|
CREATE TABLE oidc_identities (
|
|
issuer TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
email TEXT NOT NULL DEFAULT '',
|
|
created TEXT NOT NULL,
|
|
last_login TEXT NOT NULL,
|
|
PRIMARY KEY (issuer, subject)
|
|
);
|
|
|
|
CREATE INDEX idx_oidc_identities_user_id ON oidc_identities(user_id);
|
|
|
|
CREATE TABLE oidc_pending_states (
|
|
state TEXT PRIMARY KEY,
|
|
nonce TEXT NOT NULL,
|
|
code_verifier TEXT NOT NULL,
|
|
audience TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
The `oidc_identities` table links an OIDC subject (identified by
|
|
`issuer` + `subject`) to a Turnstone `user_id`. A single user can have
|
|
multiple OIDC identities (e.g. from different providers).
|
|
|
|
The `oidc_pending_states` table stores authorization flow state for
|
|
callback validation. Entries are automatically cleaned up after 5 minutes.
|
|
|
|
---
|
|
|
|
## Security Notes
|
|
|
|
- **Authorization Code Flow with PKCE**: the recommended OAuth 2.0 flow
|
|
for web applications. PKCE prevents authorization code interception
|
|
attacks even without a client secret (though the client secret is still
|
|
used for additional security).
|
|
- **ID token validation**: all tokens are validated using the provider's
|
|
JWKS public keys (RS256 or ES256). The signature, issuer, audience,
|
|
and expiry are all checked.
|
|
- **State parameter**: a cryptographically random state token prevents
|
|
CSRF attacks on the callback endpoint. The state is stored server-side
|
|
and verified on callback.
|
|
- **Nonce**: a random nonce is included in the authorization request and
|
|
verified in the ID token to prevent replay attacks.
|
|
- **Client secret**: never leaves the server — it is only used in the
|
|
server-to-IdP token exchange, not exposed to the browser.
|
|
- **OIDC users cannot use password login**: the sentinel password hash
|
|
(`!oidc`) ensures `verify_password()` always rejects password attempts
|
|
for OIDC-provisioned users.
|
|
- **Rate limiting**: the callback endpoint shares the login rate limiter
|
|
(5 attempts per 5-minute window per IP).
|
|
- **State TTL**: pending authorization states expire after 5 minutes.
|
|
Expired states are lazily cleaned up on each callback.
|
|
- **Setup guard**: OIDC login requires at least one local admin user to
|
|
exist. This ensures the initial admin account is always created via the
|
|
setup wizard with a password, not hijacked by an external identity.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### "OIDC not configured"
|
|
|
|
All three required environment variables must be set:
|
|
`TURNSTONE_OIDC_ISSUER`, `TURNSTONE_OIDC_CLIENT_ID`, and
|
|
`TURNSTONE_OIDC_CLIENT_SECRET`. Check that none are empty or
|
|
whitespace-only.
|
|
|
|
### "Login session expired"
|
|
|
|
The authorization flow must complete within 5 minutes. If the user takes
|
|
too long at the identity provider, the pending state expires. Try again.
|
|
|
|
### "Initial setup required"
|
|
|
|
OIDC login is blocked until at least one local admin user exists.
|
|
Complete the setup wizard first (navigate to the Turnstone URL and follow
|
|
the prompts to create an admin user with a password).
|
|
|
|
### Discovery fails at startup
|
|
|
|
Check that the issuer URL is reachable from the Turnstone server and
|
|
serves a valid `/.well-known/openid-configuration` document. The server
|
|
logs the discovery attempt at startup:
|
|
|
|
```
|
|
OIDC discovery failed for https://your-issuer.example.com: ...
|
|
```
|
|
|
|
OIDC is automatically disabled when discovery fails. Restart the server
|
|
after fixing the connectivity issue.
|
|
|
|
### Redirect URI mismatch
|
|
|
|
The redirect URI configured at the identity provider must exactly match
|
|
`https://your-host/v1/api/auth/oidc/callback`. Common issues:
|
|
|
|
- **Scheme mismatch**: the redirect uses `https://` — make sure TLS is
|
|
configured or a reverse proxy sets the `X-Forwarded-Proto` header
|
|
- **Port mismatch**: if running on a non-standard port, include it in
|
|
the redirect URI
|
|
- **Path mismatch**: the path must include the `/v1` API version prefix
|
|
|
|
### User not assigned expected roles
|
|
|
|
Check that:
|
|
|
|
1. `TURNSTONE_OIDC_ROLE_CLAIM` matches the exact claim name in the ID
|
|
token (case-sensitive)
|
|
2. `TURNSTONE_OIDC_ROLE_MAP` maps the correct claim values to valid
|
|
Turnstone role IDs
|
|
3. The roles referenced in the map exist in the database (check the
|
|
admin panel > Roles tab)
|
|
4. The identity provider is configured to include the claim in the ID
|
|
token (some providers require explicit scope or claim configuration)
|