+
+
+You will need to make 2 changes in the app manifest
+
+```json
+"requestedAccessTokenVersion": 2
+```
+
+and
+
+```json
+"optionalClaims": {
+ "idToken": [
+ {
+ "name": "roles",
+ "source": null,
+ "essential": false,
+ "additionalProperties": []
+ }
+ ],
+ "accessToken": [],
+ "saml2Token": []
+}
+```
+
+## Step 9: Configure Bifrost
Now configure Bifrost to use Microsoft Entra as the identity provider.
### Using the Bifrost UI
+
+
+
+
1. Navigate to **Governance** → **User Provisioning** in your Bifrost dashboard
2. Select **Microsoft Entra** as the SCIM Provider
3. Enter the following configuration:
@@ -221,8 +254,9 @@ Now configure Bifrost to use Microsoft Entra as the identity provider.
| **Audience** | Your Client ID (optional, defaults to Client ID) |
| **App ID URI** | `api://{client-id}` (optional, for v1.0 tokens) |
-4. Toggle **Enabled** to activate the provider
-5. Click **Save Configuration**
+5. **Verify** configuration and see if you get any errors. Make sure you get no errors/warnings.
+6. Toggle **Enabled** to activate the provider
+7. Click **Save Configuration**
+
+
+
+
+
+
+#### Evaluation rules
+
+- **Role mappings**: Ordered, first match wins. If no rule matches, users are not allowed to login into the system.
+- **Team and business unit mappings**: All matching rules apply — users can be placed on multiple teams and business units simultaneously.
+- **Claim values**: Can be strings, arrays, or nested objects. Bifrost resolves dotted paths (e.g., `realm_access.roles`).
+
+
+
+
+2. Choose **Internal** if you only want Workspace users, or **External** otherwise.
+3. Fill in App name, support email, and developer contact.
+4. Add the scopes: `openid`, `profile`, `email`.
+5. Save.
+
+---
+
+## Step 2: Create an OAuth Client ID
+
+1. Open **APIs & Services → Credentials → Create credentials → OAuth client ID**.
+
+
+
+
+
+2. Configure:
+
+| Field | Value |
+| --- | --- |
+| **Application type** | Web application |
+| **Name** | Bifrost Enterprise |
+| **Authorized JavaScript origins** | `https://your-bifrost-domain.com` |
+| **Authorized redirect URIs** | `https://your-bifrost-domain.com/login` |
+
+3. Save and copy the **Client ID** and **Client Secret**.
+
+---
+
+## Step 3: (Optional) Create a service account for Directory API access
+
+Skip this section if you only want SSO login without directory-based user import.
+
+1. Go to **IAM & Admin → Service Accounts → Create service account**.
+
+
+
+
+
+2. Give it a name (e.g. `bifrost-provisioning`). You can skip the "Grant this service account access to project" step — no GCP IAM roles are required; access is granted via domain-wide delegation in Step 5.
+3. Open the service account → **Keys → Add Key → Create new key → JSON**. Download and store the JSON file securely.
+4. From the service account **Details** tab, copy the **Unique ID** (a numeric value, **not** the email or OAuth Client ID).
+
+---
+
+## Step 4: Enable the Admin SDK API
+
+If you're using the service account path:
+
+1. Open **APIs & Services → Library**.
+2. Search for **Admin SDK API** and click **Enable**.
+
+---
+
+## Step 5: Set up domain-wide delegation
+
+1. In the [Google Admin Console](https://admin.google.com), go to **Security → Access and data control → API controls → Manage Domain Wide Delegation**.
+
+
+
+
+
+2. Click **Add new**.
+3. Enter the service account's **Unique ID** (from Step 3).
+4. Add these OAuth scopes (copy the full URLs, comma-separated):
+
+```
+https://www.googleapis.com/auth/admin.directory.user.readonly,
+https://www.googleapis.com/auth/admin.directory.group.readonly,
+https://www.googleapis.com/auth/admin.directory.group.member.readonly
+```
+
+5. **Authorize**.
+
+
+
+
+4. Click **Verify** — Bifrost validates the OAuth client and, if a service account is provided, attempts a Directory API impersonation to confirm delegation is working.
+5. Configure **Attribute → Role / Team / Business Unit** mappings to map groups or organizational units to Bifrost roles and teams.
+6. Toggle **Enabled** and click **Save Configuration**.
+
+### Using `config.json`
+
+```json
+{
+ "scim_config": {
+ "enabled": true,
+ "provider": "google",
+ "config": {
+ "domain": "company.com",
+ "clientId": "123-abc.apps.googleusercontent.com",
+ "clientSecret": "${GOOGLE_WORKSPACE_CLIENT_SECRET}",
+ "adminEmail": "sso-admin@company.com",
+ "serviceAccountEnvVar": "GOOGLE_SA_JSON",
+ "teamIdsField": "groups"
+ }
+ }
+}
+```
+
+Pick one of the three service-account sources: `serviceAccountJson` (raw JSON string), `serviceAccountEnvVar` (env var name holding the JSON), or `serviceAccountFile` (absolute path to the key file).
+
+
+### Custom attribute mapping
+
+You can also map any custom attributes to any entity (role, team or business unit). Make sure these are configured to send back to Bifrost in token configuration.
+
+
+
+
+
+### Configuration reference
+
+| Field | Required | Description |
+| --- | --- | --- |
+| `domain` | Yes | Google Workspace primary domain (e.g. `company.com`). |
+| `clientId` | Yes | OAuth 2.0 Web Client ID from Step 2. |
+| `clientSecret` | Yes | Client Secret — required for token revocation and for confidential server-side flows. |
+| `audience` | No | Expected JWT audience. Defaults to `clientId`. |
+| `adminEmail` | Yes | Workspace admin to impersonate via domain-wide delegation. Required when any service-account field is set. |
+| `serviceAccountJson` | One of 3 | Raw JSON string of the service account key. |
+| `serviceAccountEnvVar` | One of 3 | Name of the environment variable containing the JSON. |
+| `serviceAccountFile` | One of 3 | Absolute path to the JSON key file on the Bifrost host. |
+| `attributeRoleMappings` | Yes | Ordered list of attribute→role mappings. |
+| `attributeTeamMappings` | No | Attribute→team mappings (all matches apply). |
+| `attributeBusinessUnitMappings` | No | Attribute→business-unit mappings (all matches apply). |
+
+
+
+
+2. Configure the client:
+
+| Field | Value |
+| --- | --- |
+| **Client type** | OpenID Connect |
+| **Client ID** | `bifrost` (or your preferred identifier) |
+| **Name** | `Bifrost Enterprise` |
+
+3. On the **Capability config** step enable:
+ - **Client authentication** (makes it a confidential client)
+ - **Standard flow** (Authorization Code)
+ - **Service accounts roles** (required for Admin REST API access)
+
+
+
+
+
+4. On the **Login settings** step set:
+
+| Field | Value |
+| --- | --- |
+| **Valid redirect URIs** | `https://your-bifrost-domain.com/login` |
+| **Valid post logout redirect URIs** | `https://your-bifrost-domain.com` |
+| **Web origins** | `https://your-bifrost-domain.com` |
+
+5. **Save** the client.
+
+---
+
+## Step 2: Copy the client credentials
+
+1. Open the client and go to the **Credentials** tab.
+2. Copy the **Client Secret**.
+
+
+
+
+
+---
+
+## Step 3: Configure role and group mappers
+
+Keycloak does not include realm roles or full group paths in tokens by default. Add two mappers on the client's dedicated scope.
+
+1. Open the client → **Client Scopes** tab → click the client's `-dedicated` scope.
+2. Click **Add mapper → By configuration**.
+
+### Group Membership mapper
+
+
+
+
+
+| Field | Value |
+| --- | --- |
+| **Mapper Type** | Group Membership |
+| **Name** | `groups` |
+| **Token Claim Name** | `groups` |
+| **Full group path** | **On** |
+| **Add to ID token** | **On** |
+| **Add to access token** | **On** |
+| **Add to userinfo** | **On** |
+
+
+
+
+---
+
+## Step 5: Create realm roles and groups
+
+Create the roles and groups you plan to map into Bifrost.
+
+1. **Realm → Realm roles → Create role** for each role (e.g. `bifrost-admin`, `bifrost-developer`, `bifrost-viewer`).
+2. **Realm → Groups → Create group** for each team you want to sync (e.g. `/platform`, `/data-science`).
+3. Assign users to the appropriate roles and groups under **Users → your user → Role mapping** / **Groups**.
+
+---
+
+## Step 6: Configure Bifrost
+
+### Using the Bifrost dashboard
+
+1. In Bifrost, go to **Governance → User Provisioning**.
+2. Select **Keycloak** as the SCIM Provider.
+3. Fill in the fields:
+
+| Field | Value |
+| --- | --- |
+| **Server URL** | `https://keycloak.company.com` (no `/realms/...` suffix) |
+| **Realm** | Your realm name (e.g. `master`, `bifrost-prod`) |
+| **Client ID** | Client ID from Step 1 |
+| **Client Secret** | Client Secret from Step 2 |
+| **Audience** | Optional — defaults to Client ID |
+| **Team IDs Field** | Leave as `groups` (default) or change if you used a different mapper name |
+
+4. Click **Verify** — Bifrost connects to Keycloak's JWKS and Admin REST API to confirm the client and service-account roles.
+5. Configure **Attribute → Role / Team / Business Unit** mappings if needed.
+6. Toggle **Enabled** and click **Save Configuration**.
+
+
+
+
+
+### Using `config.json`
+
+```json
+{
+ "scim_config": {
+ "enabled": true,
+ "provider": "keycloak",
+ "config": {
+ "serverUrl": "https://keycloak.company.com",
+ "realm": "bifrost-prod",
+ "clientId": "bifrost",
+ "clientSecret": "${KEYCLOAK_CLIENT_SECRET}",
+ "teamIdsField": "groups"
+ }
+ }
+}
+```
+
+### Configuration reference
+
+| Field | Required | Description |
+| --- | --- | --- |
+| `serverUrl` | Yes | Base URL of the Keycloak server. Must be a valid URL (e.g. `https://keycloak.company.com`) and must **not** include `/realms/...`. |
+| `realm` | Yes | Realm name. |
+| `clientId` | Yes | Client ID created in Step 1. |
+| `clientSecret` | Yes | Client secret — required because the client is confidential. |
+| `audience` | No | Expected JWT audience. Defaults to `clientId`. |
+| `teamIdsField` | No | JWT claim for group IDs. Defaults to `groups`. |
+| `attributeRoleMappings` | No | Ordered list of attribute→role mappings. |
+| `attributeTeamMappings` | No | Attribute→team mappings (all matches apply). |
+| `attributeBusinessUnitMappings` | No | Attribute→business-unit mappings (all matches apply). |
+
+---
+
+## Testing the Integration
+
+1. Open the Bifrost dashboard in an incognito window.
+2. You'll be redirected to Keycloak's login page.
+3. Sign in with a Keycloak user that has one of the roles you mapped.
+4. Verify the user appears under **Governance → Users** with the expected role and teams.
+5. From **Governance → User Provisioning → Import Users**, verify the service account can list users.
+
+---
+
+## Troubleshooting
+
+### `serverUrl must not include /realms/{realm}`
+
+The `serverUrl` field is the base Keycloak URL. Set the realm in the separate **Realm** field. Example: `https://keycloak.company.com` + realm `bifrost-prod` — **not** `https://keycloak.company.com/realms/bifrost-prod`.
+
+### Users redirected back to login
+
+- Confirm the client's **Valid redirect URIs** exactly match your Bifrost login URL (trailing slash matters).
+- Verify the client is **Enabled** in Keycloak.
+
+### Roles not appearing in the token
+
+- Check that the **User Realm Role** mapper adds to both ID and Access tokens.
+- Use `Evaluate` on the client scope to preview the token a user would receive.
+
+### Service account cannot list users
+
+- Confirm `realm-management → view-users` is assigned under **Service accounts roles**.
+- If you enabled **Authorization** on the client, service account tokens may not work — disable Authorization (fine-grained authz) for this client.
+
+### `jwks keys not found`
+
+- Make sure the server URL is reachable from Bifrost. The JWKS endpoint is `{serverUrl}/realms/{realm}/protocol/openid-connect/certs`.
+
+---
+
+## Next Steps
+
+- [User Provisioning overview](./user-provisioning) — capabilities, attribute mappings, bulk import
+- [Role-Based Access Control](./rbac) — configure custom roles before mapping
+- [Audit Logs](./audit-logs) — track authentication events
diff --git a/docs/enterprise/setting-up-okta.mdx b/docs/enterprise/setting-up-okta.mdx
index 7b6f25f0bc..26ac5e8225 100644
--- a/docs/enterprise/setting-up-okta.mdx
+++ b/docs/enterprise/setting-up-okta.mdx
@@ -13,6 +13,7 @@ This guide walks you through configuring Okta as your identity provider for Bifr
- An Okta organization with admin access
- Bifrost Enterprise deployed and accessible
- The redirect URI for your Bifrost instance (e.g., `https://your-bifrost-domain.com/login`)
+- Ensure you have created all the [roles in Bifrost](/enterprise/rbac) that you are aiming to map to with Okta.
---
@@ -47,20 +48,25 @@ Configure the following settings for your application:
**General Settings:**
+
- **App integration name**: `Bifrost Enterprise`
- **Logo** (optional): You can upload the Bifrost logo from [https://www.getmaxim.ai/bifrost/bifrost-logo-only.png](https://www.getmaxim.ai/bifrost/bifrost-logo-only.png)
**Grant type:**
+
- Enable **Authorization Code**
- Enable **Refresh Token**
**Sign-in redirect URIs:**
+
- Add your Bifrost login callback URL: `https://your-bifrost-domain.com/login`
**Sign-out redirect URIs (Optional):**
+
- Add your Bifrost base URL: `https://your-bifrost-domain.com`
**Assignments:**
+
- Choose **Skip group assignment for now** (we'll configure this later)
6. Click **Save** to create the application
@@ -71,39 +77,13 @@ Configure the following settings for your application:
---
-## Step 3: Configure Authorization Server (optional)
+## Step 3: Create Custom Role Attribute (Optional)
-
-
-3. Note the **Issuer URI** for your authorization server (e.g., `https://your-domain.okta.com/oauth2/default`)
-
-
-| Field | Value |
-|-------|-------|
-| **Data type** | string |
-| **Display name** | bifrostRole |
-| **Variable name** | bifrostRole |
-| **Enum** | Check "Define enumerated list of values" |
+| Field | Value |
+| --------------------- | ----------------------------------------------------------- |
+| **Data type** | string |
+| **Display name** | bifrostRole |
+| **Variable name** | bifrostRole |
+| **Enum** | Check "Define enumerated list of values" |
| **Attribute members** | Admin → `admin`, Developer → `developer`, Viewer → `viewer` |
-| **Attribute type** | Personal |
+| **Attribute type** | Personal |
5. Click **Save**
---
-## Step 5: Add Role Claim to Tokens
+## Step 4: Add Role Claim to Tokens (If you have added custom role attribute)
Configure the authorization server to include the role in the access token.
@@ -148,27 +128,29 @@ Configure the authorization server to include the role in the access token.
Configure the claim:
-| Field | Value |
-|-------|-------|
-| **Name** | `role` |
+| Field | Value |
+| ------------------------- | -------------------- |
+| **Name** | `role` |
| **Include in token type** | Access Token, Always |
-| **Value type** | Expression |
-| **Value** | `user.bifrostRole` |
-| **Include in** | Any scope |
+| **Value type** | Expression |
+| **Value** | `user.bifrostRole` |
+| **Include in** | Any scope |
5. Click **Create**
-
-
-5. Click **Save**
-
-
-
-
-2. Under **OpenID Connect ID Token**, configure:
- - **Groups claim type**: Expression
- - **Groups claim expression**: `Arrays.flatten(Groups.startsWith("OKTA", "bifrost", 100))`
-
-
@@ -271,72 +215,121 @@ Adjust the group filter expression based on your naming convention. The example
4. Click **Save and Go Back**
-
+
+
+1. Click on "Create token"
+
+
+
+
+
+2. Copy token to be used in the next step.
+
## Step 8: Configure Bifrost
Now configure Bifrost to use Okta as the identity provider.
### Using the Bifrost UI
+
+
+
+
1. Navigate to **Governance** → **User Provisioning** in your Bifrost dashboard
2. Select **Okta** as the SCIM Provider
3. Enter the following configuration:
-| Field | Value |
-|-------|-------|
-| **Client ID** | Your Okta application Client ID |
-| **Issuer URL** | Issuer URL |
-| **Audience** | Your API audience (e.g., `api://default` or custom) |
+| Field | Value |
+| ----------------- | -------------------------------------------------------------------- |
+| **Client ID** | Your Okta application Client ID |
+| **Issuer URL** | Issuer URL |
+| **Audience** | Your API audience (e.g., `api://default` or custom) |
| **Client Secret** | Your Okta application Client Secret (optional, for token revocation) |
-4. Toggle **Enabled** to activate the provider
-5. Click **Save Configuration**
+4. **Verify** configuration and see if you get any errors. Make sure you get no errors/warnings.
+5. Toggle **Enabled** to activate the provider
+6. Click **Save Configuration**
+
+
+
-| Group Claim Name | Role |
-|------------------|------|
-| `bifrost-staging-admins` | Admin |
-| `bifrost-staging-viewers` | Viewer |
+
+
+
+
+#### Evaluation rules
+
+- **Role mappings**: Ordered, first match wins. If no rule matches, users are not allowed to login into the system.
+- **Team and business unit mappings**: All matching rules apply — users can be placed on multiple teams and business units simultaneously.
+- **Claim values**: Can be strings, arrays, or nested objects. Bifrost resolves dotted paths (e.g., `realm_access.roles`).
+
+
+
+2. Configure the application:
+
+| Field | Value |
+| --- | --- |
+| **Type** | Web |
+| **Authentication method** | PKCE (recommended) or Basic |
+| **Redirect URI** | `https://your-bifrost-domain.com/login` |
+| **Post logout URI** | `https://your-bifrost-domain.com` |
+
+3. After creating the app, note the **Client ID** from the application detail page.
+
+
+
+
+
+4. If you chose a confidential authentication method, also copy the **Client Secret** (shown once).
+
+---
+
+## Step 2: Enable role claims on the project (Optional)
+
+
+
+
+4. Note the **Project ID** — you'll need it for the Bifrost config so Bifrost can resolve the correct project roles.
+
+
+
+
+
+1. In the same project, open the **Roles** tab and create a role for each Bifrost role you plan to map. Common pattern:
+2. Authorize users to the relevant roles via **Users → Roles**.
+
+
+
+
+
+
+---
+
+## Step 4: Create a service account for provisioning
+
+Web apps in Zitadel cannot use the `client_credentials` grant. Bifrost needs a dedicated service account to list users via the Management API.
+
+1. Navigate to **Users → Service Accounts → New**.
+
+
+
+
+
+2. Name it (e.g. `bifrost-provisioning`) and create it.
+3. Open the service account → **Actions → Generate Client Secret**.
+
+
+
+
+
+4. **Copy the Client ID and Client Secret immediately** — the secret is shown only once.
+
+
+
+
+---
+
+## Step 5: App token settings
+
+1. Change **Auth Token Type** to JWT.
+2. Enable roles and profile info in ID token.
+
+
+
+
+
+
+## Step 6: Configure Bifrost
+
+### Using the Bifrost dashboard
+
+
+1. In Bifrost, go to **Governance → User Provisioning**.
+2. Select **Zitadel** as the SCIM Provider.
+3. Fill in the fields:
+
+| Field | Value |
+| --- | --- |
+| **Domain** | Your Zitadel host, e.g. `my-instance.zitadel.cloud` or `auth.company.com` (no scheme, no path) |
+| **Project ID** | The project ID from Step 2 |
+| **Client ID** | Web Application Client ID from Step 1 |
+| **Client Secret** | Web Application Client Secret from Step 1 (optional for PKCE) |
+| **Audience** | Optional access-token audience override |
+| **Service Account Client ID** | From Step 4 |
+| **Service Account Client Secret** | From Step 4 |
+
+4. Click **Verify** — Bifrost connects to Zitadel's JWKS and service account token endpoints to confirm the credentials.
+5. Configure **Attribute → Role / Team / Business Unit** mappings if you need to translate project roles or metadata into Bifrost roles.
+6. Toggle **Enabled** and click **Save Configuration**.
+
+
+
+
+
+### Using `config.json`
+
+```json
+{
+ "scim_config": {
+ "enabled": true,
+ "provider": "zitadel",
+ "config": {
+ "domain": "my-instance.zitadel.cloud",
+ "projectId": "123456789012345678",
+ "clientId": "123456789012345678@my-project",
+ "clientSecret": "${ZITADEL_CLIENT_SECRET}",
+ "serviceAccountClientId": "987654321098765432@my-project",
+ "serviceAccountClientSecret": "${ZITADEL_SA_CLIENT_SECRET}",
+ "teamIdsField": "groups"
+ }
+ }
+}
+```
+
+### Custom attribute mapping
+
+You can also map any custom attributes to any entity (role, team or business unit). Make sure these are configured to send back to Bifrost in token configuration.
+
+
+
+
+
+### Configuration reference
+
+| Field | Required | Description |
+| --- | --- | --- |
+| `domain` | Yes | Zitadel instance host (no scheme). Examples: `my-instance.zitadel.cloud`, `auth.company.com`. |
+| `clientId` | Yes | Client ID of the Web Application used for user login. |
+| `clientSecret` | Yes | Web Application secret. Omit for PKCE-only flows. |
+| `projectId` | Yes | Required to resolve project-scoped role claims and sync role grants. |
+| `audience` | No | Override the expected JWT `aud` claim. |
+| `serviceAccountClientId` | Yes | Service account used to list users via the Management API. |
+| `serviceAccountClientSecret` | Yes | Service account secret (shown once in Zitadel). |
+| `attributeRoleMappings` | Yes | Ordered list of attribute→role mappings. |
+| `attributeTeamMappings` | No | Attribute→team mappings (all matches apply). |
+| `attributeBusinessUnitMappings` | No | Attribute→business-unit mappings (all matches apply). |
+
+---
+
+## Testing the Integration
+
+1. Open the Bifrost dashboard in an incognito window.
+2. You'll be redirected to Zitadel. Sign in with a user who has a project authorization.
+3. On successful login you return to Bifrost and appear in **Governance → Users** with the correct role.
+4. From **Governance → User Provisioning → Import Users**, verify you can preview and import additional users via the service account.
+
+---
+
+## Troubleshooting
+
+### `role claims missing in token`
+
+- Enable **Assert Roles on Authentication** on the project (Step 2).
+- Ensure the user has an active authorization for the project.
+
+### `invalid audience` when validating the JWT
+
+- Check the `audience` field in the Bifrost config. It must match the `aud` claim issued by Zitadel. Leaving it empty uses the default (the project's resource owner).
+
+### Service account cannot list users
+
+- Confirm the service account has **IAM_USER_READ** or **Org Owner** role in the organization.
+- Regenerate the client secret if you've lost it — the original secret cannot be retrieved.
+
+### Redirect URI mismatch
+
+- Zitadel requires an exact string match. Check for trailing slashes and `http` vs `https`.
+
+---
+
+## Next Steps
+
+- [User Provisioning overview](./user-provisioning) — capabilities, attribute mappings, bulk import
+- [Role-Based Access Control](./rbac) — configure custom roles before mapping
+- [Audit Logs](./audit-logs) — track authentication events
diff --git a/docs/enterprise/user-provisioning.mdx b/docs/enterprise/user-provisioning.mdx
new file mode 100644
index 0000000000..14a7e182b8
--- /dev/null
+++ b/docs/enterprise/user-provisioning.mdx
@@ -0,0 +1,197 @@
+---
+title: "User Provisioning (SCIM)"
+description: "Authenticate users, sync teams, and provision roles and business units from your identity provider using SCIM-backed OAuth 2.0 / OIDC flows."
+icon: "users-gear"
+---
+
+## Overview
+
+Bifrost Enterprise uses **SCIM-backed identity provisioning** to connect your organization's identity provider to Bifrost. A single configuration gives you:
+
+- **Single sign-on (SSO)** via OAuth 2.0 / OIDC with JWKS-based JWT validation
+- **Automatic role assignment** using custom claims, app roles, or group-to-role mappings
+- **Team synchronization** from IdP groups into Bifrost teams
+- **Business unit mapping** from IdP attributes to Bifrost business units
+- **Bulk user provisioning** with filter-preview before import
+- **Silent token refresh** using server-stored refresh tokens
+
+Once configured, users sign in to Bifrost with their corporate credentials and inherit the right [role and permissions](./rbac) immediately — no manual account creation.
+
+
+
+
+
+---
+
+## Supported Identity Providers
+
+Pick your IdP to follow a step-by-step setup guide. All providers share the same Bifrost configuration surface — the only difference is how the OAuth client and role/group claims are created on the provider side.
+
+
+
+
+1. **Login** — Bifrost redirects unauthenticated users to the provider's authorization endpoint (Authorization Code flow).
+2. **Token exchange** — on callback, Bifrost exchanges the code for an access token and refresh token, stores them in an `HttpOnly` cookie / server session, and validates the JWT against the provider's JWKS.
+3. **Identity extraction** — configurable JWT claims (`userIdField`, `rolesField`, `teamIdsField`) are mapped to a Bifrost user, role, and teams. Provider-specific app roles or custom attributes override claim lookup.
+4. **Attribute mapping** — optional `attributeRoleMappings`, `attributeTeamMappings`, and `attributeBusinessUnitMappings` translate arbitrary claim values (e.g., a department string or Okta group name) into Bifrost roles, teams, or business units.
+5. **Bulk import** — admins can preview users matching a filter and bulk-import them via the dashboard, which calls the provider's user directory API.
+6. **Silent refresh** — when the access token expires, Bifrost uses the stored refresh token to mint a new one without requiring re-login.
+
+---
+
+## Capabilities
+
+| Capability | Description |
+| --- | --- |
+| **OAuth 2.0 / OIDC SSO** | Authorization Code + PKCE with configurable scopes (`openid profile email offline_access`). |
+| **JWKS validation** | JWTs are validated against the provider's published JWKS keys; configuration is cached and auto-refreshed. |
+| **Role mapping** | Map from a claim value (string or array) to Admin / Developer / Viewer or a custom role. Highest-privilege wins when multiple match. |
+| **Team mapping** | Map multiple claim values to Bifrost teams in a single pass (a user can belong to many teams). |
+| **Business unit mapping** | Same as team mapping but scoped to business units. |
+| **Provisioning preview** | Preview up to 50 users matching filters (groups, roles, departments) before importing. |
+| **Bulk import** | Import matched users into Bifrost with role + team + BU assignments applied. |
+| **Team sync** | Sync IdP groups as Bifrost teams with a single action. |
+| **Business unit sync** | Sync IdP organizational units as Bifrost business units. |
+| **Deprovisioning** | Re-running import reconciles removed users and updates role / team assignments. |
+| **API key pass-through** | Requests using Bifrost API keys (`bfst-*`) bypass SCIM middleware so inference traffic is not affected. |
+
+---
+
+## Configuration reference
+
+All providers share the same outer config shape in `config.json`:
+
+```json
+{
+ "scim_config": {
+ "enabled": true,
+ "provider": "okta | entra | zitadel | keycloak | google",
+ "config": {
+ "...": "provider-specific fields — see each IdP guide"
+ }
+ }
+}
+```
+
+Shared fields across providers:
+
+| Field | Required | Description |
+| --- | --- | --- |
+| `clientId` | Yes | OAuth client ID from the identity provider. |
+| `clientSecret` | Usually | Client secret. Required for confidential clients and (where applicable) token revocation. |
+| `audience` | Optional | JWT audience to validate against. Defaults vary per provider. |
+| `attributeRoleMappings` | Optional | Ordered list of `{ attribute, value, role }` rules evaluated top-to-bottom. |
+| `attributeTeamMappings` | Optional | List of `{ attribute, value, team }` rules (all matches apply). |
+| `attributeBusinessUnitMappings` | Optional | List of `{ attribute, value, businessUnit }` rules (all matches apply). |
+
+Provider-specific fields (domain, tenant ID, server URL, service-account credentials) are documented in each IdP's setup guide.
+
+
+
+
+4. Click **Verify** to test credentials end-to-end. Bifrost will reach the provider's JWKS / directory endpoint and report any failures.
+5. Configure **Attribute → Role / Team / Business Unit** mappings as needed.
+6. Toggle **Enabled** and click **Save Configuration**.
+
+
+
+
+Each mapping is an ordered rule:
+
+```json
+{
+ "attribute": "department",
+ "value": "Engineering",
+ "role": "developer"
+}
+```
+
+Rules are evaluated top-to-bottom:
+- **Role mappings** — first match wins. Set a fallback with `"attribute": "*"` at the end.
+- **Team mappings** and **business unit mappings** — all matching rules apply, so a user with `department=Platform` and `group=sre` can be placed on multiple teams.
+
+Claim values can be strings, arrays, or nested objects — Bifrost resolves dotted paths (e.g., `realm_access.roles`).
+
+---
+
+## Bulk user provisioning
+
+Once SCIM is enabled, import users in bulk from your IdP:
+
+1. Go to **Governance → User Provisioning → Import Users**.
+2. Select a filter — groups, roles, departments, or a custom query depending on provider support.
+3. Click **Preview** to see up to 50 matching users.
+4. Click **Import** to create them in Bifrost with role / team / BU assignments applied.
+
+
+
+
+
+Re-running an import reconciles existing users — role and team changes in the IdP are reflected on the next import.
+
+---
+
+## Troubleshooting
+
+| Symptom | Likely cause |
+| --- | --- |
+| Access denied: no application role or group mapping is assigned to this user. | Make sure you have assigned user to the Bifrost IdP application and they have a valid group/attribute mapping to role in Bifrost |
+| Redirect loop on login | Make sure you have restarted pods/Bifrost instance after changing SCIM configuration, or check for a redirect URI mismatch. Exact string match required — check trailing slashes and `http` vs `https`. |
+| `invalid audience` | `audience` field does not match the access token's `aud` claim. Use the same value your IdP issues. |
+| Empty roles / teams | Claim mapping is off. Verify the JWT at [jwt.io](https://jwt.io) and check `rolesField` / `teamIdsField`. |
+| Token refresh failing | `offline_access` scope missing or refresh token revoked. Re-enable the scope and re-authenticate. |
+| First user gets Admin | By design — if no matching role mapping applies, the first user is promoted to Admin so they can finish configuration. Subsequent users default to Viewer. |
+
+Provider-specific troubleshooting lives in each IdP's guide.
+
+---
+
+## Related
+
+- [Role-Based Access Control](./rbac) — permissions model and custom roles
+- [Advanced Governance](./advanced-governance) — budgets, limits, and compliance
+- [Audit Logs](./audit-logs) — track authentication events and role changes
diff --git a/docs/features/governance/budget-and-limits.mdx b/docs/features/governance/budget-and-limits.mdx
index 1a012b7904..b295565e72 100644
--- a/docs/features/governance/budget-and-limits.mdx
+++ b/docs/features/governance/budget-and-limits.mdx
@@ -345,59 +345,78 @@ Configure provider-level governance through Bifrost's configuration file for dec
"governance": {
"virtual_keys": [
{
+ "id": "vk-dev-001",
"name": "development-team-vk",
"description": "Development team with multi-provider setup",
+ "is_active": true,
+ "rate_limit_id": "rl-vk-dev",
"provider_configs": [
{
+ "id": 1,
"provider": "openai",
"weight": 0.6,
"allowed_models": ["gpt-4", "gpt-3.5-turbo"],
- "budget": {
- "max_limit": 1000.00,
- "reset_duration": "1M"
- },
- "rate_limit": {
- "token_max_limit": 2000000,
- "token_reset_duration": "1h",
- "request_max_limit": 2000,
- "request_reset_duration": "1h"
- }
+ "rate_limit_id": "rl-pc-openai"
},
{
+ "id": 2,
"provider": "anthropic",
"weight": 0.4,
"allowed_models": ["claude-3-opus", "claude-3-sonnet"],
- "budget": {
- "max_limit": 500.00,
- "reset_duration": "1M"
- },
- "rate_limit": {
- "token_max_limit": 1000000,
- "token_reset_duration": "1h",
- "request_max_limit": 1000,
- "request_reset_duration": "1h"
- }
+ "rate_limit_id": "rl-pc-anthropic"
}
- ],
- "budget": {
- "max_limit": 2000.00,
- "reset_duration": "1M",
- "calendar_aligned": true
- },
- "rate_limit": {
- "token_max_limit": 5000000,
- "token_reset_duration": "1h",
- "request_max_limit": 3000,
- "request_reset_duration": "1h"
- },
- "is_active": true
+ ]
+ }
+ ],
+ "budgets": [
+ {
+ "id": "budget-vk-dev",
+ "virtual_key_id": "vk-dev-001",
+ "max_limit": 2000.00,
+ "reset_duration": "1M",
+ "calendar_aligned": true
+ },
+ {
+ "id": "budget-pc-openai",
+ "provider_config_id": 1,
+ "max_limit": 1000.00,
+ "reset_duration": "1M"
+ },
+ {
+ "id": "budget-pc-anthropic",
+ "provider_config_id": 2,
+ "max_limit": 500.00,
+ "reset_duration": "1M"
+ }
+ ],
+ "rate_limits": [
+ {
+ "id": "rl-vk-dev",
+ "token_max_limit": 5000000,
+ "token_reset_duration": "1h",
+ "request_max_limit": 3000,
+ "request_reset_duration": "1h"
+ },
+ {
+ "id": "rl-pc-openai",
+ "token_max_limit": 2000000,
+ "token_reset_duration": "1h",
+ "request_max_limit": 2000,
+ "request_reset_duration": "1h"
+ },
+ {
+ "id": "rl-pc-anthropic",
+ "token_max_limit": 1000000,
+ "token_reset_duration": "1h",
+ "request_max_limit": 1000,
+ "request_reset_duration": "1h"
}
]
}
}
```
-Optional `calendar_aligned` on each `budget` matches the HTTP API and [calendar-aligned behavior](#calendar-aligned-budgets).
+Budgets and rate limits live as **separate top-level arrays** inside `governance`. Virtual keys and provider configs reference them by id (`rate_limit_id`) or are referenced back (`virtual_key_id` / `provider_config_id` on each `budgets[]` entry). Optional `calendar_aligned` on each `budget` matches the HTTP API and [calendar-aligned behavior](#calendar-aligned-budgets).
### Advanced Configuration Examples
@@ -407,34 +426,21 @@ Optional `calendar_aligned` on each `budget` matches the HTTP API and [calendar-
"governance": {
"virtual_keys": [
{
+ "id": "vk-cost-opt",
"name": "cost-optimized-vk",
"provider_configs": [
- {
- "provider": "openai-gpt-3.5",
- "weight": 0.8,
- "budget": {
- "max_limit": 50.00,
- "reset_duration": "1d"
- },
- "rate_limit": {
- "request_max_limit": 1000,
- "request_reset_duration": "1h"
- }
- },
- {
- "provider": "openai-gpt-4",
- "weight": 0.2,
- "budget": {
- "max_limit": 200.00,
- "reset_duration": "1d"
- },
- "rate_limit": {
- "request_max_limit": 100,
- "request_reset_duration": "1h"
- }
- }
+ {"id": 10, "provider": "openai-gpt-3.5", "weight": 0.8, "rate_limit_id": "rl-cheap"},
+ {"id": 11, "provider": "openai-gpt-4", "weight": 0.2, "rate_limit_id": "rl-premium"}
]
}
+ ],
+ "budgets": [
+ {"id": "b-cheap", "provider_config_id": 10, "max_limit": 50.00, "reset_duration": "1d"},
+ {"id": "b-premium", "provider_config_id": 11, "max_limit": 200.00, "reset_duration": "1d"}
+ ],
+ "rate_limits": [
+ {"id": "rl-cheap", "request_max_limit": 1000, "request_reset_duration": "1h"},
+ {"id": "rl-premium", "request_max_limit": 100, "request_reset_duration": "1h"}
]
}
}
@@ -446,52 +452,24 @@ Optional `calendar_aligned` on each `budget` matches the HTTP API and [calendar-
"governance": {
"virtual_keys": [
{
+ "id": "vk-prod-hv",
"name": "production-high-volume-vk",
"provider_configs": [
- {
- "provider": "openai",
- "weight": 0.5,
- "budget": {
- "max_limit": 5000.00,
- "reset_duration": "1M"
- },
- "rate_limit": {
- "token_max_limit": 10000000,
- "token_reset_duration": "1h",
- "request_max_limit": 10000,
- "request_reset_duration": "1h"
- }
- },
- {
- "provider": "anthropic",
- "weight": 0.3,
- "budget": {
- "max_limit": 3000.00,
- "reset_duration": "1M"
- },
- "rate_limit": {
- "token_max_limit": 6000000,
- "token_reset_duration": "1h",
- "request_max_limit": 6000,
- "request_reset_duration": "1h"
- }
- },
- {
- "provider": "azure-openai",
- "weight": 0.2,
- "budget": {
- "max_limit": 2000.00,
- "reset_duration": "1M"
- },
- "rate_limit": {
- "token_max_limit": 4000000,
- "token_reset_duration": "1h",
- "request_max_limit": 4000,
- "request_reset_duration": "1h"
- }
- }
+ {"id": 20, "provider": "openai", "weight": 0.5, "rate_limit_id": "rl-openai"},
+ {"id": 21, "provider": "anthropic", "weight": 0.3, "rate_limit_id": "rl-anthropic"},
+ {"id": 22, "provider": "azure-openai", "weight": 0.2, "rate_limit_id": "rl-azure"}
]
}
+ ],
+ "budgets": [
+ {"id": "b-openai", "provider_config_id": 20, "max_limit": 5000.00, "reset_duration": "1M"},
+ {"id": "b-anthropic", "provider_config_id": 21, "max_limit": 3000.00, "reset_duration": "1M"},
+ {"id": "b-azure", "provider_config_id": 22, "max_limit": 2000.00, "reset_duration": "1M"}
+ ],
+ "rate_limits": [
+ {"id": "rl-openai", "token_max_limit": 10000000, "token_reset_duration": "1h", "request_max_limit": 10000, "request_reset_duration": "1h"},
+ {"id": "rl-anthropic", "token_max_limit": 6000000, "token_reset_duration": "1h", "request_max_limit": 6000, "request_reset_duration": "1h"},
+ {"id": "rl-azure", "token_max_limit": 4000000, "token_reset_duration": "1h", "request_max_limit": 4000, "request_reset_duration": "1h"}
]
}
}
@@ -514,20 +492,23 @@ A virtual key configured with multiple providers and different budget allocation
```json
{
- "name": "marketing-team-vk",
- "budget": { "max_limit": 100, "reset_duration": "1M" },
- "provider_configs": [
- {
- "provider": "openai",
- "weight": 0.7,
- "budget": { "max_limit": 50, "reset_duration": "1M" }
- },
- {
- "provider": "anthropic",
- "weight": 0.3,
- "budget": { "max_limit": 30, "reset_duration": "1M" }
- }
- ]
+ "governance": {
+ "virtual_keys": [
+ {
+ "id": "vk-mkt",
+ "name": "marketing-team-vk",
+ "provider_configs": [
+ {"id": 30, "provider": "openai", "weight": 0.7},
+ {"id": 31, "provider": "anthropic", "weight": 0.3}
+ ]
+ }
+ ],
+ "budgets": [
+ {"id": "b-vk-mkt", "virtual_key_id": "vk-mkt", "max_limit": 100, "reset_duration": "1M"},
+ {"id": "b-openai", "provider_config_id": 30, "max_limit": 50, "reset_duration": "1M"},
+ {"id": "b-anth", "provider_config_id": 31, "max_limit": 30, "reset_duration": "1M"}
+ ]
+ }
}
```
@@ -542,27 +523,22 @@ Different rate limits based on provider capabilities:
```json
{
- "name": "high-volume-vk",
- "provider_configs": [
- {
- "provider": "openai",
- "rate_limit": {
- "request_max_limit": 1000,
- "request_reset_duration": "1h",
- "token_max_limit": 1000000,
- "token_reset_duration": "1h"
- }
- },
- {
- "provider": "anthropic",
- "rate_limit": {
- "request_max_limit": 500,
- "request_reset_duration": "1h",
- "token_max_limit": 500000,
- "token_reset_duration": "1h"
+ "governance": {
+ "virtual_keys": [
+ {
+ "id": "vk-hv",
+ "name": "high-volume-vk",
+ "provider_configs": [
+ {"id": 40, "provider": "openai", "rate_limit_id": "rl-openai"},
+ {"id": 41, "provider": "anthropic", "rate_limit_id": "rl-anthropic"}
+ ]
}
- }
- ]
+ ],
+ "rate_limits": [
+ {"id": "rl-openai", "request_max_limit": 1000, "request_reset_duration": "1h", "token_max_limit": 1000000, "token_reset_duration": "1h"},
+ {"id": "rl-anthropic", "request_max_limit": 500, "request_reset_duration": "1h", "token_max_limit": 500000, "token_reset_duration": "1h"}
+ ]
+ }
}
```
@@ -577,25 +553,25 @@ Provider configurations with budget-based failover:
```json
{
- "name": "cost-optimized-vk",
- "provider_configs": [
- {
- "provider": "openai-cheap",
- "weight": 1.0,
- "budget": { "max_limit": 10, "reset_duration": "1d" }
- },
- {
- "provider": "openai-premium",
- "weight": 0.0,
- "budget": { "max_limit": 50, "reset_duration": "1d" },
- "rate_limit": {
- "request_max_limit": 100,
- "request_reset_duration": "1h",
- "token_max_limit": 50000,
- "token_reset_duration": "1h"
+ "governance": {
+ "virtual_keys": [
+ {
+ "id": "vk-cost",
+ "name": "cost-optimized-vk",
+ "provider_configs": [
+ {"id": 50, "provider": "openai-cheap", "weight": 1.0},
+ {"id": 51, "provider": "openai-premium", "weight": 0.0, "rate_limit_id": "rl-premium"}
+ ]
}
- }
- ]
+ ],
+ "budgets": [
+ {"id": "b-cheap", "provider_config_id": 50, "max_limit": 10, "reset_duration": "1d"},
+ {"id": "b-premium", "provider_config_id": 51, "max_limit": 50, "reset_duration": "1d"}
+ ],
+ "rate_limits": [
+ {"id": "rl-premium", "request_max_limit": 100, "request_reset_duration": "1h", "token_max_limit": 50000, "token_reset_duration": "1h"}
+ ]
+ }
}
```
diff --git a/docs/features/governance/virtual-keys.mdx b/docs/features/governance/virtual-keys.mdx
index 8ae28e42ce..6c8cff9320 100644
--- a/docs/features/governance/virtual-keys.mdx
+++ b/docs/features/governance/virtual-keys.mdx
@@ -169,7 +169,8 @@ curl -X DELETE http://localhost:8080/api/governance/virtual-keys/{vk_id}
{
"provider": "openai",
"weight": 0.5,
- "allowed_models": ["gpt-4o-mini"]
+ "allowed_models": ["gpt-4o-mini"],
+ "key_ids": ["openai-primary"]
},
{
"provider": "anthropic",
@@ -178,11 +179,7 @@ curl -X DELETE http://localhost:8080/api/governance/virtual-keys/{vk_id}
}
],
"team_id": "team-eng-001",
- "budget_id": "budget-eng-vk",
- "rate_limit_id": "rate-limit-eng-vk",
- "keys": [
- {"key_id": "8c52039e-38c6-48b2-8016-0bd884b7befb"}
- ]
+ "rate_limit_id": "rate-limit-eng-vk"
},
{
"id": "vk-002",
@@ -202,16 +199,13 @@ curl -X DELETE http://localhost:8080/api/governance/virtual-keys/{vk_id}
"allowed_models": ["claude-3-opus-20240229"]
}
],
- "customer_id": "customer-acme-corp",
- "budget_id": "budget-exec-vk",
- "keys": [
- {"key_id": "8c52039e-38c6-48b2-8016-0bd884b7befb"}
- ]
+ "customer_id": "customer-acme-corp"
}
],
"budgets": [
{
"id": "budget-eng-vk",
+ "virtual_key_id": "vk-001",
"max_limit": 100.00,
"reset_duration": "1M",
"current_usage": 0.0,
@@ -219,6 +213,7 @@ curl -X DELETE http://localhost:8080/api/governance/virtual-keys/{vk_id}
},
{
"id": "budget-exec-vk",
+ "virtual_key_id": "vk-002",
"max_limit": 500.00,
"reset_duration": "1M",
"current_usage": 0.0,
diff --git a/docs/features/litellm-compat.mdx b/docs/features/litellm-compat.mdx
index 51cd26dcd9..490a37efa4 100644
--- a/docs/features/litellm-compat.mdx
+++ b/docs/features/litellm-compat.mdx
@@ -9,8 +9,10 @@ icon: "train"
The LiteLLM compatibility plugin provides two transformations:
1. **Text-to-Chat Conversion** - Automatically converts text completion requests to chat completion format for models that only support chat APIs
+2. **Chat-to-Responses Conversion** - Automatically converts chat completion requests to responses format for models that only support responses APIs
+3. **Drop Unsupported Params** - Automatically drops unsupported parameters if the model doesn't support them
-When either transformation is applied, responses include `extra_fields.litellm_compat: true`.
+When either transformation is applied, responses include `extra_fields.converted_request_type:
+
+
+- Copy API key to use it in the Azure content moderation config form
+- Copy project endpoint and use base URL as endpoint in the form. e.g. (`https://xxx-resource.services.ai.azure.com`)
## Severity Threshold Levels
-| Threshold | Numeric Value | Behavior |
-|-----------|---------------|----------|
-| `low` | 2 | Most strict - blocks severity 2 and above |
-| `medium` | 4 | Balanced - blocks severity 4 and above |
-| `high` | 6 | Least strict - blocks only severity 6 |
+| Threshold | Numeric Value | Behavior |
+| --------- | ------------- | ----------------------------------------- |
+| `low` | 2 | Most strict - blocks severity 2 and above |
+| `medium` | 4 | Balanced - blocks severity 4 and above |
+| `high` | 6 | Least strict - blocks only severity 6 |
## Detection Categories
@@ -47,23 +58,23 @@ Bifrost integrates with **Azure AI Content Safety** to provide multi-modal conte
- Self-harm