@@ -29,9 +29,17 @@ unless you explicitly choose anonymous mode for development scenarios.
2929
3030## OAuth authentication
3131
32- OAuth mode (the default) validates JWT tokens from identity providers. This
33- enables enterprise authentication with providers like Keycloak, Auth0, Okta,
34- Azure AD, Kubernetes service accounts, or any OIDC-compliant service.
32+ OAuth mode (the default) validates access tokens from identity providers. The
33+ server supports two token formats:
34+
35+ - ** JWT tokens** : Validated locally using the provider's public keys (JWKS
36+ endpoint). This is the most common format for modern identity providers.
37+ - ** Opaque tokens** : Validated via token introspection (RFC 7662) by querying
38+ the provider's introspection endpoint. Use this when your provider issues
39+ non-JWT tokens.
40+
41+ This enables enterprise authentication with providers like Keycloak, Auth0,
42+ Okta, Azure AD, Kubernetes service accounts, or any OAuth-compliant service.
3543
3644### Basic OAuth configuration
3745
5361| ` mode` | string | Yes | `oauth` | Authentication mode (`oauth` or `anonymous`) |
5462| `resourceUrl` | string | Yes | - | The URL of the registry resource being protected |
5563| `realm` | string | No | `mcp-registry` | OAuth realm identifier |
56- | `scopesSupported` | []string | No | `[mcp-registry:read, mcp-registry:write]` | Supported OAuth scopes |
64+ | `scopesSupported` | []string | No | `[mcp-registry:read, mcp-registry:write]` | OAuth scopes advertised in the discovery endpoint |
5765| `publicPaths` | []string | No | `[]` | Additional paths accessible without authentication |
5866| `providers` | array | Yes | - | List of OAuth/OIDC identity providers |
5967
@@ -63,10 +71,14 @@ auth:
6371| ------------------ | ------ | -------- | ----------------------------------------------------------------------- |
6472| `name` | string | Yes | Provider identifier for logging and monitoring |
6573| `issuerUrl` | string | Yes | OAuth/OIDC issuer URL (e.g., `https://keycloak.example.com/realms/mcp`) |
66- | `audience` | string | Yes | Expected audience claim in the JWT token |
67- | `clientId` | string | No | OAuth client ID (for token introspection) |
68- | `clientSecretFile` | string | No | Path to file containing client secret (for token introspection) |
74+ | `audience` | string | Yes | Expected audience claim in the access token |
75+ | `jwksUrl` | string | No | JWKS endpoint URL (skips OIDC discovery if specified) |
76+ | `introspectionUrl` | string | No | Token introspection endpoint URL for opaque token validation |
77+ | `clientId` | string | No | OAuth client ID (for authenticated introspection requests) |
78+ | `clientSecretFile` | string | No | Path to file containing client secret (for authenticated introspection) |
6979| `caCertPath` | string | No | Path to CA certificate for TLS verification |
80+ | `authTokenFile` | string | No | Path to token file for authenticating JWKS/introspection requests |
81+ | `allowPrivateIP` | bool | No | Allow connections to private IP addresses (for in-cluster use) |
7082
7183# ## Complete OAuth configuration example
7284
@@ -94,6 +106,27 @@ auth:
94106 audience: registry-api-staging
95107` ` `
96108
109+ # ## Opaque token configuration
110+
111+ When your identity provider issues opaque (non-JWT) tokens, configure the
112+ `introspectionUrl` field to enable token introspection :
113+
114+ ` ` ` yaml title="config-opaque-tokens.yaml"
115+ auth:
116+ mode: oauth
117+ oauth:
118+ resourceUrl: https://registry.example.com
119+ providers:
120+ - name: google
121+ issuerUrl: https://accounts.google.com
122+ introspectionUrl: https://oauth2.googleapis.com/tokeninfo
123+ audience: 407408718192.apps.googleusercontent.com
124+ ` ` `
125+
126+ The server automatically detects the token format and uses the appropriate
127+ validation method—attempting JWT validation first and falling back to token
128+ introspection if needed.
129+
97130# # Kubernetes authentication
98131
99132For Kubernetes deployments, you can configure OAuth to validate service account
@@ -109,26 +142,31 @@ auth:
109142 resourceUrl: https://registry.example.com
110143 providers:
111144 - name: kubernetes
112- issuerUrl: https://kubernetes.default.svc
113- audience: https://kubernetes.default.svc
145+ issuerUrl: https://kubernetes.default.svc.cluster.local
146+ jwksUrl: https://kubernetes.default.svc/openid/v1/jwks
147+ audience: registry-server
114148 caCertPath: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
149+ authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
150+ allowPrivateIP: true
115151` ` `
116152
117- :::tip[In-cluster service DNS ]
153+ :::info[Kubernetes-specific configuration ]
118154
119- The correct Kubernetes API server URL for in-cluster access is
120- ` https://kubernetes.default.svc` (not
121- ` https://kubernetes.default.svc.cluster.local` ).
155+ - **issuerUrl**: Use `https://kubernetes.default.svc.cluster.local` to match the
156+ ` iss` claim in Kubernetes service account tokens.
157+ - **jwksUrl**: Specify the JWKS endpoint directly to skip OIDC discovery.
158+ - **authTokenFile**: The server uses this token to authenticate when fetching
159+ the JWKS from the Kubernetes API server.
160+ - **allowPrivateIP**: Required for in-cluster communication with the API server.
122161
123162:: :
124163
125164# ## How Kubernetes authentication works
126165
127- 1. Workloads in the cluster mount service account tokens automatically at
128- ` /var/run/secrets/kubernetes.io/serviceaccount/token`
166+ 1. Workloads mount projected service account tokens with a specific audience
1291672. Clients send these tokens in the `Authorization : Bearer <TOKEN>` header
1301683. The server validates tokens using the Kubernetes API server's public keys
131- 4. Access is granted based on the service account's identity and token claims
169+ 4. Only tokens with the correct audience (e.g., `registry-server`) are accepted
132170
133171# ## Kubernetes deployment example
134172
@@ -174,6 +212,39 @@ The service account token and CA certificate are automatically mounted at:
174212- Token : ` /var/run/secrets/kubernetes.io/serviceaccount/token`
175213- CA cert : ` /var/run/secrets/kubernetes.io/serviceaccount/ca.crt`
176214
215+ # ## Client workload example
216+
217+ Clients that need to authenticate with the registry should mount a projected
218+ service account token with the correct audience :
219+
220+ ` ` ` yaml title="client-workload.yaml"
221+ apiVersion: v1
222+ kind: Pod
223+ metadata:
224+ name: registry-client
225+ spec:
226+ serviceAccountName: my-client-sa
227+ containers:
228+ - name: client
229+ image: my-client-image
230+ volumeMounts:
231+ - name: registry-token
232+ mountPath: /var/run/secrets/registry
233+ readOnly: true
234+ volumes:
235+ - name: registry-token
236+ projected:
237+ sources:
238+ - serviceAccountToken:
239+ audience: registry-server
240+ expirationSeconds: 3600
241+ path: token
242+ ` ` `
243+
244+ The client reads the token from `/var/run/secrets/registry/token` and includes
245+ it in the `Authorization : Bearer <TOKEN>` header when making requests to the
246+ registry.
247+
177248# # Provider-specific examples
178249
179250# ## Keycloak
@@ -317,6 +388,10 @@ GET /.well-known/oauth-protected-resource
317388This allows OAuth clients to automatically configure themselves without manual
318389setup, improving interoperability and reducing configuration errors.
319390
391+ When a request fails authentication, the server returns a `WWW-Authenticate`
392+ header that includes a link to the discovery endpoint, helping clients locate
393+ the authentication requirements.
394+
320395# # Testing authentication
321396
322397# ## Using curl with a bearer token
@@ -330,17 +405,27 @@ curl -H "Authorization: Bearer $TOKEN" \
330405
331406# ## Using kubectl with Kubernetes service accounts
332407
408+ Use `kubectl create token` to generate a token with the correct audience :
409+
333410` ` ` bash
334- # Get the service account token
335- TOKEN=$(kubectl get secret <service-account-token -name> \
411+ # Create a token with the registry-server audience
412+ TOKEN=$(kubectl create token <service-account-name> \
336413 -n <namespace> \
337- -o jsonpath='{.data.token}' | base64 -d )
414+ --audience=registry-server )
338415
339416# Make authenticated request
340417curl -H "Authorization: Bearer $TOKEN" \
341- https://registry.example.com/default /v0.1/servers
418+ https://registry.example.com/registry /v0.1/servers
342419` ` `
343420
421+ :::tip[Projected tokens vs kubectl create token]
422+
423+ For automated workloads, use projected service account tokens (see
424+ [Client workload example](#client-workload-example)). The `kubectl create token`
425+ command is useful for manual testing and debugging.
426+
427+ :: :
428+
344429# ## Testing token validation
345430
346431To verify your token is valid :
@@ -373,12 +458,14 @@ To verify your token is valid:
373458
374459All OAuth providers validate :
375460
376- - Token signature using the provider's public keys (fetched from issuer's JWKS
377- endpoint)
378461- Token expiration (`exp` claim)
379462- Audience claim (`aud`) matches configuration
380463- Issuer (`iss`) matches the configured provider
381464
465+ For JWT tokens, signature verification uses the provider's public keys (fetched
466+ from the issuer's JWKS endpoint). For opaque tokens, the server queries the
467+ configured `introspectionUrl` to validate the token.
468+
382469# ## HTTPS requirements
383470
384471Always use HTTPS in production to protect tokens in transit :
0 commit comments