diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index 9c66edb8..5b5f4ed2 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -113,6 +113,14 @@ stringData: clientSecret: ``` +:::tip + +For step-by-step walkthroughs with Microsoft Entra ID or Okta, including app +registration, group/role configuration, and deployment YAML, see +[Connect ToolHive to an enterprise identity provider](../integrations/vmcp-idp-overview.mdx). + +::: + ### Kubernetes service account tokens Authenticate using Kubernetes service account tokens for in-cluster clients: @@ -615,6 +623,8 @@ automatically include your GitHub access token. multiple backends are presented to clients - [Set up audit logging](./audit-logging.mdx) to track authentication decisions and request activity +- [Connect ToolHive to an enterprise identity provider](../integrations/vmcp-idp-overview.mdx) + for IdP-specific setup guides covering Entra ID and Okta ## Related information diff --git a/docs/toolhive/guides-vmcp/intro.mdx b/docs/toolhive/guides-vmcp/intro.mdx index 3f1bb088..67c5425a 100644 --- a/docs/toolhive/guides-vmcp/intro.mdx +++ b/docs/toolhive/guides-vmcp/intro.mdx @@ -119,3 +119,4 @@ guide. - [Optimize tool discovery](./optimizer.mdx) - [Scaling and performance](./scaling-and-performance.mdx) - [Proxy remote MCP servers](../guides-k8s/remote-mcp-proxy.mdx) +- [Connect ToolHive to an enterprise identity provider](../integrations/vmcp-idp-overview.mdx) diff --git a/docs/toolhive/integrations/vmcp-entra-id.mdx b/docs/toolhive/integrations/vmcp-entra-id.mdx new file mode 100644 index 00000000..1358a3fa --- /dev/null +++ b/docs/toolhive/integrations/vmcp-entra-id.mdx @@ -0,0 +1,502 @@ +--- +title: Connect ToolHive to Microsoft Entra ID +description: + Use Entra ID App Roles to control access to MCP tools through a + VirtualMCPServer. +--- + +This guide covers the full setup for connecting a +[VirtualMCPServer](../guides-vmcp/authentication.mdx) to Microsoft Entra ID. App +Roles are the standard Entra mechanism for application-level access control, and +this guide uses them to place role values directly in the `roles` claim of the +access token. + +## What you'll need + +Collect these values as you complete the steps below: + +- [ ] Application (client) ID +- [ ] Client Secret +- [ ] Tenant ID +- [ ] Application ID URI (e.g. `api://`) +- [ ] Issuer URL: `https://login.microsoftonline.com/{tenant-id}/v2.0` +- [ ] Redirect URI: `https:///oauth/callback` + +## Configure Entra ID + +### Step 1: Register an application + +- **Entra ID > App registrations > New registration** +- Name: e.g. `toolhive-engineering` +- Supported account types: **Single tenant** +- Redirect URI: platform **Web**, URI + `https:///oauth/callback` +- Note the **Application (client) ID** and **Directory (tenant) ID** + +:::note + +The redirect URI must be an exact match - no wildcards, no trailing slashes. + +::: + +
+CLI equivalent + +```bash +TENANT_ID=$(az account show --query tenantId -o tsv) +DISPLAY_NAME="toolhive-engineering" +REDIRECT_URI="https:///oauth/callback" + +# --sign-in-audience: AzureADMyOrg = single-tenant +# Alternatives: AzureADMultipleOrgs, AzureADandPersonalMicrosoftAccount +APP=$(az ad app create \ + --display-name "$DISPLAY_NAME" \ + --sign-in-audience AzureADMyOrg \ + --web-redirect-uris "$REDIRECT_URI" \ + --query "{appId:appId, id:id}" \ + -o json) + +APP_ID=$(echo $APP | jq -r .appId) +OBJECT_ID=$(echo $APP | jq -r .id) + +echo "APP_ID=$APP_ID" +echo "OBJECT_ID=$OBJECT_ID" +echo "TENANT_ID=$TENANT_ID" +``` + +`APP_ID` = Application (client) ID. `OBJECT_ID` = Graph object ID (needed for +PATCH calls). These are different values. + +
+ +### Step 2: Expose an API + +By default, Entra issues access tokens for Microsoft Graph, not for your app. +App Roles only appear in tokens where your app is the audience. Exposing a +custom scope under `api:///` forces the embedded auth server to +request a token with your app as the audience. + +- **App registrations > your app > Expose an API** +- Click **Add** next to "Application ID URI" - accept the default + `api://` +- Click **Add a scope**: + - Scope name: `mcp.access` + - Who can consent: **Admins and users** + - Admin consent display name: "Access MCP Servers" + - Admin consent description: "Allow access to ToolHive MCP servers" + - User consent display name: "Access MCP Servers" + - User consent description: "Allow access to ToolHive MCP servers" + - State: **Enabled** + +
+CLI equivalent + +```bash +# 2a: Set Application ID URI +az ad app update \ + --id "$APP_ID" \ + --identifier-uris "api://$APP_ID" + +# 2b: Add the mcp.access scope +SCOPE_ID=$(uuidgen | tr '[:upper:]' '[:lower:]') + +az rest \ + --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body "{ + \"api\": { + \"oauth2PermissionScopes\": [ + { + \"id\": \"$SCOPE_ID\", + \"adminConsentDescription\": \"Allow access to ToolHive MCP servers\", + \"adminConsentDisplayName\": \"Access MCP Servers\", + \"userConsentDescription\": \"Allow access to ToolHive MCP servers\", + \"userConsentDisplayName\": \"Access MCP Servers\", + \"isEnabled\": true, + \"type\": \"User\", + \"value\": \"mcp.access\" + } + ] + } + }" +``` + +`oauth2PermissionScopes` is a full-replacement array. To add more scopes later, +include existing scopes with their original UUIDs. Set `type` to `"Admin"` to +require admin consent for this scope. + +
+ +### Step 3: Require assignment + +By default, any user in your tenant can authenticate to the app (they just won't +have any roles). To restrict access to explicitly assigned users only: + +- **Enterprise applications > your app > Properties** +- Set **Assignment required?** to **Yes** > **Save** + +:::note + +Without this setting, unassigned users can still obtain tokens. They will have +no `roles` claim and be denied by Cedar, but the experience (successful login +followed by 403) is avoidable. + +::: + +
+CLI equivalent + +```bash +# The portal creates the service principal automatically; +# the CLI requires it explicitly before you can set properties on it. +SP_OBJECT_ID=$(az ad sp create --id "$APP_ID" --query id -o tsv) + +az rest \ + --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body '{"appRoleAssignmentRequired": true}' # false = unassigned users can still authenticate (just get 403 from Cedar) +``` + +
+ +### Step 4: Create app roles + +- **App registrations > your app > App roles > Create app role** +- Create each role: + +| Display name | Value | Description | Allowed member types | +| -------------- | ---------------- | -------------------------------------- | -------------------- | +| MCP Developers | `mcp-developers` | Developer access to MCP tools | Users/Groups | +| MCP Platform | `mcp-platform` | Platform/SRE access to MCP tools | Users/Groups | +| MCP Admin | `mcp-admin` | Administrative access to all MCP tools | Users/Groups | + +:::note + +For machine-to-machine scenarios, set Allowed member types to **Applications**. + +::: + +
+CLI equivalent + +```bash +# All three roles must be set in a single call (full replacement). +ROLE_DEVELOPERS_ID=$(uuidgen | tr '[:upper:]' '[:lower:]') +ROLE_PLATFORM_ID=$(uuidgen | tr '[:upper:]' '[:lower:]') +ROLE_ADMIN_ID=$(uuidgen | tr '[:upper:]' '[:lower:]') + +az ad app update \ + --id "$APP_ID" \ + --app-roles "[ + { + \"id\": \"$ROLE_DEVELOPERS_ID\", + \"allowedMemberTypes\": [\"User\"], + \"displayName\": \"MCP Developers\", + \"description\": \"Developer access to MCP tools\", + \"value\": \"mcp-developers\", + \"isEnabled\": true + }, + { + \"id\": \"$ROLE_PLATFORM_ID\", + \"allowedMemberTypes\": [\"User\"], + \"displayName\": \"MCP Platform\", + \"description\": \"Platform/SRE access to MCP tools\", + \"value\": \"mcp-platform\", + \"isEnabled\": true + }, + { + \"id\": \"$ROLE_ADMIN_ID\", + \"allowedMemberTypes\": [\"User\"], + \"displayName\": \"MCP Admin\", + \"description\": \"Administrative access to all MCP tools\", + \"value\": \"mcp-admin\", + \"isEnabled\": true + } + ]" +``` + +`allowedMemberTypes: ["User"]` covers both users and groups in assignments. +`"Group"` is not a separate Graph API value; the portal's "Users/Groups" option +maps to `["User"]`. Use `["Application"]` for M2M/service principal assignments. + +
+ +### Step 5: Assign users and groups to roles + +- **Enterprise applications > your app > Users and groups > Add user/group** +- Select users or security groups +- **Select the role** (e.g. `mcp-developers`) - Entra defaults to "Default + Access" if you skip this step, which will not match your Cedar policies +- Click **Assign** +- Roles appear in the `roles` claim on next sign-in + +
+CLI equivalent + +```bash +# For a standard managed-tenant user: +USER_OID=$(az ad user show --id "user@yourdomain.com" --query id -o tsv) + +# For the signed-in account (tenant owner or personal Microsoft account): +# USER_OID=$(az ad signed-in-user show --query id -o tsv) + +# For a guest/external user, their UPN uses the #EXT# format: +# USER_OID=$(az ad user show \ +# --id "user_externaldomain.com#EXT#@yourtenant.onmicrosoft.com" \ +# --query id -o tsv) + +# Assign user to mcp-developers role +az rest \ + --method POST \ + --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_OBJECT_ID/appRoleAssignedTo" \ + --headers "Content-Type=application/json" \ + --body "{ + \"principalId\": \"$USER_OID\", + \"resourceId\": \"$SP_OBJECT_ID\", + \"appRoleId\": \"$ROLE_DEVELOPERS_ID\" + }" +``` + +`resourceId` must be the service principal object ID (`SP_OBJECT_ID`), not the +application client ID (`APP_ID`). Use `ROLE_PLATFORM_ID` or `ROLE_ADMIN_ID` in +`appRoleId` to assign other roles. + +
+ +### Step 6: Create a client secret + +- **App registrations > your app > Certificates & secrets > Client secrets > New + client secret** +- Set an expiry (the example below uses 1 year) +- Copy the **Value** immediately - it is shown only once and cannot be retrieved + later + +
+CLI equivalent + +```bash +SECRET=$(az ad app credential reset \ + --id "$APP_ID" \ + --display-name "toolhive-vmcp-secret" \ + --years 1 \ + --append \ + --query password \ + -o tsv) + +echo "CLIENT_SECRET=$SECRET" # Store immediately - shown only once +``` + +**Use `--append`** or this command deletes all existing credentials. The +`--query password` flag extracts the secret value (`password` is the Graph API +field name for the generated credential). + +
+ +### Optional: Configure additional token claims + +If you want Cedar policies to reference the user's name or email, or want +human-readable names in audit logs: + +- **App registrations > your app > Token configuration > Add optional claim > + select "ID" > check `email`, `given_name`, `family_name` > Add** +- When prompted to add Microsoft Graph permissions, click **Yes** + +This is not required for group-based access control. The `roles` claim is +already present in the access token from step 2. + +
+CLI equivalent + +```bash +az ad app update \ + --id "$APP_ID" \ + --optional-claims '{ + "idToken": [ + {"name": "email", "essential": false}, + {"name": "given_name", "essential": false}, + {"name": "family_name", "essential": false} + ], + "accessToken": [], + "saml2Token": [] + }' +``` + +
+ +| Item | Value | +| ---------------------------- | -------------------------------------------------------------------------- | +| Scopes requested from Entra | `api:///mcp.access` `openid` `profile` `email` `offline_access` | +| Access token claims produced | `roles: ["mcp-developers"]`, `sub`, `iss`, `aud: "api://"` | + +### Consistency checklist + +Role values must match exactly (case-sensitive) in three places: + +1. **App role Value field** (step 4): App registrations > your app > App roles > + Value (e.g. `mcp-developers`) +2. **Role assignment** (step 5): Enterprise applications > Users and groups > + the role selected during assignment +3. **Cedar policies** (see [Deploy to ToolHive](#deploy-to-toolhive)): + `THVGroup::"mcp-developers"` must match the app role Value exactly + +:::warning + +Changing the Value in place 1 does not update existing assignments in place 2. +If you rename a role, you must remove and recreate all assignments. + +::: + +:::warning + +This Entra application is exclusively for vMCP authentication. Do not reuse an +existing app registered for other services. Each VirtualMCPServer should have +its own app registration. + +::: + +## Deploy to ToolHive + +### Step 1: Create the IdP client secret + +```bash +kubectl create secret generic idp-client-secret \ + -n \ + --from-literal=client-secret= +``` + +:::note + +For production, configure persistent signing keys so tokens survive pod +restarts. See +[Configure the embedded auth server](../guides-vmcp/authentication.mdx#configure-the-embedded-auth-server). + +::: + +### Step 2: Create an MCPGroup and backend MCPServers + +If you haven't already set up your backends, follow the +[vMCP quickstart](../guides-vmcp/quickstart.mdx) steps 1 and 2 to create an +MCPGroup and deploy your MCPServers into it. The examples below assume a group +named `engineering-tools`. + +### Step 3: Create the VirtualMCPServer + +The VirtualMCPServer ties everything together: it references the backend group, +configures the embedded auth server with Entra ID as the upstream provider, sets +up incoming OIDC validation, and defines Cedar policies for group-based access +control. + +```yaml {24,30-35} +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPOIDCConfig +metadata: + name: engineering-tools-oidc +spec: + type: inline + inline: + issuer: 'https://' +--- +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: VirtualMCPServer +metadata: + name: engineering-tools +spec: + groupRef: + name: engineering-tools + + authServerConfig: + issuer: 'https://' + upstreamProviders: + - name: entra + type: oidc + oidcConfig: + issuerUrl: 'https://login.microsoftonline.com//v2.0' + clientId: '' + clientSecretRef: + name: idp-client-secret + key: client-secret + redirectUri: 'https:///oauth/callback' + scopes: + - 'api:///mcp.access' + - openid + - profile + - email + - offline_access + + incomingAuth: + type: oidc + oidcConfigRef: + name: engineering-tools-oidc + audience: 'https://' + resourceUrl: 'https://' + authzConfig: + type: inline + inline: + policies: + - | + permit( + principal in THVGroup::"mcp-developers", + action, + resource + ); + - | + forbid( + principal in THVGroup::"mcp-developers", + action == Action::"call_tool", + resource == Tool::"delete_namespace" + ); + - | + permit( + principal in THVGroup::"mcp-platform", + action, + resource + ); + - | + permit( + principal in THVGroup::"mcp-admin", + action, + resource + ); + + outgoingAuth: + source: discovered +``` + +The highlighted lines are Entra-specific — `issuerUrl` uses your tenant's v2.0 +endpoint, and `scopes` must include `api:///mcp.access` to get an +access token with your app as the audience (and thus the `roles` claim). + +- **MCPOIDCConfig**: validates tokens issued by the embedded auth server to + incoming MCP client requests. The `issuer` must match + `authServerConfig.issuer`. +- **groupRef**: the MCPGroup containing your backend MCPServers. +- **authServerConfig**: configures the embedded auth server with Entra as the + upstream IdP. +- **incomingAuth**: applies Cedar policies to each tool call. Role values in + `THVGroup::"mcp-developers"` must match the App Role values from step 4 + exactly. +- **outgoingAuth**: `source: discovered` means vMCP automatically forwards + credentials to backend MCPServers that require authentication. + +## Next steps + +- [Connect clients to MCP servers](../guides-k8s/connect-clients.mdx) +- [Authentication](../guides-vmcp/authentication.mdx) +- [Cedar policies](../concepts/cedar-policies.mdx) +- [Authentication and authorization](../concepts/auth-framework.mdx) + +## Troubleshooting + +**Role dropdown only shows "Default Access"**: App Roles created on the App +registration can take a moment to propagate to the Enterprise application view. +A hard browser refresh (Ctrl+Shift+R) usually resolves this. + +**Nested groups not supported**: App role assignments do not support nested +groups. Only directly assigned group members receive the role claim. + +**Roles not appearing**: The most common causes: (1) the user hasn't signed in +since the role was assigned — roles update on next token issuance; (2) the user +is not assigned to the app; (3) the correct role was not selected during +assignment (Entra defaults to "Default Access" if you skip the role selection). diff --git a/docs/toolhive/integrations/vmcp-idp-overview.mdx b/docs/toolhive/integrations/vmcp-idp-overview.mdx new file mode 100644 index 00000000..52586085 --- /dev/null +++ b/docs/toolhive/integrations/vmcp-idp-overview.mdx @@ -0,0 +1,37 @@ +--- +title: Connect ToolHive to an enterprise identity provider +description: + Connect ToolHive to Okta or Entra ID using the vMCP embedded auth server and + Cedar group-based access control. +--- + +Connecting your corporate identity provider to ToolHive lets your teams access +MCP tools using their existing credentials and group memberships. This guide +covers the setup using a Virtual MCP Server (vMCP) with its embedded OAuth 2.0 +Authorization Server, which brokers authentication between MCP clients and your +IdP and enforces access control through Cedar policies. + +## Prerequisites + +- Kubernetes cluster with the ToolHive operator installed +- `kubectl` access to your target namespace +- Admin access to your identity provider +- A publicly reachable URL for your VirtualMCPServer (the embedded auth server + needs a callback URL that your IdP can redirect to) + +## Choose your identity provider + +Follow the guide for your IdP to complete the full setup and deployment: + +- [Microsoft Entra ID](./vmcp-entra-id.mdx) - uses App Roles for group-based + access control, with the `roles` claim in access tokens +- [Okta](./vmcp-okta.mdx) - uses Okta Groups and a custom authorization server, + with the `groups` claim in access tokens + +For other OIDC-compliant providers, see +[vMCP authentication](../guides-vmcp/authentication.mdx). + +## Next steps + +- [Authentication and authorization](../concepts/auth-framework.mdx) +- [Cedar policies](../concepts/cedar-policies.mdx) diff --git a/docs/toolhive/integrations/vmcp-okta.mdx b/docs/toolhive/integrations/vmcp-okta.mdx new file mode 100644 index 00000000..de4d61a4 --- /dev/null +++ b/docs/toolhive/integrations/vmcp-okta.mdx @@ -0,0 +1,334 @@ +--- +title: Connect ToolHive to Okta +description: + Use Okta Groups to control access to MCP tools through a VirtualMCPServer. +--- + +Okta Groups are the standard mechanism for group-based access control in Okta. +This guide uses them to place group values in the `groups` claim of the access +token, which vMCP then uses for Cedar policy evaluation. + +## What you'll need + +Collect these values as you complete the steps below: + +- [ ] Client ID +- [ ] Client Secret +- [ ] Okta domain (e.g. `your-org.okta.com`) +- [ ] Authorization Server ID (or `default`) +- [ ] Issuer URL: `https:///oauth2/` +- [ ] Redirect URI: `https:///oauth/callback` + +## Configure Okta + +### Step 1: Create a custom authorization server + +You **must** use a custom authorization server (not the Org Authorization +Server). The Org AS cannot add groups claims to access tokens and its tokens are +not intended for external resource server validation. + +- **Security > API > Authorization Servers > Add Authorization Server** +- Name: e.g. `toolhive-engineering` +- Audience: `https://` (this becomes the `aud` claim) +- Note the **Issuer URI** + +:::note + +Creating a custom authorization server requires the Okta API Access Management +add-on in production orgs. A dedicated AS gives each application its own +audience, preventing tokens issued for one app from being accepted by another. +If you use the `default` AS instead, set its audience to your vMCP endpoint URL +— but note it is a shared resource. + +::: + +### Step 2: Create an OIDC application + +- Okta Admin > Applications > **Create App Integration** +- Sign-in method: **OIDC - OpenID Connect** +- Application type: **Web Application** (confidential client) +- Grant types: check **Authorization Code** and **Refresh Token** +- Sign-in redirect URI: `https:///oauth/callback` +- Assignments: select **Limit access to selected groups** (you will assign + groups in step 5) +- Note **Client ID** and **Client Secret** + +:::warning + +Enable **Refresh Token** on the application. The embedded auth server uses +refresh tokens to maintain long-lived sessions — without them, user sessions +expire when the upstream access token expires (typically 1 hour), requiring +users to re-authenticate. + +::: + +### Step 3: Create a `groups` scope + +The scope controls whether clients can request group data; the claim (step 4) +defines what gets embedded in the token. Both are required. + +Check the Scopes tab on your authorization server first — `groups` is a reserved +scope name in Okta and may already exist. If it does, skip to step 4. If not, +add it now: + +- **Security > API > Authorization Servers > [your server] > Scopes tab > Add + Scope** +- Name: `groups` +- Check **Include in public metadata** +- Save + +### Step 4: Add a groups claim to the access token + +- **Security > API > Authorization Servers > [your server] > Claims tab > Add + Claim** + +| Field | Value | +| --------------------- | ---------------------- | +| Name | `groups` | +| Include in token type | **Access Token** | +| Value type | **Groups** | +| Filter | **Starts with** `mcp-` | +| Include in | **Any scope** | +| Disable claim | **unchecked** | + +Use a prefix convention (e.g., `mcp-`) and name your Okta groups accordingly +(`mcp-developers`, `mcp-platform`). This keeps tokens small and avoids leaking +unrelated group memberships (admin groups, infrastructure groups, etc.) into +every access token. Note that the groups claim has a hard limit of 100 groups — +if the filter matches more than 100, the token request fails. + +:::warning[Gotcha] + +The filter dropdown defaults to "Starts with". If you switch to "Matches regex", +make sure to enter `.*` (not leave it blank). If you accidentally leave the +filter type on "Starts with" while entering `.*`, Okta tries to match group +names literally starting with `.*`, which matches nothing. + +::: + +### Step 5: Create groups and assign users + +- **Directory > Groups > Add Group** +- Create `mcp-developers` and `mcp-platform` groups (with descriptions) +- Click each group > **People** tab > **Assign people** > add users > click + **Done** +- **Applications > your app > Assignments > Assign > Assign to Groups** > search + for each group, click **Assign**, then click **Done** + +:::warning + +**This is the most commonly missed step.** Both conditions are required: the +groups must be assigned to the app, and users must be members of those groups. +Without group assignment, users get "not assigned to the application" errors. + +::: + +:::note + +Okta's app assignment is permissive — a user in any assigned group is granted +access. Fine-grained allow and deny logic (e.g., allowing one group but blocking +another from a specific tool) is handled by Cedar policies, not Okta assignment. +See [Cedar policies](../concepts/cedar-policies.mdx). + +::: + +### Step 6: Add an access policy + +On your custom authorization server: **Access Policies tab > Add Policy** + +- Name: `default-policy` +- Assign to: **All clients** +- Click **Create Policy**, then **Add Rule**: + - Name: `allow-authcode` + - Grant type: **Authorization Code** + - Scopes: `openid`, `profile`, `groups`, `offline_access` + - Access token lifetime: **1 hour** (or your preference) + - Refresh token lifetime: **24 hours** (must exceed expected user session + duration; the embedded auth server uses Okta refresh tokens to maintain + long-lived sessions) + +:::note + +The `groups` scope must already exist (step 3) for it to appear in the scope +picker. `offline_access` is a built-in OIDC scope; it does not need to be +created, but must be listed in the rule for refresh tokens to be issued. Both +this and the Refresh Token grant type enabled in step 2 are required — either +alone is not sufficient. + +::: + +| Item | Value | +| ---------------------------- | ----------------------------------------------------------------- | +| Scopes requested from Okta | `openid` `profile` `groups` `offline_access` | +| Access token claims produced | `groups: ["mcp-developers", "mcp-platform"]`, `sub`, `iss`, `aud` | + +### Consistency checklist + +Group names must match exactly in three places. A mismatch in any one causes +silent authorization failures (Cedar default-deny): + +1. **Okta group names** (step 5): e.g., `mcp-developers` +2. **Claim filter** (step 4): must match the group name prefix (e.g., + `Starts with mcp-`) +3. **Cedar policies** (see [Deploy to ToolHive](#deploy-to-toolhive)): + `THVGroup::"mcp-developers"` must match the Okta group name exactly, + including case + +:::warning + +This Okta application is exclusively for vMCP authentication. Do not reuse an +existing app registered for other cluster services (Grafana, Flux, Registry, +etc.). Each VirtualMCPServer should have its own app registration. See +[Connect ToolHive to an enterprise identity provider](./vmcp-idp-overview.mdx). + +::: + +## Deploy to ToolHive + +### Step 1: Create the IdP client secret + +```bash +kubectl create secret generic idp-client-secret \ + -n \ + --from-literal=client-secret= +``` + +:::note + +For production, configure persistent signing keys so tokens survive pod +restarts. See +[Configure the embedded auth server](../guides-vmcp/authentication.mdx#configure-the-embedded-auth-server). + +::: + +### Step 2: Create an MCPGroup and backend MCPServers + +If you haven't already set up your backends, follow the +[vMCP quickstart](../guides-vmcp/quickstart.mdx) steps 1 and 2 to create an +MCPGroup and deploy your MCPServers into it. The examples below assume a group +named `engineering-tools`. + +### Step 3: Create the VirtualMCPServer + +The VirtualMCPServer ties everything together: it references the backend group, +configures the embedded auth server with Okta as the upstream provider, sets up +incoming OIDC validation, and defines Cedar policies for group-based access +control. + +```yaml {24,30-34} +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPOIDCConfig +metadata: + name: engineering-tools-oidc +spec: + type: inline + inline: + issuer: 'https://' +--- +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: VirtualMCPServer +metadata: + name: engineering-tools +spec: + groupRef: + name: engineering-tools + + authServerConfig: + issuer: 'https://' + upstreamProviders: + - name: okta + type: oidc + oidcConfig: + issuerUrl: 'https:///oauth2/' + clientId: '' + clientSecretRef: + name: idp-client-secret + key: client-secret + redirectUri: 'https:///oauth/callback' + scopes: + - openid + - profile + - groups + - offline_access + + incomingAuth: + type: oidc + oidcConfigRef: + name: engineering-tools-oidc + audience: 'https://' + resourceUrl: 'https://' + authzConfig: + type: inline + inline: + policies: + - | + permit( + principal in THVGroup::"mcp-developers", + action, + resource + ); + - | + forbid( + principal in THVGroup::"mcp-developers", + action == Action::"call_tool", + resource == Tool::"delete_namespace" + ); + - | + permit( + principal in THVGroup::"mcp-platform", + action, + resource + ); + - | + permit( + principal in THVGroup::"mcp-admin", + action, + resource + ); + + outgoingAuth: + source: discovered +``` + +The highlighted lines are Okta-specific — `issuerUrl` uses your custom +authorization server's issuer, and `scopes` must include `groups` to get group +membership in the access token. + +- **MCPOIDCConfig**: validates tokens issued by the embedded auth server to + incoming MCP client requests. The `issuer` must match + `authServerConfig.issuer`. +- **groupRef**: the MCPGroup containing your backend MCPServers. +- **authServerConfig**: configures the embedded auth server with Okta as the + upstream IdP. +- **incomingAuth**: applies Cedar policies to each tool call. Group values in + `THVGroup::"mcp-developers"` must match the Okta group names from step 5 + exactly. +- **outgoingAuth**: `source: discovered` means vMCP automatically forwards + credentials to backend MCPServers that require authentication. + +## Next steps + +- [Connect clients to MCP servers](../guides-k8s/connect-clients.mdx) +- [Authentication](../guides-vmcp/authentication.mdx) +- [Cedar policies](../concepts/cedar-policies.mdx) +- [Authentication and authorization](../concepts/auth-framework.mdx) + +## Troubleshooting + +**`default` vs custom authorization server**: Custom authorization servers +support groups claims in access tokens. The Org Authorization Server can only +add groups claims to ID tokens, not access tokens. + +**Groups claim empty**: Common causes: (1) the claim filter prefix does not +match the Okta group names (e.g., filter says `mcp-` but groups are named +`developers` without the prefix); (2) the app is not assigned to the groups; (3) +when using "Matches regex", the dropdown was left on "Starts with" by accident +(see step 4 gotcha). + +**`invalid_grant` on token exchange**: Common causes: (1) the Refresh Token +grant type is not enabled on the application — edit the app and check Refresh +Token; (2) the refresh token has expired or been revoked — the user must +re-authenticate. + +**Scope not available in policy rule**: The `groups` scope must be created +(Scopes tab) before it can be referenced in a policy rule. diff --git a/package-lock.json b/package-lock.json index b6befd43..4c0b0bd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "stacklok-docs", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "3.10.0", - "@docusaurus/faster": "3.10.0", - "@docusaurus/plugin-vercel-analytics": "3.10.0", - "@docusaurus/preset-classic": "3.10.0", - "@docusaurus/theme-mermaid": "3.10.0", + "@docusaurus/core": "^3.10.0", + "@docusaurus/faster": "^3.10.0", + "@docusaurus/plugin-vercel-analytics": "^3.10.0", + "@docusaurus/preset-classic": "^3.10.0", + "@docusaurus/theme-mermaid": "^3.10.0", "@iconify-json/logos": "^1.2.11", "@mdx-js/react": "^3.1.1", "@signalwire/docusaurus-plugin-llms-txt": "1.2.2", @@ -26,9 +26,9 @@ }, "devDependencies": { "@apidevtools/json-schema-ref-parser": "^15.3.5", - "@docusaurus/module-type-aliases": "3.10.0", - "@docusaurus/tsconfig": "3.10.0", - "@docusaurus/types": "3.10.0", + "@docusaurus/module-type-aliases": "^3.10.0", + "@docusaurus/tsconfig": "^3.10.0", + "@docusaurus/types": "^3.10.0", "@eslint/compat": "^2.0.5", "@eslint/js": "^9.39.4", "eslint": "^9.39.4", @@ -3757,9 +3757,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3773,9 +3770,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3789,9 +3783,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3805,9 +3796,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 8669d8cd..899cfdc4 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,11 @@ "write-translations": "docusaurus write-translations" }, "dependencies": { - "@docusaurus/core": "3.10.0", - "@docusaurus/faster": "3.10.0", - "@docusaurus/plugin-vercel-analytics": "3.10.0", - "@docusaurus/preset-classic": "3.10.0", - "@docusaurus/theme-mermaid": "3.10.0", + "@docusaurus/core": "^3.10.0", + "@docusaurus/faster": "^3.10.0", + "@docusaurus/plugin-vercel-analytics": "^3.10.0", + "@docusaurus/preset-classic": "^3.10.0", + "@docusaurus/theme-mermaid": "^3.10.0", "@iconify-json/logos": "^1.2.11", "@mdx-js/react": "^3.1.1", "@signalwire/docusaurus-plugin-llms-txt": "1.2.2", @@ -38,9 +38,9 @@ }, "devDependencies": { "@apidevtools/json-schema-ref-parser": "^15.3.5", - "@docusaurus/module-type-aliases": "3.10.0", - "@docusaurus/tsconfig": "3.10.0", - "@docusaurus/types": "3.10.0", + "@docusaurus/module-type-aliases": "^3.10.0", + "@docusaurus/tsconfig": "^3.10.0", + "@docusaurus/types": "^3.10.0", "@eslint/compat": "^2.0.5", "@eslint/js": "^9.39.4", "eslint": "^9.39.4", diff --git a/sidebars.ts b/sidebars.ts index 3a5397df..36856ede 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -255,6 +255,18 @@ const sidebars: SidebarsConfig = { 'toolhive/integrations/aws-sts', 'toolhive/integrations/ingress-ngrok', 'toolhive/integrations/okta', + { + type: 'category', + label: 'Identity provider integration', + link: { + type: 'doc', + id: 'toolhive/integrations/vmcp-idp-overview', + }, + items: [ + 'toolhive/integrations/vmcp-entra-id', + 'toolhive/integrations/vmcp-okta', + ], + }, ], },