Skip to content

Wire non-interactive registry auth for serve mode#4111

Merged
ChrisJBurns merged 8 commits intomainfrom
feat/registry-auth-serve-mode
Mar 19, 2026
Merged

Wire non-interactive registry auth for serve mode#4111
ChrisJBurns merged 8 commits intomainfrom
feat/registry-auth-serve-mode

Conversation

@ChrisJBurns
Copy link
Copy Markdown
Collaborator

@ChrisJBurns ChrisJBurns commented Mar 11, 2026

Summary

  • Why: When thv serve runs 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 serve API:

    • Non-interactive provider: WithInteractive(false) suppresses browser OAuth flows in serve mode
    • Auth status in responses: auth_status (none / configured / authenticated) and auth_type fields in all registry GET responses
    • Structured 503 errors: JSON {"code": "registry_auth_required", "message": "..."} when tokens are missing, instead of generic 500s
    • Typed HTTP errors: RegistryHTTPError with Unwrap() for 401/403 detection from upstream registries, with LimitReader on error bodies
    • Auth error propagation: 401s from the upstream registry are wrapped as ErrRegistryAuthRequired and surfaced as 503s to clients
    • API auth endpoints (serve mode only):
      • POST /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 cache
    • OAuth config via API: PUT /api/v1beta/registry/default now accepts an optional auth object with issuer, client_id, audience, and scopes fields for configuring registry OAuth
    • AuthManager extension: GetAuthStatus() method consolidates auth state inspection for API responses

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

Changes

