Skip to content

Commit 0808957

Browse files
authored
Improve registry authentication documentation (#394)
* Add opaque token and introspection support to auth docs - Clarify OAuth mode supports both JWT and opaque tokens - Add introspectionUrl field to provider configuration table - Add opaque token configuration example using Google - Update terminology from OIDC-compliant to OAuth-compliant * Clarify token validation for JWT vs opaque tokens Move JWT-specific signature verification details under a separate paragraph that distinguishes between JWT and opaque token validation methods. * Update Kubernetes authentication docs with tested configuration - Update provider config with correct issuerUrl (.cluster.local suffix) - Add jwksUrl, authTokenFile, and allowPrivateIP fields to config table - Add client workload example with projected service account tokens - Replace legacy kubectl get secret with kubectl create token - Add tip explaining projected tokens vs kubectl create token - Update "How it works" section to reflect audience-based auth * Add WWW-Authenticate note and clarify scopesSupported - Add note about WWW-Authenticate header in RFC 9728 section - Clarify that scopesSupported is for discovery endpoint advertisement
1 parent 89d3f9d commit 0808957

File tree

1 file changed

+109
-22
lines changed

1 file changed

+109
-22
lines changed

docs/toolhive/guides-registry/authentication.mdx

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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

@@ -53,7 +61,7 @@ auth:
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

99132
For 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
129167
2. Clients send these tokens in the `Authorization: Bearer <TOKEN>` header
130168
3. 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
317388
This allows OAuth clients to automatically configure themselves without manual
318389
setup, 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
340417
curl -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

346431
To verify your token is valid:
@@ -373,12 +458,14 @@ To verify your token is valid:
373458

374459
All 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

384471
Always use HTTPS in production to protect tokens in transit:

0 commit comments

Comments
 (0)