diff --git a/docs/toolhive/concepts/auth-framework.mdx b/docs/toolhive/concepts/auth-framework.mdx index 0df6e5e3..63ed4e1d 100644 --- a/docs/toolhive/concepts/auth-framework.mdx +++ b/docs/toolhive/concepts/auth-framework.mdx @@ -41,19 +41,40 @@ flexible, and auditable. You don't need to add custom authentication or authorization logic to every server—ToolHive handles it for you, consistently and securely. -## ToolHive vs. MCP specification +## Why ToolHive centralizes authentication The -[official Model Context Protocol (MCP) specification](https://modelcontextprotocol.io/docs/tutorials/security/authorization) -recommends OAuth 2.1-based authorization for HTTP transports, which requires -each MCP server to act as an OAuth resource server that validates access tokens -and enforces scope-based access control. ToolHive takes a different approach: it -centralizes authentication and authorization in its proxy layer, using -OAuth/OIDC for authentication and Cedar for fine-grained authorization. This -means you don't need to implement token validation or scope management in every -server—just configure ToolHive with your IdP and write clear Cedar policies. -This approach is more flexible, secure, and easier to manage for you and your -team. +[official MCP specification](https://modelcontextprotocol.io/docs/tutorials/security/authorization) +recommends OAuth 2.1-based authorization for HTTP transports, where each MCP +server acts as an OAuth resource server. In practice, this model creates +significant operational challenges: + +- **OAuth client registration burden:** OAuth 2.0 requires pre-registered + redirect URIs at each identity provider. Many providers—such as Google, + GitHub, and Atlassian—require manual registration of OAuth clients to obtain a + client ID and client secret. If each user client (for example, an IDE) were + its own OAuth client, the registration burden would be impractical at scale. +- **No federation with external services:** While token exchange (RFC 8693) and + federated identity providers work when the upstream service is in the same + trust domain as the MCP server or has an established trust relationship with + the identity provider, many MCP servers need to access external services like + GitHub, Google, or Atlassian APIs where no federation relationship exists. +- **Per-server implementation cost:** Each MCP server would need to implement + its own token validation and scope management, duplicating security-critical + logic across servers. + +ToolHive addresses these challenges by centralizing authentication and +authorization in its proxy layer. You configure ToolHive with your identity +provider and write Cedar policies for fine-grained authorization—individual MCP +servers don't need to implement token validation or scope management. + +With the [embedded authorization server](#embedded-authorization-server), +ToolHive can also manage interactive token acquisition. The proxy exposes +standard OAuth endpoints and handles the full OAuth web flow—clients don't need +to obtain or manage tokens externally. ToolHive delegates authentication to an +upstream identity provider and issues its own tokens, giving MCP clients a +spec-compliant OAuth experience while centralizing the complexity of client +registration and token management. ## Authentication framework @@ -146,6 +167,110 @@ flowchart TD Cedar_Authorizer -->|Deny| Denied[403 Forbidden] ``` +### Embedded authorization server + +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. + +::: + +This approach is designed for MCP servers that accept `Authorization: Bearer` +tokens and is particularly useful when you want ToolHive to handle the full +OAuth flow rather than requiring clients to obtain tokens independently. + +#### How the embedded authorization server works + +The embedded authorization server runs in-process within the ToolHive proxy. +When a client connects, the following flow occurs: + +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. The proxy redirects the client to the upstream identity provider for + authentication. +4. The user authenticates with the upstream identity provider (for example, + signing in with Google or GitHub). +5. The upstream identity provider redirects back to the proxy with an + authorization code. +6. The embedded authorization server exchanges the authorization code for tokens + with the upstream identity provider. +7. The embedded authorization server issues its own JWT to the client, signed + with keys you configure. +8. The client includes this JWT as a `Bearer` token in the `Authorization` + header on subsequent requests. +9. The proxy validates the JWT, retrieves the upstream token, and forwards + requests to the MCP server. + +```mermaid +sequenceDiagram + participant Client + participant Proxy as ToolHive Proxy + participant IdP as Upstream IdP + participant MCP as MCP Server + + Client->>Proxy: POST /oauth/register (DCR) + Proxy-->>Client: client_id + client_secret + Client->>Proxy: Connect to MCP server + Proxy-->>Client: Redirect to /oauth/authorize + Client->>Proxy: GET /oauth/authorize + Proxy-->>Client: Redirect to upstream IdP + Client->>IdP: Authenticate + IdP-->>Client: Redirect with authorization code + Client->>Proxy: GET /oauth/callback?code=... + Proxy->>IdP: Exchange code for tokens + IdP-->>Proxy: Upstream tokens + Proxy-->>Client: Issue ToolHive JWT + Client->>Proxy: MCP request with Bearer token + Proxy->>Proxy: Validate JWT + Proxy->>MCP: Forward request + MCP-->>Proxy: Response + Proxy-->>Client: Response +``` + +#### 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 + without manual configuration at the identity provider. +- **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](./backend-auth.mdx#same-idp-with-token-exchange) with a +federated identity provider instead. + +::: + +For guidance on choosing the right backend authentication pattern for your MCP +servers, see +[Choosing the right backend authentication model](./backend-auth.mdx#choosing-the-right-backend-authentication-model). + ### Identity providers ToolHive can integrate with any provider that supports OAuth 2.1 or OIDC, @@ -158,8 +283,9 @@ including: - Auth0 - Kubernetes (service account tokens) -This flexibility lets you use your existing identity infrastructure for both -users and services, reducing operational overhead and improving security. +These same providers work with both external token validation and the embedded +authorization server. For the embedded authorization server, the upstream +provider must support the OAuth 2.0 authorization code flow. ### Token validation methods @@ -280,6 +406,8 @@ standardized across clients. ## Related information +- For configuring the embedded authorization server in Kubernetes, see + [Embedded authorization server authentication](../guides-k8s/auth-k8s.mdx#set-up-embedded-authorization-server-authentication) - For backend authentication concepts, see [Backend authentication](./backend-auth.mdx) - For detailed policy writing guidance, see diff --git a/docs/toolhive/concepts/backend-auth.mdx b/docs/toolhive/concepts/backend-auth.mdx index 3274e09c..359f31c3 100644 --- a/docs/toolhive/concepts/backend-auth.mdx +++ b/docs/toolhive/concepts/backend-auth.mdx @@ -283,8 +283,33 @@ ToolHive's token exchange approach provides several key advantages: - **Consistent:** The same pattern works across different backend services and identity providers +## Choosing the right backend authentication model + +How you configure backend authentication depends on what the MCP server needs to +call and how that backend service accepts credentials: + +- **Static credentials or API keys:** If the MCP server only supports static + credentials or API keys, configure them in ToolHive directly—either as + environment variables, secrets, or injected headers. No token exchange or + embedded authorization server is needed. +- **Token exchange:** If the MCP server makes authenticated API calls to a + backend service in the same trust domain as your corporate identity provider + (for example, an internal API that accepts tokens from your Okta or Entra ID + tenant), or federation exists between the two, token exchange is a good fit. + ToolHive exchanges the client's token for a backend-scoped token using RFC + 8693, preserving the user's identity across services. +- **Embedded authorization server:** If the MCP server needs to call an external + API where no federation relationship exists—such as GitHub, Google, or + Atlassian APIs—the + [embedded authorization server](./auth-framework.mdx#embedded-authorization-server) + is a good fit. It runs the full OAuth web flow against the external provider, + obtaining tokens that the MCP server can use to access those APIs on behalf of + the user. + ## Related information - 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 policy configuration, see [Cedar policies](./cedar-policies.mdx) diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index 9535a141..18f40bbb 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -33,8 +33,7 @@ You'll need: ## Choose your authentication approach -There are three main ways to authenticate with MCP servers running in -Kubernetes: +There are four main ways to authenticate with MCP servers running in Kubernetes: ### Approach 1: External identity provider authentication @@ -67,6 +66,22 @@ account tokens for authentication. - Client applications running in Kubernetes pods - Understanding of Kubernetes service accounts and RBAC +### Approach 4: Embedded authorization server authentication + +Use this when you want ToolHive to handle the full OAuth flow, including +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). + +**Prerequisites for embedded authorization server:** + +- An upstream identity provider that supports the OAuth 2.0 authorization code + flow (such as Okta, Microsoft Entra ID, Auth0, or any OIDC-compliant provider) +- A registered OAuth application/client with your upstream provider +- Client ID and client secret from your upstream provider + ## Set up external identity provider authentication **Step 1: Create an MCPServer with external OIDC** @@ -304,10 +319,284 @@ Your client application can now authenticate to the MCP server using its Kubernetes service account token, which is automatically mounted at `/var/run/secrets/kubernetes.io/serviceaccount/token`. +## 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. + +This setup uses the `MCPExternalAuthConfig` custom resource, following the same +pattern as [token exchange configuration](./token-exchange-k8s.mdx). + +**Step 1: Create a Secret for the upstream provider client credentials** + +Store the OAuth client secret for your upstream identity provider: + +```yaml title="upstream-idp-secret.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: upstream-idp-secret + namespace: toolhive-system +type: Opaque +stringData: + client-secret: '' +``` + +```bash +kubectl apply -f upstream-idp-secret.yaml +``` + +**Step 2: Create a Secret for JWT signing keys** + +The embedded authorization server signs JWTs with a private key you provide. +Generate a PEM-encoded private key (RSA or EC), for example: + +```bash +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out signing-key.pem +``` + +Then create a Secret containing the key: + +```yaml title="auth-server-signing-key.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: auth-server-signing-key + namespace: toolhive-system +type: Opaque +stringData: + signing-key: | + -----BEGIN PRIVATE KEY----- + + -----END PRIVATE KEY----- +``` + +```bash +kubectl apply -f auth-server-signing-key.yaml +``` + +:::tip[Key rotation] + +For key rotation, you can reference multiple signing key Secrets in the +`signingKeySecretRefs` list. The first key is used for signing new tokens. +Additional keys are used for verification only, so tokens signed before rotation +remain valid. + +::: + +**Step 3: Create a Secret for HMAC keys** + +The embedded authorization server uses a symmetric HMAC key to sign +authorization codes and refresh tokens. The key must be at least 32 bytes and +cryptographically random, for example: + +```bash +openssl rand -base64 32 +``` + +```yaml title="auth-server-hmac-secret.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: auth-server-hmac-secret + namespace: toolhive-system +type: Opaque +stringData: + hmac-key: '' +``` + +```bash +kubectl apply -f auth-server-hmac-secret.yaml +``` + +:::warning[Ephemeral keys for development only] + +If you omit the `signingKeySecretRefs` and `hmacSecretRefs` fields, ToolHive +generates ephemeral keys that are lost on pod restart. All previously issued +tokens become invalid after a restart. Only omit these Secrets for development +and testing. + +::: + +**Step 4: Create the MCPExternalAuthConfig resource** + +Create an `MCPExternalAuthConfig` resource with the `embeddedAuthServer` type. +This example configures an OIDC upstream provider (the most common case): + +```yaml title="embedded-auth-config.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPExternalAuthConfig +metadata: + name: embedded-auth-server + namespace: toolhive-system +spec: + type: embeddedAuthServer + embeddedAuthServer: + issuer: 'https://mcp.example.com' + signingKeySecretRefs: + - name: auth-server-signing-key + key: signing-key + hmacSecretRefs: + - name: auth-server-hmac-secret + key: hmac-key + tokenLifespans: + accessTokenLifespan: '1h' + refreshTokenLifespan: '168h' + authCodeLifespan: '10m' + upstreamProviders: + - name: google + type: oidc + oidcConfig: + issuerUrl: 'https://accounts.google.com' + clientId: '' + clientSecretRef: + name: upstream-idp-secret + key: client-secret + scopes: + - openid + - profile + - email +``` + +```bash +kubectl apply -f embedded-auth-config.yaml +``` + +**Configuration reference:** + +| Field | Description | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `issuer` | HTTPS URL identifying this authorization server. Appears in the `iss` claim of issued JWTs. | +| `signingKeySecretRefs` | References to Secrets containing JWT signing keys. First key is active; additional keys support rotation. | +| `hmacSecretRefs` | References to Secrets with symmetric keys for signing authorization codes and refresh tokens. | +| `tokenLifespans` | Configurable durations for access tokens (default: 1h), refresh tokens (default: 168h), and auth codes (default: 10m). | +| `upstreamProviders` | Configuration for the upstream identity provider. Currently supports one provider. | + +**Step 5: Create the MCPServer resource** + +The MCPServer needs two configuration references: `externalAuthConfigRef` +enables the embedded authorization server, and `oidcConfig` validates the JWTs +that the embedded authorization server issues. Unlike approaches 1–3 where +`oidcConfig` points to an external identity provider, here it points to the +embedded authorization server itself—the `oidcConfig` issuer must match the +`issuer` in your `MCPExternalAuthConfig`. + +```yaml title="mcp-server-embedded-auth.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPServer +metadata: + name: weather-server-embedded + namespace: toolhive-system +spec: + image: ghcr.io/stackloklabs/weather-mcp/server + transport: streamable-http + proxyPort: 8080 + permissionProfile: + type: builtin + name: network + # Reference the embedded authorization server configuration + externalAuthConfigRef: + name: embedded-auth-server + # Validate JWTs issued by the embedded authorization server + oidcConfig: + type: inline + resourceUrl: 'https://mcp.example.com/mcp' + inline: + # This must match the embedded authorization server issuer url + issuer: 'https://mcp.example.com' + resources: + limits: + cpu: '100m' + memory: '128Mi' + requests: + cpu: '50m' + memory: '64Mi' +``` + +```bash +kubectl apply -f mcp-server-embedded-auth.yaml +``` + +:::note + +The `oidcConfig` issuer must match the `issuer` in your `MCPExternalAuthConfig`. +The embedded authorization server exposes a JWKS endpoint that the proxy uses to +validate the JWTs it issues. The proxy also exposes OAuth discovery endpoints +(`/.well-known/oauth-authorization-server`) so MCP clients can discover the +authorization endpoints automatically. + +::: + +### Using an OAuth 2.0 upstream provider + +If your upstream identity provider does not support OIDC discovery, you can +configure it as an OAuth 2.0 provider with explicit endpoints. This is useful +for providers like GitHub that use OAuth 2.0 but don't implement the full OIDC +specification. + +```yaml title="embedded-auth-oauth2-config.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPExternalAuthConfig +metadata: + name: embedded-auth-oauth2 + namespace: toolhive-system +spec: + type: embeddedAuthServer + embeddedAuthServer: + issuer: 'https://mcp.example.com' + signingKeySecretRefs: + - name: auth-server-signing-key + key: signing-key + hmacSecretRefs: + - name: auth-server-hmac-secret + key: hmac-key + upstreamProviders: + - name: github + type: oauth2 + oauth2Config: + authorizationEndpoint: 'https://github.com/login/oauth/authorize' + tokenEndpoint: 'https://github.com/login/oauth/access_token' + userInfo: + endpointUrl: 'https://api.github.com/user' + httpMethod: GET + additionalHeaders: + Accept: 'application/vnd.github+json' + fieldMapping: + subjectFields: + - id + - login + nameFields: + - name + - login + emailFields: + - email + clientId: '' + clientSecretRef: + name: upstream-idp-secret + key: client-secret + scopes: + - user:email + - read:user +``` + +:::note + +OAuth 2.0 providers require explicit endpoint configuration and a `userInfo` +section, unlike OIDC providers which auto-discover these from the issuer URL. +The `fieldMapping` section maps provider-specific response fields to standard +user identity fields. For example, GitHub returns `login` instead of the +standard `name` field. + +::: + ## Set up authorization -Both authentication approaches can use the same authorization configuration -using Cedar policies. +All authentication approaches can use the same authorization configuration using +Cedar policies. **Step 1: Create authorization configuration** @@ -406,6 +695,32 @@ kubectl apply -f mcp-server-with-authz.yaml kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-k8s ``` +### Test embedded authorization server authentication + +1. Deploy the `MCPExternalAuthConfig` and `MCPServer` resources +2. Check that the MCPServer is running: + + ```bash + kubectl get mcpserver -n toolhive-system weather-server-embedded + ``` + +3. If the server is exposed outside the cluster, verify the OAuth discovery + endpoint is available: + + ```bash + curl https:///.well-known/oauth-authorization-server + ``` + +4. Connect with an MCP client that supports the MCP OAuth specification. The + client should be redirected to your upstream identity provider for + authentication. +5. Check the proxy logs for successful authentication: + + ```bash + kubectl logs -n toolhive-system \ + -l app.kubernetes.io/name=weather-server-embedded + ``` + ### Test authorization 1. Make requests that should be permitted by your policies @@ -416,6 +731,10 @@ 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) +- For a similar configuration pattern using token exchange, see + [Configure token exchange](./token-exchange-k8s.mdx) - For detailed Cedar policy syntax, see [Cedar policies](../concepts/cedar-policies.mdx) and the [Cedar documentation](https://docs.cedarpolicy.com/) @@ -469,3 +788,49 @@ kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-k8s - Verify the operator is running: `kubectl get pods -n toolhive-system` + +
+Embedded authorization server issues + +**OAuth flow not initiating:** + +- Verify the `MCPExternalAuthConfig` resource exists in the same namespace: + `kubectl get mcpexternalauthconfig -n toolhive-system` +- Check that the `externalAuthConfigRef.name` in your `MCPServer` matches the + `MCPExternalAuthConfig` resource name +- Verify the upstream provider's client ID and redirect URI are correctly + configured in the `MCPExternalAuthConfig` + +**Token validation failures after restart:** + +- Ensure you have configured `signingKeySecretRefs` and `hmacSecretRefs` with + persistent keys +- Without these, ephemeral keys are generated on startup, invalidating all + previously issued tokens + +**Upstream IdP redirect errors:** + +- Verify the redirect URI configured in your upstream provider matches the + ToolHive proxy's callback URL (typically + `https:///oauth/callback`) +- Check that the upstream provider's issuer URL is accessible from within the + cluster +- For OIDC providers, ensure the `/.well-known/openid-configuration` endpoint is + reachable from the proxy pod + +**JWT signing key issues:** + +- Verify signing key Secrets exist: + `kubectl get secret -n toolhive-system auth-server-signing-key` +- Ensure the key format is correct (PEM-encoded RSA or EC private key) +- Check proxy logs for key loading errors: + `kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-embedded` + +**OIDC configuration mismatch:** + +- Ensure the `oidcConfig.inline.issuer` on your `MCPServer` matches the `issuer` + in your `MCPExternalAuthConfig` +- Verify the `resourceUrl` in `oidcConfig` matches the external URL of the MCP + server + +