File Change
pkg/registry/api/client.go Add ErrRegistryUnauthorized, RegistryHTTPError type with Unwrap() for 401/403. Replace fmt.Errorf with typed errors and LimitReader in GetServer, fetchServersPage, SearchServers.
pkg/registry/factory.go Add ProviderOption / WithInteractive(), thread opts through GetDefaultProviderWithConfig and NewRegistryProvider.
pkg/registry/provider_api.go Wrap 401 from validation probe and GetRegistry() with ErrRegistryAuthRequired for structured 503 responses.
pkg/registry/auth_manager.go Add GetAuthStatus() method and AuthStatusNone / AuthStatusConfigured / AuthStatusAuthenticated constants.
pkg/registry/auth_manager_test.go Table-driven tests for GetAuthStatus().
pkg/api/v1/registry.go Serve-mode support: RegistryRoutes.serveMode, NewRegistryRoutesForServe(), RegistryRouter(serveMode bool). Structured 503 errors: writeRegistryAuthRequiredError(), isRegistryAuthError(). Auth status: resolveAuthStatus(), auth_status/auth_type fields in registryInfo and getRegistryResponse. API endpoints: POST /auth/login, POST /auth/logout. OAuth config: processAuthUpdate(), UpdateRegistryAuthRequest. Auth error handling in listRegistries, getRegistry, listServers, updateRegistry.
pkg/api/server.go Pass serveMode: true to RegistryRouter().
docs/server/* Regenerated swagger docs.

Does this introduce a user-facing change?

Yes.

  • Registry API responses (GET /api/v1beta/registry and GET /api/v1beta/registry/default) now include auth_status and auth_type fields.
  • When thv serve cannot authenticate to a registry (no cached tokens), registry endpoints return HTTP 503 with {"code": "registry_auth_required"} instead of a generic 500.
  • New POST /api/v1beta/registry/auth/login and POST /api/v1beta/registry/auth/logout endpoints (serve mode only) allow desktop clients to trigger OAuth login/logout via the API.
  • PUT /api/v1beta/registry/default now accepts an auth object to configure OAuth settings via the API.

Special notes for reviewers

  • 503 not 401: HTTP 503 is intentional per RFC-0043. Studio is authenticated to the thv serve API — the problem is that thv serve itself lacks a registry credential. 503 correctly signals a server-side dependency issue, not a client auth failure.
  • auth_status limitations: 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.
  • Browser-open from login endpoint: The /auth/login endpoint opens the user's default browser for the OAuth flow. This is intentional for desktop-only serve mode (Studio running thv serve locally). A follow-up could return a redirect URL instead for remote/container environments.
  • No omitempty on auth fields: auth_status and auth_type intentionally omit omitempty so clients always receive the fields, even when the value is "none" or empty string.

Generated with Claude Code

@github-actions github-actions bot added the size/M Medium PR: 300-599 lines changed label Mar 11, 2026
@ChrisJBurns ChrisJBurns changed the title Wire non-interactive registry auth for serve mode DRAFT: Wire non-interactive registry auth for serve mode Mar 11, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 20.00000% with 124 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.08%. Comparing base (a881814) to head (6cb7f55).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
pkg/api/v1/registry.go 11.29% 107 Missing and 3 partials ⚠️
pkg/registry/provider_api.go 0.00% 11 Missing ⚠️
pkg/api/server.go 0.00% 1 Missing ⚠️
pkg/registry/auth_manager.go 94.11% 1 Missing ⚠️
pkg/registry/factory.go 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4111      +/-   ##
==========================================
+ Coverage   68.82%   69.08%   +0.26%     
==========================================
  Files         468      470       +2     
  Lines       47087    47351     +264     
==========================================
+ Hits        32407    32713     +306     
- Misses      11996    12085      +89     
+ Partials     2684     2553     -131     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed size/S Small PR: 100-299 lines changed and removed size/M Medium PR: 300-599 lines changed size/S Small PR: 100-299 lines changed labels Mar 11, 2026
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed size/L Large PR: 600-999 lines changed and removed size/S Small PR: 100-299 lines changed size/M Medium PR: 300-599 lines changed labels Mar 11, 2026
Copy link
Copy Markdown
Contributor

@jhrozek jhrozek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ResetDefaultProvider and GetDefaultProviderWithConfig (factory.go)
  • MEDIUM: updateConfigTokenRef bypasses injected config provider (oauth_token_source.go:213)
  • MEDIUM: Panic on empty baseURL in api.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

@ChrisJBurns ChrisJBurns force-pushed the feat/registry-auth-serve-mode branch from f8aebd1 to 4713fe7 Compare March 13, 2026 22:56
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/L Large PR: 600-999 lines changed size/M Medium PR: 300-599 lines changed labels Mar 13, 2026
@github-actions github-actions bot added the size/M Medium PR: 300-599 lines changed label Mar 17, 2026
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>
@ChrisJBurns ChrisJBurns force-pushed the feat/registry-auth-serve-mode branch from 51cc2d7 to b3d9fc1 Compare March 17, 2026 19:24
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 17, 2026
Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 17, 2026
@ChrisJBurns
Copy link
Copy Markdown
Collaborator Author

@jhrozek I might need another approval

@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 18, 2026
@github-actions github-actions bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 18, 2026
Clients like Studio need to display the configured issuer and client_id
without reading the config file directly. Add an auth_config field to
the GET /registry and GET /registry/default responses that surfaces the
non-secret OAuth configuration (issuer, client_id, audience, scopes).
The field is omitted when no auth is configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/M Medium PR: 300-599 lines changed labels Mar 19, 2026
Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>
@github-actions github-actions bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Mar 19, 2026
@ChrisJBurns ChrisJBurns merged commit 41c7474 into main Mar 19, 2026
42 checks passed
@ChrisJBurns ChrisJBurns deleted the feat/registry-auth-serve-mode branch March 19, 2026 16:22
Sanskarzz pushed a commit to Sanskarzz/toolhive that referenced this pull request Mar 23, 2026
* Add serve-mode registry auth to thv serve API

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>

* docs

Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>

* Return OAuth public config in registry GET responses

Clients like Studio need to display the configured issuer and client_id
without reading the config file directly. Add an auth_config field to
the GET /registry and GET /registry/default responses that surfaces the
non-secret OAuth configuration (issuer, client_id, audience, scopes).
The field is omitted when no auth is configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* updates docs

Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>

---------

Signed-off-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants