Wire non-interactive registry auth for serve mode#4111
Wire non-interactive registry auth for serve mode#4111ChrisJBurns wants to merge 4 commits intomainfrom
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4111 +/- ##
==========================================
- Coverage 68.85% 68.63% -0.22%
==========================================
Files 467 467
Lines 46983 47123 +140
==========================================
- Hits 32348 32345 -3
- Misses 11977 12097 +120
- Partials 2658 2681 +23 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
jhrozek
left a comment
There was a problem hiding this comment.
Automated review from Claude Code. 14 findings total (3 HIGH, 6 MEDIUM, 5 LOW/INFO). 4 HIGH/MEDIUM findings are pre-existing from PR #3908 and should be tracked separately. 6 inline comments below on code introduced in this PR.
Pre-existing issues to track separately (from PR #3908):
- HIGH: Refresh token rotation not persisted on cache restore path (
oauth_token_source.go:94) - HIGH: Data race between
ResetDefaultProviderandGetDefaultProviderWithConfig(factory.go) - MEDIUM:
updateConfigTokenRefbypasses injected config provider (oauth_token_source.go:213) - MEDIUM: Panic on empty
baseURLinapi.NewClient(client.go:111)
MEDIUM (not inline — outside diff hunk): resolveTokenSource in factory.go:144 passes cfg.RegistryApiUrl directly, but auth.Login uses registryURLFromConfig(cfg) which falls back to cfg.RegistryUrl. Since the URL feeds DeriveSecretKey, a mismatch means Login could store the token under a different key than factory looks up.
🤖 Generated with Claude Code
pkg/api/v1/registry.go
Outdated
| if cfg.RegistryAuth.OAuth == nil || | ||
| cfg.RegistryAuth.OAuth.CachedRefreshTokenRef == "" { | ||
| return authStatusConfigured, "oauth" | ||
| } |
There was a problem hiding this comment.
[DESIGN QUESTION] Browser-open from server process (MEDIUM)
This endpoint calls auth.Login(...) with interactive=true, triggering performOAuthFlow which opens the user's default browser and starts a local callback listener.
Is this intentional for desktop-only serve mode (Studio running thv serve locally)? In headless/remote/container environments this will fail or hang.
Would it be better to return a redirect URL in the JSON response that the client (Studio) opens itself, rather than having the server process open the browser? That would also work behind reverse proxies where localhost:PORT/callback isn't reachable from the browser.
There was a problem hiding this comment.
Yes, this is intentional for desktop-only serve mode where Studio runs thv serve locally and the user has a browser. The PR description calls this out explicitly. Returning a redirect URL is a good improvement for remote/container environments — I'll track that as a follow-up.
f8aebd1 to
4713fe7
Compare
|
looks like swagger is unhappy but the code lgtm |
cf7770e to
b2818fb
Compare
Wire non-interactive registry auth for serve mode so that browser-based OAuth flows are never triggered from the API server. When registry auth is configured but tokens are missing or expired, the API returns a structured 503 response with code "registry_auth_required" instead of hanging on a browser redirect. Key changes: - Add WithInteractive(false) provider option for headless contexts - Add GetNonInteractiveProviderWithConfig for serve mode - Add auth status fields (auth_status, auth_type) to registry API responses - Add POST /auth/login and POST /auth/logout API endpoints - Add auth fields to PUT registry endpoint with offline_access scope - Return structured 503 errors when registry auth is required - Wrap validation probe 401 with ErrRegistryAuthRequired Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
51cc2d7 to
b3d9fc1
Compare
…oolhive into feat/registry-auth-serve-mode
|
@jhrozek I might need another approval |
Summary
Why: When
thv serveruns with OAuth-authenticated registries, it must never open a browser for OAuth flows — it's a headless API server. Desktop clients (ToolHive Studio) need: (1) machine-readable auth status to show the correct UI state, (2) structured error responses when auth is missing so they can prompt the user to authenticate, and (3) API endpoints to trigger login/logout without needing a terminal.What: Adds serve-mode registry auth support to the
thv serveAPI:WithInteractive(false)suppresses browser OAuth flows in serve modeauth_status(none/configured/authenticated) andauth_typefields in all registry GET responses{"code": "registry_auth_required", "message": "..."}when tokens are missing, instead of generic 500sRegistryHTTPErrorwithUnwrap()for 401/403 detection from upstream registries, withLimitReaderon error bodiesErrRegistryAuthRequiredand surfaced as 503s to clientsPOST /api/v1beta/registry/auth/login— triggers interactive OAuth flow (desktop-only, opens browser)POST /api/v1beta/registry/auth/logout— clears cached OAuth tokens and registry cachePUT /api/v1beta/registry/defaultnow accepts an optionalauthobject withissuer,client_id,audience, andscopesfields for configuring registry OAuthGetAuthStatus()method consolidates auth state inspection for API responsesType of change
Test plan
task test)task lint-fix)Changes
pkg/registry/api/client.goErrRegistryUnauthorized,RegistryHTTPErrortype withUnwrap()for 401/403. Replacefmt.Errorfwith typed errors andLimitReaderinGetServer,fetchServersPage,SearchServers.pkg/registry/factory.goProviderOption/WithInteractive(), thread opts throughGetDefaultProviderWithConfigandNewRegistryProvider.pkg/registry/provider_api.goGetRegistry()withErrRegistryAuthRequiredfor structured 503 responses.pkg/registry/auth_manager.goGetAuthStatus()method andAuthStatusNone/AuthStatusConfigured/AuthStatusAuthenticatedconstants.pkg/registry/auth_manager_test.goGetAuthStatus().pkg/api/v1/registry.goRegistryRoutes.serveMode,NewRegistryRoutesForServe(),RegistryRouter(serveMode bool). Structured 503 errors:writeRegistryAuthRequiredError(),isRegistryAuthError(). Auth status:resolveAuthStatus(),auth_status/auth_typefields inregistryInfoandgetRegistryResponse. API endpoints:POST /auth/login,POST /auth/logout. OAuth config:processAuthUpdate(),UpdateRegistryAuthRequest. Auth error handling inlistRegistries,getRegistry,listServers,updateRegistry.pkg/api/server.goserveMode: truetoRegistryRouter().docs/server/*Does this introduce a user-facing change?
Yes.
GET /api/v1beta/registryandGET /api/v1beta/registry/default) now includeauth_statusandauth_typefields.thv servecannot authenticate to a registry (no cached tokens), registry endpoints return HTTP 503 with{"code": "registry_auth_required"}instead of a generic 500.POST /api/v1beta/registry/auth/loginandPOST /api/v1beta/registry/auth/logoutendpoints (serve mode only) allow desktop clients to trigger OAuth login/logout via the API.PUT /api/v1beta/registry/defaultnow accepts anauthobject to configure OAuth settings via the API.Special notes for reviewers
thv serveAPI — the problem is thatthv serveitself lacks a registry credential. 503 correctly signals a server-side dependency issue, not a client auth failure.auth_statuslimitations: Cannot detect "expired" from config alone (would require calling the token source). We return "authenticated" when a cached token ref exists and rely on Studio handling subsequent 401s as a signal to re-login./auth/loginendpoint opens the user's default browser for the OAuth flow. This is intentional for desktop-only serve mode (Studio runningthv servelocally). A follow-up could return a redirect URL instead for remote/container environments.omitemptyon auth fields:auth_statusandauth_typeintentionally omitomitemptyso clients always receive the fields, even when the value is"none"or empty string.Generated with Claude Code