Skip to content

Latest commit

 

History

History
600 lines (471 loc) · 20.5 KB

File metadata and controls

600 lines (471 loc) · 20.5 KB
title Authentication configuration
description How to configure authentication for the ToolHive Registry Server with anonymous and OAuth modes

The Registry server provides secure-by-default authentication, defaulting to OAuth mode to protect your registry. You can configure authentication to fit different deployment scenarios, from development environments to production deployments with enterprise identity providers.

:::tip[Looking for authorization?]

This page covers authentication (verifying caller identity). For authorization (controlling what callers can do), including role-based access control and claims-based scoping, see Authorization.

:::

Authentication modes

The server supports two authentication modes configured via the required auth section in your configuration file:

  • OAuth (default): Secure authentication using JWT tokens from identity providers
  • Anonymous: No authentication (development/testing only)

:::info[Secure by default]

The server defaults to OAuth mode when no explicit auth configuration is provided. This secure-by-default posture ensures your registry is protected unless you explicitly choose anonymous mode for development scenarios.

:::

OAuth authentication

OAuth mode (the default) validates access tokens from identity providers. The server supports two token formats:

  • JWT tokens: Validated locally using the provider's public keys (JWKS endpoint). This is the most common format for modern identity providers.
  • Opaque tokens: Validated via token introspection (RFC 7662) by querying the provider's introspection endpoint. Use this when your provider issues non-JWT tokens.

This enables enterprise authentication with providers like Keycloak, Auth0, Okta, Azure AD, Kubernetes service accounts, or any OAuth-compliant service.

Basic OAuth configuration

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    providers:
      - name: keycloak
        issuerUrl: https://keycloak.example.com/realms/mcp
        audience: registry-api

OAuth configuration fields

Field Type Required Default Description
mode string Yes oauth Authentication mode (oauth or anonymous)
resourceUrl string Yes - The URL of the registry resource being protected
realm string No mcp-registry OAuth realm identifier
scopesSupported []string No [mcp-registry:read, mcp-registry:write] OAuth scopes advertised in the discovery endpoint
publicPaths []string No [] Additional paths accessible without authentication
providers array Yes - List of OAuth/OIDC identity providers

Provider configuration fields

