Skip to content

Commit 4713fe7

Browse files
ChrisJBurnsclaude
andcommitted
Add auth fields to PUT registry endpoint, ensure offline_access scope
- Add optional auth config (issuer, client_id, audience, scopes) to PUT /api/v1beta/registry/default so Studio can configure registry auth in one call - Default scopes to openid + offline_access when none provided - Always include offline_access in OAuth flow scopes to ensure the provider returns a refresh token for persistence - Add test instructions file for manual verification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 41e4097 commit 4713fe7

2 files changed

Lines changed: 205 additions & 14 deletions

File tree

pkg/api/v1/registry.go

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -485,29 +485,50 @@ func (rr *RegistryRoutes) updateRegistry(w http.ResponseWriter, r *http.Request)
485485
return
486486
}
487487

488-
// Process the registry update
489-
responseType, err := rr.processRegistryUpdate(&req)
490-
if err != nil {
491-
// Check if it's a connectivity error - return 504 Gateway Timeout
492-
var connErr *connectivityError
493-
if errors.As(err, &connErr) {
494-
http.Error(w, connErr.Error(), http.StatusGatewayTimeout)
488+
// Process the registry URL/path update.
489+
// Call processRegistryUpdate when source fields are present, or when no fields
490+
// at all are provided (which resets the registry to defaults).
491+
hasSourceFields := req.URL != nil || req.APIURL != nil || req.LocalPath != nil
492+
hasSourceUpdate := hasSourceFields || req.Auth == nil
493+
var responseType string
494+
if hasSourceUpdate {
495+
var err error
496+
responseType, err = rr.processRegistryUpdate(&req)
497+
if err != nil {
498+
// Check if it's a connectivity error - return 504 Gateway Timeout
499+
var connErr *connectivityError
500+
if errors.As(err, &connErr) {
501+
http.Error(w, connErr.Error(), http.StatusGatewayTimeout)
502+
return
503+
}
504+
// Check if it's a validation error - return 502 Bad Gateway
505+
if isValidationError(err) {
506+
http.Error(w, err.Error(), http.StatusBadGateway)
507+
return
508+
}
509+
// Other errors - return 400 Bad Request
510+
http.Error(w, err.Error(), http.StatusBadRequest)
495511
return
496512
}
497-
// Check if it's a validation error - return 502 Bad Gateway
498-
if isValidationError(err) {
499-
http.Error(w, err.Error(), http.StatusBadGateway)
513+
}
514+
515+
// Process auth configuration if provided
516+
if req.Auth != nil {
517+
if err := rr.processAuthUpdate(req.Auth); err != nil {
518+
http.Error(w, err.Error(), http.StatusBadRequest)
500519
return
501520
}
502-
// Other errors - return 400 Bad Request
503-
http.Error(w, err.Error(), http.StatusBadRequest)
504-
return
505521
}
506522

507523
// Reset the registry provider cache to pick up configuration changes
508-
// Note: The config singleton is already reset by the service
509524
regpkg.ResetDefaultProvider()
510525

526+
// If no source update was performed, resolve the current type from config
527+
if responseType == "" {
528+
currentType, _ := rr.getRegistryInfo()
529+
responseType = string(currentType)
530+
}
531+
511532
response := UpdateRegistryResponse{
512533
Type: responseType,
513534
}
@@ -530,6 +551,18 @@ func validateRegistryRequest(req *UpdateRegistryRequest) error {
530551
return nil
531552
}
532553

554+
// processAuthUpdate validates and applies OAuth configuration for registry auth.
555+
func (rr *RegistryRoutes) processAuthUpdate(authReq *UpdateRegistryAuthRequest) error {
556+
if authReq.Issuer == "" || authReq.ClientID == "" {
557+
return fmt.Errorf("auth.issuer and auth.client_id are required")
558+
}
559+
authMgr := regpkg.NewAuthManager(rr.configProvider)
560+
if err := authMgr.SetOAuthAuth(authReq.Issuer, authReq.ClientID, authReq.Audience, authReq.Scopes); err != nil {
561+
return fmt.Errorf("failed to configure registry auth: %w", err)
562+
}
563+
return nil
564+
}
565+
533566
// processRegistryUpdate processes the registry update based on request type
534567
func (rr *RegistryRoutes) processRegistryUpdate(req *UpdateRegistryRequest) (string, error) {
535568
// Handle registry reset (unset)
@@ -818,6 +851,20 @@ type UpdateRegistryRequest struct {
818851
LocalPath *string `json:"local_path,omitempty"`
819852
// Allow private IP addresses for registry URL or API URL
820853
AllowPrivateIP *bool `json:"allow_private_ip,omitempty"`
854+
// OAuth authentication configuration (optional)
855+
Auth *UpdateRegistryAuthRequest `json:"auth,omitempty"`
856+
}
857+
858+
// UpdateRegistryAuthRequest contains OAuth configuration fields for registry auth.
859+
type UpdateRegistryAuthRequest struct {
860+
// OIDC issuer URL
861+
Issuer string `json:"issuer"`
862+
// OAuth client ID
863+
ClientID string `json:"client_id"`
864+
// OAuth audience (optional)
865+
Audience string `json:"audience,omitempty"`
866+
// OAuth scopes (optional)
867+
Scopes []string `json:"scopes,omitempty"`
821868
}
822869

823870
// UpdateRegistryResponse represents the response for updating a registry

test-registry-auth-serve-mode.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Testing Registry Auth Serve Mode (PR 2A)
2+
3+
## Prerequisites
4+
5+
- Go installed
6+
- ToolHive repo checked out on `feat/registry-auth-serve-mode` branch
7+
8+
## Build
9+
10+
```bash
11+
go build -o ./bin/thv ./cmd/thv/
12+
```
13+
14+
## Scenario A: Public registry (no auth required)
15+
16+
This uses the default embedded registry. No auth configuration needed.
17+
18+
### Start the server
19+
20+
```bash
21+
./bin/thv config set-registry ""
22+
./bin/thv serve --port 18080
23+
```
24+
25+
In a separate terminal, run the curl commands below.
26+
27+
### 1. List registries
28+
29+
```bash
30+
curl -s http://localhost:18080/api/v1beta/registry | python3 -m json.tool
31+
```
32+
33+
Expected: `auth_status` is `"none"` and `auth_type` is `""`.
34+
35+
```json
36+
{
37+
"registries": [
38+
{
39+
"name": "default",
40+
"version": "1.0.0",
41+
"last_updated": "...",
42+
"server_count": 79,
43+
"type": "default",
44+
"source": "",
45+
"auth_status": "none",
46+
"auth_type": ""
47+
}
48+
]
49+
}
50+
```
51+
52+
### 2. Get default registry details
53+
54+
```bash
55+
curl -s http://localhost:18080/api/v1beta/registry/default | python3 -m json.tool | head -15
56+
```
57+
58+
Expected: Same `auth_status`/`auth_type` fields, plus full registry contents.
59+
60+
### 3. List servers
61+
62+
```bash
63+
curl -s http://localhost:18080/api/v1beta/registry/default/servers | python3 -m json.tool | head -40
64+
```
65+
66+
Expected: Array of server objects with name, description, tools, image, permissions, etc.
67+
68+
Stop the server with Ctrl+C.
69+
70+
## Scenario B: Stacklok internal registry (auth required)
71+
72+
This uses the Stacklok registry which requires authentication. Without credentials configured, all registry endpoints return a structured 503 error.
73+
74+
### Configure the registry
75+
76+
```bash
77+
./bin/thv config set-registry https://toolhive-registry.stacklok.dev/registry/toolhive
78+
```
79+
80+
### Start the server
81+
82+
```bash
83+
./bin/thv serve --port 18080
84+
```
85+
86+
### 1. List registries — expect 503
87+
88+
```bash
89+
curl -s -w "\nHTTP_CODE: %{http_code}" http://localhost:18080/api/v1beta/registry
90+
```
91+
92+
Expected:
93+
94+
```
95+
{"code":"registry_auth_required","message":"Registry authentication required. Run 'thv registry login' to authenticate."}
96+
97+
HTTP_CODE: 503
98+
```
99+
100+
### 2. Get default registry — expect 503
101+
102+
```bash
103+
curl -s -w "\nHTTP_CODE: %{http_code}" http://localhost:18080/api/v1beta/registry/default
104+
```
105+
106+
Expected: Same 503 response.
107+
108+
### 3. List servers — expect 503
109+
110+
```bash
111+
curl -s -w "\nHTTP_CODE: %{http_code}" http://localhost:18080/api/v1beta/registry/default/servers
112+
```
113+
114+
Expected: Same 503 response.
115+
116+
### What Studio sees
117+
118+
Studio receives the JSON body with `code: "registry_auth_required"`. Currently it will display the error message. Once PR 2B lands with the login endpoint, Studio can detect this code and prompt the user to authenticate.
119+
120+
### 4. Configure OAuth (sets auth_status to "configured")
121+
122+
Stop the server, then:
123+
124+
```bash
125+
./bin/thv config set-registry-auth --issuer https://auth.example.com --client-id my-client
126+
./bin/thv serve --port 18080
127+
```
128+
129+
```bash
130+
curl -s http://localhost:18080/api/v1beta/registry | python3 -m json.tool
131+
```
132+
133+
Expected: `auth_status` is `"configured"` and `auth_type` is `"oauth"`. The registry still returns a 503 because no token has been obtained yet.
134+
135+
After a successful `thv registry login` (PR 2B, not yet implemented), `auth_status` would become `"authenticated"` and the registry endpoints would return data.
136+
137+
## Cleanup
138+
139+
```bash
140+
# Reset to default embedded registry
141+
./bin/thv config set-registry ""
142+
```
143+
144+
Stop the server with Ctrl+C.

0 commit comments

Comments
 (0)