diff --git a/docs/toolhive/concepts/auth-framework.mdx b/docs/toolhive/concepts/auth-framework.mdx index f8fc6388..dc3a8abf 100644 --- a/docs/toolhive/concepts/auth-framework.mdx +++ b/docs/toolhive/concepts/auth-framework.mdx @@ -174,33 +174,19 @@ flowchart TD In the standard authentication flow described above, clients obtain tokens independently from an external identity provider and present them to ToolHive for validation. The embedded authorization server provides an alternative model -where ToolHive itself acts as an OAuth authorization server, retrieving tokens -from an upstream identity provider on behalf of clients. - -:::note - -The embedded authorization server is currently available only for Kubernetes -deployments using the ToolHive Operator. - -::: - -From the client's perspective, the embedded authorization server provides a -standard OAuth 2.0 experience: - -1. If the client is not yet registered, it registers via Dynamic Client - Registration (DCR), receiving a `client_id` and `client_secret`. -2. The client is directed to the ToolHive authorization endpoint. -3. ToolHive redirects the client to the upstream identity provider for - authentication (for example, signing in with GitHub or Atlassian). -4. ToolHive exchanges the authorization code for upstream tokens and issues its - own JWT to the client, signed with keys you configure. -5. The client includes this JWT as a `Bearer` token in the `Authorization` - header on subsequent requests. - -Behind the scenes, ToolHive stores the upstream tokens and uses them to -authenticate MCP server requests to external APIs. For the complete flow, -including token storage and forwarding, see -[Embedded authorization server](./backend-auth.mdx#embedded-authorization-server). +where ToolHive itself acts as an OAuth authorization server, obtaining tokens +from an upstream provider on behalf of clients—then storing those tokens and +issuing its own JWTs for clients to use on subsequent requests. + +This solves two problems at once: it eliminates the client registration burden +through Dynamic Client Registration (DCR), and it bridges the gap for external +APIs like GitHub or Atlassian where no federation relationship exists with your +identity provider. + +For the complete conceptual description—including the OAuth flow, token storage +and forwarding, session storage options, and differences between MCPServer and +VirtualMCPServer—see +[Embedded authorization server](./embedded-auth-server.mdx). For Kubernetes setup instructions, see [Set up embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication). diff --git a/docs/toolhive/concepts/backend-auth.mdx b/docs/toolhive/concepts/backend-auth.mdx index 04efb279..d6881fd0 100644 --- a/docs/toolhive/concepts/backend-auth.mdx +++ b/docs/toolhive/concepts/backend-auth.mdx @@ -159,33 +159,19 @@ When the MCP server needs to call an external API where no federation relationship exists—such as GitHub, Google Workspace, or Atlassian APIs—the embedded authorization server handles the full OAuth web flow against the external provider. The proxy redirects the user to authenticate directly with -the external service, obtains tokens on behalf of the user, and passes the -upstream token to the MCP server. - -```mermaid -sequenceDiagram - participant User - participant Proxy as ToolHive Proxy - participant ExtProvider as External Provider - - User->>Proxy: Connect - Proxy-->>User: Redirect to login - User->>ExtProvider: Authenticate - ExtProvider->>Proxy: Authorization code - Proxy->>ExtProvider: Exchange code for token - ExtProvider->>Proxy: Upstream tokens - Proxy->>User: Issue JWT -``` - -On subsequent MCP requests, ToolHive uses the JWT to retrieve the stored -upstream tokens and forward them to the MCP server. For details on this -mechanism, see [Token storage and forwarding](#token-storage-and-forwarding). +the external service, obtains tokens on behalf of the user, and automatically +forwards the upstream token to the MCP server on each subsequent request. The embedded authorization server runs in-process within the ToolHive proxy—no separate infrastructure is needed. It supports Dynamic Client Registration (DCR), so MCP clients can register automatically with ToolHive—no manual client configuration in ToolHive is required. +For a full explanation of how the OAuth flow works, token storage and +forwarding, automatic token refresh, session storage options, and differences +between MCPServer and VirtualMCPServer deployments, see +[Embedded authorization server](./embedded-auth-server.mdx). + :::note The embedded authorization server is currently available only for Kubernetes @@ -193,111 +179,6 @@ deployments using the ToolHive Operator. ::: -#### Key characteristics - -- **In-process execution:** The authorization server runs within the ToolHive - proxy—no separate infrastructure or sidecar containers needed. -- **Configurable signing keys:** JWTs are signed with keys you provide, - supporting key rotation for zero-downtime updates. -- **Flexible upstream providers:** Supports both OIDC providers (with automatic - endpoint discovery) and OAuth 2.0 providers (with explicit endpoint - configuration). -- **Configurable token lifespans:** Access tokens, refresh tokens, and - authorization codes have configurable durations with sensible defaults. -- **Dynamic Client Registration (DCR):** Supports OAuth 2.0 Dynamic Client - Registration (RFC 7591), allowing MCP clients to register automatically with - ToolHive's authorization server—no manual client registration in ToolHive is - required. -- **Direct upstream redirect:** The embedded authorization server redirects - clients directly to the upstream provider for authentication (for example, - GitHub or Atlassian). -- **Single upstream provider:** Currently supports one upstream identity - provider per configuration. - -:::info[Chained authentication not yet supported] - -The embedded authorization server redirects clients directly to the upstream -provider. This means the upstream provider must be the service whose API the MCP -server calls. Chained authentication—where a client authenticates with a -corporate IdP like Okta, which then federates to an external provider like -GitHub—is not yet supported. If your deployment requires this pattern, consider -using [token exchange](#same-idp-with-token-exchange) with a federated identity -provider instead. - -::: - -#### Token storage and forwarding - -The embedded authorization server stores upstream tokens (access tokens, refresh -tokens, and ID tokens from external providers) in session storage. When the -OAuth flow completes, the server generates a unique session ID and stores the -upstream tokens keyed by this ID. The JWT issued to the client contains a `tsid` -(Token Session ID) claim that references this session. - -When a client makes an MCP request with this JWT: - -1. The ToolHive proxy validates the JWT signature and extracts the `tsid` claim -2. It retrieves the upstream tokens from session storage using the `tsid` -3. The proxy replaces the `Authorization` header with the upstream access token -4. The request is forwarded to the MCP server with the external provider's token - -```mermaid -sequenceDiagram - participant Client - participant Proxy as ToolHive Proxy - participant Store as Session Storage - participant MCP as MCP Server - participant API as External API - - Note over Client,Store: Initial OAuth flow - Proxy->>Store: Store upstream tokens
keyed by session ID - Proxy-->>Client: Issue JWT with tsid claim - - Note over Client,API: Subsequent MCP requests - Client->>Proxy: MCP request with JWT - Proxy->>Proxy: Validate JWT signature - Proxy->>Store: Look up upstream token
using tsid from JWT - Store-->>Proxy: Return upstream access token - Proxy->>MCP: Forward request with
upstream access token - MCP->>API: Call external API - API-->>MCP: Response - MCP-->>Proxy: Response - Proxy-->>Client: Response -``` - -This mechanism allows MCP servers to call external APIs with the user's actual -credentials from the upstream provider, while the client only needs to manage a -single ToolHive-issued JWT. - -#### Automatic token refresh - -Upstream access tokens have their own expiration, independent of the ToolHive -JWT lifespan. When the stored upstream access token has expired, ToolHive -automatically refreshes it using the stored refresh token before forwarding the -request — your MCP session continues without re-authentication. - -If the refresh token is also expired or has been revoked by the upstream -provider, ToolHive returns a `401` response, prompting you to re-authenticate -through the OAuth flow. - -:::warning[Session storage limitations] - -By default, session storage is in-memory only. Upstream tokens are lost when -pods restart, requiring users to re-authenticate. For production deployments, -configure Redis Sentinel as the storage backend for persistent, highly available -session storage. See -[Configure session storage](../guides-k8s/auth-k8s.mdx#configure-session-storage) -for a quick setup, or the full -[Redis Sentinel session storage](../guides-k8s/redis-session-storage.mdx) -tutorial for an end-to-end walkthrough. - -::: - -For the client-facing OAuth flow, see -[Embedded authorization server](./auth-framework.mdx#embedded-authorization-server). -For Kubernetes setup instructions, see -[Set up embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication). - ## Token exchange in depth This section provides implementation details for the token exchange patterns @@ -467,8 +348,8 @@ setup guide. - For client authentication concepts, see [Authentication and authorization](./auth-framework.mdx) -- For the embedded authorization server, see - [Embedded authorization server](./auth-framework.mdx#embedded-authorization-server) +- For a deep dive into the embedded authorization server, see + [Embedded authorization server](./embedded-auth-server.mdx) - For configuring the embedded authorization server in Kubernetes, see [Set up embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication) - For configuring token exchange, see diff --git a/docs/toolhive/concepts/embedded-auth-server.mdx b/docs/toolhive/concepts/embedded-auth-server.mdx new file mode 100644 index 00000000..109bb07d --- /dev/null +++ b/docs/toolhive/concepts/embedded-auth-server.mdx @@ -0,0 +1,193 @@ +--- +title: Embedded authorization server +description: + How the ToolHive embedded authorization server works, including the OAuth + flow, token storage and forwarding, and when to use it. +--- + +The embedded authorization server is an OAuth 2.0 authorization server that runs +in-process within the ToolHive proxy. It solves a specific problem: how to +authenticate MCP server requests to external APIs—like GitHub, Google Workspace, +or Atlassian—where no federation relationship exists between your identity +provider and that external service. + +Without the embedded auth server, every MCP client would need to register its +own OAuth application with each external provider, manage redirect URIs, and +handle token acquisition separately. The embedded auth server centralizes this: +it handles the full OAuth web flow against the external provider on behalf of +clients, stores the resulting tokens, and issues its own JWTs that clients use +for subsequent requests. + +:::note + +The embedded authorization server is currently available only for Kubernetes +deployments using the ToolHive Operator. + +::: + +## When to use the embedded authorization server + +Use the embedded authorization server when your MCP servers call external APIs +on behalf of individual users and no federation relationship exists between your +identity provider and those services. + +| Scenario | Pattern to use | +| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| Backend only accepts API keys or static credentials | [Static credentials](./backend-auth.mdx#static-credentials-and-api-keys) | +| Backend trusts the same IdP as your clients | [Token exchange (same IdP)](./backend-auth.mdx#same-idp-with-token-exchange) | +| Backend trusts a federated IdP (for example, Google Cloud, AWS) | [Token exchange (federation)](./backend-auth.mdx#federated-idps-with-identity-mapping) | +| Backend is an external API with no federation (for example, GitHub) | **Embedded authorization server** (this page) | + +## How the OAuth flow works + +From the client's perspective, the embedded authorization server provides a +standard OAuth 2.0 experience: + +1. If the client is not yet registered, it registers via Dynamic Client + Registration (DCR, RFC 7591), receiving a `client_id` and `client_secret`. No + manual client registration in ToolHive is required. +2. The client is directed to the ToolHive authorization endpoint. +3. ToolHive redirects the client to the upstream identity provider for + authentication (for example, signing in with GitHub or Atlassian). +4. ToolHive exchanges the authorization code for upstream tokens and issues its + own JWT to the client, signed with keys you configure. +5. The client includes this JWT as a `Bearer` token in the `Authorization` + header on subsequent requests. + +```mermaid +sequenceDiagram + participant User + participant Proxy as ToolHive Proxy + participant ExtProvider as External Provider + + User->>Proxy: Connect + Proxy-->>User: Redirect to login + User->>ExtProvider: Authenticate + ExtProvider->>Proxy: Authorization code + Proxy->>ExtProvider: Exchange code for token + ExtProvider->>Proxy: Upstream tokens + Proxy->>User: Issue JWT +``` + +Behind the scenes, ToolHive stores the upstream tokens in session storage and +uses them to authenticate MCP server requests to external APIs. The client only +manages a single ToolHive-issued JWT. + +## Token storage and forwarding + +When the OAuth flow completes, the embedded auth server generates a unique +session ID and stores the upstream tokens (access token, refresh token, and ID +token from the external provider) keyed by this ID in session storage. The JWT +issued to the client contains a `tsid` (Token Session ID) claim that references +this session. + +When a client makes an MCP request with this JWT: + +1. The ToolHive proxy validates the JWT signature and extracts the `tsid` claim. +2. It retrieves the upstream tokens from session storage using the `tsid`. +3. The proxy replaces the `Authorization` header with the upstream access token. +4. The request is forwarded to the MCP server with the external provider's + token. + +```mermaid +sequenceDiagram + participant Client + participant Proxy as ToolHive Proxy + participant Store as Session Storage + participant MCP as MCP Server + participant API as External API + + Note over Client,Store: Initial OAuth flow + Proxy->>Store: Store upstream tokens
keyed by session ID + Proxy-->>Client: Issue JWT with tsid claim + + Note over Client,API: Subsequent MCP requests + Client->>Proxy: MCP request with JWT + Proxy->>Proxy: Validate JWT signature + Proxy->>Store: Look up upstream token
using tsid from JWT + Store-->>Proxy: Return upstream access token + Proxy->>MCP: Forward request with
upstream access token + MCP->>API: Call external API + API-->>MCP: Response + MCP-->>Proxy: Response + Proxy-->>Client: Response +``` + +MCP servers receive the upstream access token in the `Authorization: Bearer` +header—they don't need to implement custom authentication logic or manage +secrets. + +## Automatic token refresh + +Upstream access tokens expire independently of the ToolHive JWT lifespan. When +the stored upstream access token has expired, ToolHive automatically refreshes +it using the stored refresh token before forwarding the request. Your MCP +session continues without re-authentication. + +If the refresh token is also expired or has been revoked by the upstream +provider, ToolHive returns a `401` response, prompting re-authentication through +the OAuth flow. + +## Key characteristics + +- **In-process execution:** The authorization server runs within the ToolHive + proxy—no separate infrastructure or sidecar containers needed. +- **Dynamic Client Registration (DCR):** Supports OAuth 2.0 DCR (RFC 7591), + allowing MCP clients to register automatically. No manual client registration + in ToolHive is required. +- **Direct upstream redirect:** Redirects clients directly to the upstream + provider for authentication (for example, GitHub or Atlassian). +- **Configurable signing keys:** JWTs are signed with keys you provide, + supporting key rotation for zero-downtime updates. +- **Flexible upstream providers:** Supports OIDC providers (with automatic + endpoint discovery) and plain OAuth 2.0 providers (with explicit endpoint + configuration). +- **Configurable token lifespans:** Access tokens, refresh tokens, and + authorization codes have configurable durations with sensible defaults. + +## Session storage + +By default, session storage is in-memory. Upstream tokens are lost when pods +restart, requiring users to re-authenticate. + +For production deployments, configure Redis Sentinel as the storage backend for +persistent, highly available session storage. See +[Configure session storage](../guides-k8s/auth-k8s.mdx#configure-session-storage) +for a quick setup, or the full +[Redis Sentinel session storage](../guides-k8s/redis-session-storage.mdx) guide +for an end-to-end walkthrough. + +## MCPServer vs. VirtualMCPServer + +The embedded auth server is available on both `MCPServer` and `VirtualMCPServer` +resources, with some differences: + +| | MCPServer | VirtualMCPServer | +| ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------------ | +| Configuration location | Separate `MCPExternalAuthConfig` resource | Inline `authServerConfig` block on the resource | +| Upstream providers | Single upstream provider | Multiple upstream providers with sequential authorization chaining | +| Token forwarding | Automatic (single provider, single backend) | Explicit `upstreamInject` or `tokenExchange` config maps providers to backends | + +For single-backend deployments on MCPServer, the embedded auth server +automatically swaps the token for each request. For vMCP with multiple backends, +you configure which upstream provider's token goes to which backend using +[upstream token injection](../guides-vmcp/authentication.mdx#upstream-token-injection) +or +[token exchange with upstream tokens](../guides-vmcp/authentication.mdx#token-exchange-with-upstream-tokens). + +## Next steps + +- [Set up embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication) + — step-by-step setup for MCPServer resources in Kubernetes +- [vMCP embedded authorization server](../guides-vmcp/authentication.mdx#embedded-authorization-server) + — configuring multiple upstream providers on a VirtualMCPServer +- [Redis Sentinel session storage](../guides-k8s/redis-session-storage.mdx) — + production session storage configuration + +## Related information + +- [Authentication and authorization](./auth-framework.mdx) — client-to-MCP + authentication concepts and the overall framework +- [Backend authentication](./backend-auth.mdx) — all backend authentication + patterns, including when to choose the embedded auth server +- [Cedar policies](./cedar-policies.mdx) — authorization policy configuration diff --git a/docs/toolhive/concepts/vmcp.mdx b/docs/toolhive/concepts/vmcp.mdx index 93312dc5..6b36fb85 100644 --- a/docs/toolhive/concepts/vmcp.mdx +++ b/docs/toolhive/concepts/vmcp.mdx @@ -22,7 +22,7 @@ pre-configured tools with sensible defaults. ## Core value propositions -vMCP delivers four key benefits: +vMCP delivers five key benefits: 1. **Reduce complexity**: Many connections become one, dramatically simplifying configuration @@ -136,6 +136,16 @@ your identity provider and all backend access is revoked instantly. This approach provides single sign-on for users, centralized access control, and a complete audit trail. +vMCP can also run an embedded authorization server that handles the full OAuth +flow with multiple upstream identity providers (such as GitHub, Google, or +Okta). This enables per-user backend authentication: when a user logs in, the +auth server acquires tokens from each upstream provider and injects them into +requests to the appropriate backends. MCP clients register automatically through +Dynamic Client Registration (DCR), so no manual client configuration is needed. +See +[Authentication](../guides-vmcp/authentication.mdx#embedded-authorization-server) +for setup details. + ## When to use vMCP ### Good fit diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index f88a4f2e..6e8e989b 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -73,7 +73,7 @@ redirecting users to an upstream identity provider for authentication. This approach is ideal for MCP servers that accept `Authorization: Bearer` tokens. For conceptual background, see -[Embedded authorization server](../concepts/auth-framework.mdx#embedded-authorization-server). +[Embedded authorization server](../concepts/embedded-auth-server.mdx). **Prerequisites for embedded authorization server:** @@ -317,12 +317,12 @@ Kubernetes service account token, which is automatically mounted at ## Set up embedded authorization server authentication -The embedded authorization server runs an OAuth authorization server within the -ToolHive proxy. It handles the full OAuth flow by redirecting users to your -upstream identity provider for authentication, then issuing JWTs that the proxy -validates on subsequent requests. This provides MCP servers with -`Authorization: Bearer` tokens without requiring separate authorization server -infrastructure. +The [embedded authorization server](../concepts/embedded-auth-server.mdx) runs +an OAuth authorization server within the ToolHive proxy. It handles the full +OAuth flow by redirecting users to your upstream identity provider for +authentication, then issuing JWTs that the proxy validates on subsequent +requests. This provides MCP servers with `Authorization: Bearer` tokens without +requiring separate authorization server infrastructure. This setup uses the `MCPExternalAuthConfig` custom resource, following the same pattern as [token exchange configuration](./token-exchange-k8s.mdx). @@ -772,7 +772,9 @@ kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-k8s - For conceptual understanding, see [Authentication and authorization framework](../concepts/auth-framework.mdx) - For conceptual background on the embedded authorization server, see - [Embedded authorization server](../concepts/auth-framework.mdx#embedded-authorization-server) + [Embedded authorization server](../concepts/embedded-auth-server.mdx) +- For multi-upstream provider support with vMCP, see + [vMCP embedded authorization server](../guides-vmcp/authentication.mdx#embedded-authorization-server) - For a similar configuration pattern using token exchange, see [Configure token exchange](./token-exchange-k8s.mdx) - For detailed Cedar policy syntax, see diff --git a/docs/toolhive/guides-k8s/redis-session-storage.mdx b/docs/toolhive/guides-k8s/redis-session-storage.mdx index 35ac2909..6f826741 100644 --- a/docs/toolhive/guides-k8s/redis-session-storage.mdx +++ b/docs/toolhive/guides-k8s/redis-session-storage.mdx @@ -6,11 +6,11 @@ description: --- Deploy Redis Sentinel and configure it as the session storage backend for the -ToolHive embedded authorization server. By default, sessions are stored in -memory, which means upstream tokens are lost when pods restart and users must -re-authenticate. Redis Sentinel provides persistent storage with automatic -master discovery, ACL-based access control, and optional failover when replicas -are configured. +ToolHive [embedded authorization server](../concepts/embedded-auth-server.mdx). +By default, sessions are stored in memory, which means upstream tokens are lost +when pods restart and users must re-authenticate. Redis Sentinel provides +persistent storage with automatic master discovery, ACL-based access control, +and optional failover when replicas are configured. :::info[Prerequisites] @@ -163,7 +163,9 @@ spec: ``` The next section deploys a three-node Sentinel cluster that monitors the Redis -master and handles automatic failover: +master. With a single master and no replicas, Sentinel provides master discovery +for ToolHive but cannot perform automatic failover. To enable failover, add +Redis replicas to the StatefulSet and configure replication. ```yaml title="redis-sentinel.yaml — Sentinel cluster (append to same file)" # --- Sentinel configuration diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index e116d87d..1c0853d4 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -157,6 +157,448 @@ See [Configure token exchange for backend authentication](../guides-k8s/token-exchange-k8s.mdx) for details on using service account token exchange for backend authentication. +### Upstream token injection + +The `upstreamInject` outgoing auth strategy injects a user's upstream access +token into outgoing requests to a backend. Unlike other strategies that use +static credentials or token exchange, upstream token injection reads tokens that +the [embedded authorization server](#embedded-authorization-server) acquired +during the user's interactive login. + +Create an `MCPExternalAuthConfig` resource with the `upstreamInject` type. The +`providerName` must match an upstream provider configured on the embedded +authorization server: + +```yaml title="MCPExternalAuthConfig resource" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPExternalAuthConfig +metadata: + name: inject-github + namespace: toolhive-system +spec: + type: upstreamInject + upstreamInject: + providerName: github +``` + +Then reference it in the VirtualMCPServer's outgoing auth configuration: + +```yaml title="VirtualMCPServer resource" +spec: + outgoingAuth: + source: inline + backends: + backend-github: + type: external_auth_config_ref + externalAuthConfigRef: + name: inject-github +``` + +When a request reaches the `backend-github` MCPServer, vMCP replaces the +`Authorization` header with the upstream access token stored for the `github` +provider during the user's login flow. Backends not listed in the `backends` map +receive unauthenticated requests. + +:::note + +Upstream token injection requires an +[embedded authorization server](#embedded-authorization-server) configured on +the VirtualMCPServer. The `providerName` must match a provider `name` in the +auth server's `upstreamProviders` list. + +::: + +### Token exchange with upstream tokens + +You can combine the embedded authorization server with +[token exchange](../guides-k8s/token-exchange-k8s.mdx) by adding the +`subjectProviderName` field to a `tokenExchange` config. This tells the token +exchange middleware to use the stored upstream token from the named provider as +the subject token for the +[RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) exchange, instead of +the vMCP-issued JWT. + +This is useful when a backend needs a token exchanged at the same identity +provider that issued the upstream token. For example, if the embedded auth +server acquires an Okta access token during login, you can exchange that token +at a different Okta authorization server for a backend-scoped token: + +```yaml title="MCPExternalAuthConfig resource" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPExternalAuthConfig +metadata: + name: exchange-okta + namespace: toolhive-system +spec: + type: tokenExchange + tokenExchange: + tokenUrl: https:///oauth2//v1/token + clientId: + clientSecretRef: + name: okta-exchange-client-secret + key: client-secret + audience: backend + scopes: + - backend-api:read + # highlight-next-line + subjectProviderName: okta +``` + +Without `subjectProviderName`, token exchange uses the vMCP-issued JWT as the +subject token. With it, the exchange uses the raw upstream provider's access +token, which the exchange endpoint can validate directly. + +You can mix both strategies in the same vMCP deployment. For example, some +backends can use `upstreamInject` for direct token forwarding while others use +`tokenExchange` with `subjectProviderName` for exchanged tokens: + +```yaml title="VirtualMCPServer resource" +spec: + outgoingAuth: + source: inline + backends: + backend-github: + type: external_auth_config_ref + externalAuthConfigRef: + name: inject-github + backend-okta-app: + type: external_auth_config_ref + externalAuthConfigRef: + name: exchange-okta +``` + +## Embedded authorization server + +The embedded authorization server runs an OAuth authorization server within the +vMCP process. It redirects users to one or more upstream identity providers +(such as GitHub, Google, or Okta) for interactive authentication, stores the +upstream tokens, and issues its own JWTs that the vMCP validates on subsequent +requests. Combined with [upstream token injection](#upstream-token-injection) or +[token exchange with upstream tokens](#token-exchange-with-upstream-tokens), +this bridges both authentication boundaries: the auth server handles incoming +auth by issuing JWTs, while the outgoing strategies forward or exchange the +stored upstream tokens for backends. + +Use the embedded authorization server when your backend MCP servers call +external APIs on behalf of individual users and no federation relationship +exists between your identity provider and those services. It also provides OAuth +2.0 Dynamic Client Registration +([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)), so MCP clients can +register automatically without manual client configuration in ToolHive. + +:::info + +For conceptual background on the embedded authorization server—including the +OAuth flow, token storage and forwarding, and when to use it—see +[Embedded authorization server](../concepts/embedded-auth-server.mdx). For +configuring the embedded auth server on individual MCPServer resources (single +upstream provider), see +[Set up embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication). + +::: + +### How it works + +```mermaid +sequenceDiagram + participant Client as MCP Client + participant AS as Embedded Auth Server + participant GitHub as GitHub (upstream) + participant Google as Google (upstream) + participant vMCP as vMCP + participant Backend as Backend MCP Server + + Client->>AS: Connect (OAuth authorize) + AS-->>Client: Redirect to GitHub + Client->>GitHub: Authenticate + consent + GitHub-->>AS: Authorization code + AS->>GitHub: Exchange code for token + AS-->>Client: Redirect to Google + Client->>Google: Authenticate + consent + Google-->>AS: Authorization code + AS->>Google: Exchange code for token + AS-->>Client: Issue JWT (upstream tokens stored) + + Client->>vMCP: MCP request with JWT + vMCP->>vMCP: Validate JWT, load upstream tokens + vMCP->>Backend: Forward with GitHub access token + Backend-->>vMCP: Response + vMCP-->>Client: Response +``` + +When multiple upstream providers are configured, the auth server chains +authorization flows sequentially. The user is redirected to each provider in +order, and the auth server stores each provider's tokens before moving to the +next. After the final provider completes, the auth server issues a single JWT to +the client. + +### Differences from MCPServer embedded auth server + +The embedded auth server uses the same configuration structure whether on a +VirtualMCPServer or an MCPServer. The key differences for vMCP are: + +- **Inline configuration:** The auth server config lives directly on the + VirtualMCPServer resource under `authServerConfig`, rather than in a separate + `MCPExternalAuthConfig` resource. +- **Multiple upstream providers:** vMCP supports multiple upstream providers + with sequential authorization chaining. MCPServer is limited to a single + upstream provider. +- **Flexible outgoing strategies:** vMCP uses `upstreamInject` or + `tokenExchange` with `subjectProviderName` to route stored tokens to the + correct backends. MCPServer swaps the token automatically because it has a + single upstream provider. + +### Configure the embedded auth server + +Add an `authServerConfig` block to your VirtualMCPServer. The configuration +fields are the same as for the +[MCPServer embedded auth server](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication) +-- see that guide for generating keys and creating Secrets. + +```yaml title="VirtualMCPServer resource" +spec: + authServerConfig: + issuer: https://auth.example.com + signingKeySecretRefs: + - name: auth-signing-key + key: private-key + hmacSecretRefs: + - name: auth-hmac-key + key: hmac-key + tokenLifespans: + accessTokenLifespan: 1h + refreshTokenLifespan: 168h + authCodeLifespan: 10m + upstreamProviders: + - name: github + type: oauth2 + oauth2Config: { ... } + - name: google + type: oidc + oidcConfig: { ... } +``` + +:::warning[Signing keys and HMAC secrets] + +`signingKeySecretRefs` and `hmacSecretRefs` are technically optional. When +omitted, the auth server auto-generates ephemeral keys on startup. This is +convenient for development, but **tokens become invalid after pod restart** — +JWTs can no longer be verified (signing keys) and authorization codes and +refresh tokens can no longer be decoded (HMAC secrets), forcing all users to +re-authenticate. Always configure persistent keys for production. See +[Set up embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication) +for key generation steps. + +::: + +If the browser-facing authorization endpoint needs to be on a different host +than the issuer (for example, behind an ingress that rewrites paths), set +`authorizationEndpointBaseUrl` to override the `authorization_endpoint` in the +OAuth discovery document. All other endpoints remain derived from `issuer`: + +```yaml +spec: + authServerConfig: + issuer: https://auth.internal.example.com + authorizationEndpointBaseUrl: https://auth.example.com +``` + +Each upstream provider `name` must be a valid DNS label (lowercase alphanumeric +and hyphens, max 63 characters). This name is what +[upstream token injection](#upstream-token-injection) and +[token exchange](#token-exchange-with-upstream-tokens) configs reference to map +backends to providers. For details on configuring OIDC vs OAuth 2.0 upstream +providers, see +[Using an OAuth 2.0 upstream provider](../guides-k8s/auth-k8s.mdx#using-an-oauth-20-upstream-provider). +The [complete example](#complete-example) below shows full provider +configurations. + +:::tip[Non-standard token responses] + +Some OAuth 2.0 providers nest tokens under non-standard paths instead of +returning them at the top level (for example, GovSlack returns the access token +at `authed_user.access_token`). Add a `tokenResponseMapping` block to the +`oauth2Config` with dot-notation paths for `accessTokenPath`, `scopePath`, +`refreshTokenPath`, and `expiresInPath`. See the +[CRD reference](../reference/crd-spec.md) for field details. + +::: + +### Incoming auth with the embedded auth server + +When using the embedded auth server, configure `incomingAuth` to validate the +JWTs it issues. The `issuer` must match `authServerConfig.issuer`. Note that +`jwksAllowPrivateIP: true` is no longer needed when using the embedded auth +server because JWKS retrieval is done in-process. + +```yaml title="VirtualMCPServer resource" +spec: + incomingAuth: + type: oidc + resourceUrl: https://mcp.example.com/mcp + oidcConfig: + type: inline + inline: + issuer: https://auth.example.com + audience: https://mcp.example.com/mcp +``` + +The `resourceUrl` is the externally reachable URL of the MCP endpoint. MCP +clients use this for [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) +protected resource metadata discovery to find the authorization server. + +### Session storage + +By default, upstream tokens are stored in memory and lost on pod restart. For +production, configure Redis Sentinel by adding a `storage` block to +`authServerConfig`. The configuration is the same as for the MCPServer embedded +auth server. See +[Redis Sentinel session storage](../guides-k8s/redis-session-storage.mdx) for a +complete walkthrough. + +### Complete example + +This example deploys a vMCP with an embedded auth server that authenticates +users through GitHub and Google, then injects the GitHub access token into +requests to a GitHub MCP server backend. + +**Prerequisites:** Create Secrets for signing keys, HMAC keys, and upstream +provider credentials following the steps in +[Set up embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication). +You need: `auth-signing-key`, `auth-hmac-key`, `github-client-secret`, and +`google-client-secret`. + +**Step 1:** Create an MCPGroup and deploy the backend MCP server: + +```yaml title="backends.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPGroup +metadata: + name: my-backends + namespace: toolhive-system +--- +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPServer +metadata: + name: backend-github + namespace: toolhive-system +spec: + image: ghcr.io/github/github-mcp-server + transport: streamable-http + groupRef: my-backends +``` + +**Step 2:** Create the upstream token injection config: + +```yaml title="auth-configs.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPExternalAuthConfig +metadata: + name: inject-github + namespace: toolhive-system +spec: + type: upstreamInject + upstreamInject: + providerName: github +``` + +**Step 3:** Deploy the VirtualMCPServer: + +```yaml title="virtualmcpserver.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: VirtualMCPServer +metadata: + name: my-vmcp + namespace: toolhive-system +spec: + config: + groupRef: my-backends + # highlight-start + authServerConfig: + issuer: https://auth.example.com + signingKeySecretRefs: + - name: auth-signing-key + key: private-key + hmacSecretRefs: + - name: auth-hmac-key + key: hmac-key + tokenLifespans: + accessTokenLifespan: 1h + refreshTokenLifespan: 168h + authCodeLifespan: 10m + upstreamProviders: + - name: github + type: oauth2 + oauth2Config: + authorizationEndpoint: https://github.com/login/oauth/authorize + tokenEndpoint: https://github.com/login/oauth/access_token + clientId: + clientSecretRef: + name: github-client-secret + key: client-secret + scopes: + - repo + - read:user + userInfo: + endpointUrl: https://api.github.com/user + httpMethod: GET + additionalHeaders: + Accept: application/vnd.github+json + fieldMapping: + subjectFields: + - id + - login + nameFields: + - name + - login + emailFields: + - email + - name: google + type: oidc + oidcConfig: + issuerUrl: https://accounts.google.com + clientId: + clientSecretRef: + name: google-client-secret + key: client-secret + scopes: + - openid + - email + # highlight-end + incomingAuth: + type: oidc + resourceUrl: https://mcp.example.com/mcp + oidcConfig: + type: inline + inline: + issuer: https://auth.example.com + audience: https://mcp.example.com/mcp + outgoingAuth: + source: inline + backends: + backend-github: + type: external_auth_config_ref + externalAuthConfigRef: + name: inject-github +``` + +**Step 4:** Verify the deployment: + +```bash +# Check the VirtualMCPServer status +kubectl get virtualmcpserver -n toolhive-system my-vmcp + +# Verify OAuth discovery is available +curl https://auth.example.com/.well-known/oauth-authorization-server +``` + +Connect with an MCP client that supports the +[MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). +The client discovers the authorization server through protected resource +metadata, then redirects you through each upstream provider for authentication. +After completing the login flow, MCP tool calls to the GitHub backend +automatically include your GitHub access token. + ## Next steps - [Configure tool aggregation](./tool-aggregation.mdx) to manage how tools from @@ -166,7 +608,12 @@ for details on using service account token exchange for backend authentication. ## Related information -- [Authentication framework concepts](../concepts/auth-framework.mdx) +- [Authentication and authorization](../concepts/auth-framework.mdx) +- [Backend authentication](../concepts/backend-auth.mdx) +- [Embedded authorization server](../concepts/embedded-auth-server.mdx) for + conceptual background on the embedded auth server - [Cedar policies](../concepts/cedar-policies.mdx) for detailed policy syntax - [VirtualMCPServer configuration](./configuration.mdx) - [Token exchange in Kubernetes](../guides-k8s/token-exchange-k8s.mdx) +- [Embedded auth server for MCPServer](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication) +- [Redis Sentinel session storage](../guides-k8s/redis-session-storage.mdx) diff --git a/sidebars.ts b/sidebars.ts index ce844364..68dc995d 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -227,6 +227,7 @@ const sidebars: SidebarsConfig = { 'toolhive/concepts/auth-framework', 'toolhive/concepts/cedar-policies', 'toolhive/concepts/backend-auth', + 'toolhive/concepts/embedded-auth-server', 'toolhive/concepts/vmcp', 'toolhive/concepts/skills', ],