Field Type Required Description
name string Yes Provider identifier for logging and monitoring
issuerUrl string Yes OAuth/OIDC issuer URL (e.g., https://keycloak.example.com/realms/mcp)
audience string Yes Expected audience claim in the access token
jwksUrl string No JWKS endpoint URL (skips OIDC discovery if specified)
introspectionUrl string No Token introspection endpoint URL for opaque token validation
clientId string No OAuth client ID (for authenticated introspection requests)
clientSecretFile string No Path to file containing client secret (for authenticated introspection)
caCertPath string No Path to CA certificate for TLS verification
authTokenFile string No Path to token file for authenticating JWKS/introspection requests
allowPrivateIP bool No Allow connections to private IP addresses (for in-cluster use)

Complete OAuth configuration example

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    realm: mcp-registry
    scopesSupported:
      - mcp-registry:read
      - mcp-registry:write
    publicPaths:
      - /custom-health
      - /metrics
    providers:
      - name: keycloak-prod
        issuerUrl: https://keycloak.example.com/realms/production
        audience: registry-api
        clientId: registry-client
        clientSecretFile: /etc/secrets/keycloak-secret
        caCertPath: /etc/ssl/certs/keycloak-ca.crt
      - name: keycloak-staging
        issuerUrl: https://keycloak.example.com/realms/staging
        audience: registry-api-staging

Opaque token configuration

When your identity provider issues opaque (non-JWT) tokens, configure the introspectionUrl field to enable token introspection:

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    providers:
      - name: google
        issuerUrl: https://accounts.google.com
        introspectionUrl: https://oauth2.googleapis.com/tokeninfo
        audience: 407408718192.apps.googleusercontent.com

The server automatically detects the token format and uses the appropriate validation method, attempting JWT validation first and falling back to token introspection if needed.

Kubernetes authentication

For Kubernetes deployments, you can configure OAuth to validate service account tokens. This provides automatic, zero-config authentication for workloads running in the cluster.

Kubernetes provider configuration

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    providers:
      - name: kubernetes
        issuerUrl: https://kubernetes.default.svc.cluster.local
        jwksUrl: https://kubernetes.default.svc/openid/v1/jwks
        audience: registry-server
        caCertPath: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
        allowPrivateIP: true

:::info[Kubernetes-specific configuration]

  • issuerUrl: Use https://kubernetes.default.svc.cluster.local to match the iss claim in Kubernetes service account tokens.
  • jwksUrl: Specify the JWKS endpoint directly to skip OIDC discovery.
  • authTokenFile: The server uses this token to authenticate when fetching the JWKS from the Kubernetes API server.
  • allowPrivateIP: Required for in-cluster communication with the API server.

:::

How Kubernetes authentication works

  1. Workloads mount projected service account tokens with a specific audience
  2. Clients send these tokens in the Authorization: Bearer <TOKEN> header
  3. The server validates tokens using the Kubernetes API server's public keys
  4. Only tokens with the correct audience (e.g., registry-server) are accepted

Kubernetes deployment example

When deploying in Kubernetes, the service account CA certificate is automatically mounted:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry-api
  template:
    metadata:
      labels:
        app: registry-api
    spec:
      serviceAccountName: registry-api
      containers:
        - name: registry-api
          image: ghcr.io/stacklok/thv-registry-api:latest
          args:
            - serve
            - --config=/etc/registry/config.yaml
          volumeMounts:
            - name: config
              mountPath: /etc/registry/config.yaml
              subPath: config.yaml
              readOnly: true
            # Service account token and CA cert are mounted automatically by Kubernetes
      volumes:
        - name: config
          configMap:
            name: registry-api-config

The service account token and CA certificate are automatically mounted at:

  • Token: /var/run/secrets/kubernetes.io/serviceaccount/token
  • CA cert: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt

Client workload example

Clients that need to authenticate with the registry should mount a projected service account token with the correct audience:

apiVersion: v1
kind: Pod
metadata:
  name: registry-client
spec:
  serviceAccountName: my-client-sa
  containers:
    - name: client
      image: my-client-image
      volumeMounts:
        - name: registry-token
          mountPath: /var/run/secrets/registry
          readOnly: true
  volumes:
    - name: registry-token
      projected:
        sources:
          - serviceAccountToken:
              audience: registry-server
              expirationSeconds: 3600
              path: token

The client reads the token from /var/run/secrets/registry/token and includes it in the Authorization: Bearer <TOKEN> header when making requests to the registry.

Provider-specific examples

Pick the provider you use and adapt the example. Each tab includes the configuration and any notes specific to that provider.

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    providers:
      - name: keycloak
        issuerUrl: https://keycloak.example.com/realms/YOUR_REALM
        audience: registry-api

The issuerUrl should point to your Keycloak realm. The realm name is part of the URL path.

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    providers:
      - name: auth0
        issuerUrl: https://YOUR_DOMAIN.auth0.com/
        audience: https://registry.example.com

For Auth0, the issuerUrl is your Auth0 domain. The audience should match the API identifier configured in your Auth0 dashboard.

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    providers:
      - name: azure-ad
        issuerUrl: https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0
        audience: api://YOUR_CLIENT_ID

For Azure AD, the issuerUrl includes your tenant ID, and the audience typically uses the api:// scheme with your application's client ID.

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com
    providers:
      - name: okta
        issuerUrl: https://YOUR_DOMAIN.okta.com/oauth2/default
        audience: api://default

For Okta, the issuerUrl points to your Okta authorization server. Use /oauth2/default for the default authorization server or /oauth2/YOUR_AUTH_SERVER_ID for custom servers.

Anonymous authentication

Anonymous mode disables authentication entirely, allowing unrestricted access to all registry endpoints. This is only suitable for development, testing, or internal deployments where authentication is handled at a different layer (e.g., network policies, VPN, or reverse proxy).

Anonymous configuration

auth:
  mode: anonymous

:::danger[No access control]

Anonymous mode provides no access control. Only use it in trusted environments or when other security measures are in place. Do not use anonymous mode in production.

:::

Anonymous use cases

  • Local development and testing
  • Internal deployments behind corporate firewalls
  • Read-only public registries
  • Environments with external authentication (reverse proxy, API gateway)

Default public paths

The following endpoints are always accessible without authentication, regardless of the auth mode:

  • /openapi.json - OpenAPI specification
  • /.well-known/* - OAuth discovery endpoints (RFC 9728)

The /health, /readiness, and /version endpoints are served on a separate internal server (default port 8081) and are not exposed on the main API port. See the command-line flags for the --internal-address option.

You can configure additional public paths using the publicPaths field in your OAuth configuration. See the Registry API reference for complete endpoint documentation.

RFC 9728 OAuth discovery

The server implements RFC 9728 for OAuth Protected Resource Metadata, enabling clients to automatically discover authentication requirements.

Discovery endpoint

Clients can discover the server's OAuth configuration at:

GET /.well-known/oauth-protected-resource

Example discovery response

{
  "resource": "https://registry.example.com",
  "authorization_servers": [
    "https://keycloak.example.com/realms/production",
    "https://keycloak.example.com/realms/staging"
  ],
  "scopes_supported": ["mcp-registry:read", "mcp-registry:write"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://docs.example.com/registry"
}

This allows OAuth clients to automatically configure themselves without manual setup, improving interoperability and reducing configuration errors.

When a request fails authentication, the server returns a WWW-Authenticate header that includes a link to the discovery endpoint, helping clients locate the authentication requirements.

Testing authentication

Using curl with a bearer token

TOKEN="your-jwt-token-here"

curl -H "Authorization: Bearer $TOKEN" \
  https://registry.example.com/registry/default/v0.1/servers

Using kubectl with Kubernetes service accounts

Use kubectl create token to generate a token with the correct audience:

# Create a token with the registry-server audience
TOKEN=$(kubectl create token <service-account-name> \
  -n <namespace> \
  --audience=registry-server)

# Make authenticated request
curl -H "Authorization: Bearer $TOKEN" \
  https://registry.example.com/registry/default/v0.1/servers

:::tip[Projected tokens vs kubectl create token]

For automated workloads, use projected service account tokens (see Client workload example). The kubectl create token command is useful for manual testing and debugging.

:::

Testing token validation

To verify your token is valid:

  1. Decode the JWT at jwt.io (don't paste production tokens!)
  2. Check the iss (issuer) matches your configured issuerUrl
  3. Check the aud (audience) matches your configured audience
  4. Check the exp (expiration) is in the future

Choosing an authentication mode

Mode Security Complexity Best for
OAuth High Medium Production deployments, enterprise environments
Anonymous None None Development, testing, internal trusted networks

Recommendations:

  • Production deployments: Always use OAuth mode with your organization's identity provider
  • Kubernetes deployments: Use OAuth with Kubernetes provider for automatic authentication
  • Development/testing: Use anonymous mode for local development only
  • Public read-only registries: Use OAuth mode with rate limiting; avoid anonymous in production

Security considerations

Token validation

All OAuth providers validate:

  • Token expiration (exp claim)
  • Audience claim (aud) matches configuration
  • Issuer (iss) matches the configured provider

For JWT tokens, signature verification uses the provider's public keys (fetched from the issuer's JWKS endpoint). For opaque tokens, the server queries the configured introspectionUrl to validate the token.

HTTPS requirements

Always use HTTPS in production to protect tokens in transit:

auth:
  mode: oauth
  oauth:
    resourceUrl: https://registry.example.com # Use HTTPS
    providers:
      - issuerUrl: https://keycloak.example.com/realms/mcp # Use HTTPS

Token storage

  • Never log or persist JWT tokens in plaintext
  • Use short-lived tokens when possible (e.g., 1 hour)
  • Implement token refresh flows for long-running clients
  • Rotate client secrets regularly if using clientSecretFile

Custom CA certificates

If your identity provider uses a custom CA certificate, specify the caCertPath in your provider configuration:

providers:
  - name: internal-keycloak
    issuerUrl: https://keycloak.internal.example.com/realms/mcp
    audience: registry-api
    caCertPath: /etc/ssl/certs/internal-ca.crt

Next steps

Troubleshooting

401 Unauthorized responses

A 401 means the server rejected the bearer token. Decode the token (for example, paste it into jwt.io, and never use a production token), then verify:

  • Header format. The Authorization header must be Authorization: Bearer <TOKEN> with no extra whitespace or quoting.
  • Issuer (iss) matches. The iss claim must exactly match the configured issuerUrl for one of your providers, including any path components (for example, /realms/production for Keycloak or /oauth2/default for Okta).
  • Audience (aud) matches. The aud claim must match the configured audience for that provider.
  • Token hasn't expired. Check the exp claim is in the future.

If those all check out, look at the Registry Server logs for the specific validation error, which usually pinpoints the failing claim or signature problem.

Server can't reach the identity provider's JWKS endpoint

The Registry Server first fetches the OIDC discovery document from ${issuerUrl}/.well-known/openid-configuration, then fetches signing keys from the jwks_uri listed in that document. Either fetch can fail. If logs show failed to fetch JWKS, connection refused, or x509: certificate signed by unknown authority, check the URL in the error to identify which fetch failed:

  1. DNS and network egress. Confirm the Registry Server pod or host can resolve and reach both the issuer hostname and the JWKS hostname (often the same, but providers like Auth0 host JWKS on a separate domain). Test with curl against the URL in the error message.
  2. Firewall rules. Ensure outbound HTTPS (port 443) to the provider is permitted.
  3. Custom CA. If your provider uses an internal CA, set caCertPath on the provider config to a file containing the trust chain. Mount that file into the pod.
  4. Kubernetes API server provider. For the Kubernetes provider, set allowPrivateIP: true, point caCertPath at /var/run/secrets/kubernetes.io/serviceaccount/ca.crt, and confirm the Service Account has the system:auth-delegator role.
Tokens from one provider work but tokens from another don't

When you configure multiple providers, the server tries each in order and accepts the first match. If only one provider works:

  1. Confirm iss matches exactly. A trailing slash mismatch is a common culprit; the iss claim and the configured issuerUrl must be byte-for- byte identical.
  2. Confirm aud is correct per provider. Each provider can have a different audience; verify the failing provider's tokens have the audience you configured for it.
  3. Verify each issuer's discovery endpoint. Run curl ${issuerUrl}/.well-known/openid-configuration for each provider to confirm it's reachable from the server and returns valid JSON.
  4. Check the logs. The server logs which provider is being tried and why validation failed. That tells you whether the issue is reachability, signature validation, or claim mismatch.