Repository navigation
Conversation
The embedded auth server keeps a session's upstream provider tokens for the upstream expiry plus thirty days and rewrites the row only at login or at an upstream refresh. A client that keeps its own refresh token alive but makes no tool call for that long keeps a valid ToolHive session while every stored upstream row expires. The token validator then projected an empty credential map onto the identity and admitted the request, so a Cedar authorizer pinned to the provider denied every call with HTTP 403 and no WWW-Authenticate, and the client never re-authenticated. Answer the same 401 challenge the validator already sends when an upstream refresh is refused, so the client signs in again and the login writes fresh rows. The check runs on the projected access-token map, so rows that carry only an ID token count as none, and the two branches share one challenge writer. A session that still holds a token for another provider is unchanged and keeps reaching the consumers. Deployments that run the embedded auth server with upstream token injection disabled and no consumer of the upstream token see one re-authentication after the rows expire where they saw none before. The rejection is logged at Debug with the session id and the providers whose rows were still stored, because a client retrying on a stale token sets the rate and the audit log records the 401. Signed-off-by: Aleksandr Filippov <71711753+alex-feel@users.noreply.github.com>
alex-feel
requested review from
ChrisJBurns,
JAORMX,
jhrozek,
rdimitrov and
tgrunnagle
as code owners
October 16, 2026 16:10
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6846 +/- ##
==========================================
- Coverage 79.67% 79.67% -0.01%
==========================================
Files 817 817
Lines 83425 83430 +5
==========================================
+ Hits 66470 66473 +3
- Misses 16950 16952 +2
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The embedded auth server stores a session's upstream provider tokens for the upstream access-token expiry plus
DefaultRefreshTokenTTL(30 days) and rewrites the row only at login or at an upstream refresh. A client that keeps its session alive through refresh grants but makes no tool call for that long keeps a valid ToolHive token while every upstream row of its session expires, because a refresh grant slides the client's refresh token and touches no row. On the next callTokenValidator.Middlewareloaded an empty credential map, projected it onto the identity, and admitted the request. Cedar, pinned to the provider, then failed claim resolution, and the client received HTTP 403 with JSON-RPC error 403 "Unauthorized" and noWWW-Authenticate, whileinitializeandpingkept succeeding, so it never re-authenticated. Today the validator answers 401 only when a stored token could not be refreshed (#5651).WWW-Authenticate: Bearerchallenge witherror="invalid_token"and the RFC 6750 body, also when a token session holds no usable upstream access token for any provider, inTokenValidator.Middleware, before any consumer ofIdentity.UpstreamTokensruns.docs/middleware.mdanddocs/authz.md.Closes #6845
Type of change
Test plan
task test)task test-e2e)task lint-fix)task lint-fixreports nothing in the changed files.task test: every package the change touches passes (pkg/authand its subpackages,pkg/authzand its authorizers,pkg/authserver,pkg/runner,pkg/vmcp/auth); the remaining failures in that run come from packages whose tests depend on the environment (a container runtime, a keyring, non-root file permissions, lock timing) and do not involve this change, so the Unit tests box is left for CI.New tests:
TestMiddleware_UpstreamTokenEnrichment/returns 401 with WWW-Authenticate when the session holds no usable upstream access tokeninpkg/auth/token_test.go, beside the refresh-failure cases, with two rows (no rows stored; rows without an access token), asserting status, challenge, and body; andTestUpstreamSessionWithoutRowsEndToEndinpkg/authz/authorizers/cedar/upstream_session_e2e_test.go, which drives the real validator middleware and the real upstream-token service over in-memory storage: a stored row authenticates and Cedar authorizes; with the rows deleted, authentication rejects the session and Cedar is never consulted. Both fail on main. Inverting the emptiness check, dropping thetsidcondition, removing the header, and changing the description each fail at least one of them.Does this introduce a user-facing change?
Yes. Before: a client whose session had lost every stored upstream token row received HTTP 403 with JSON-RPC error 403 "Unauthorized" on every
tools/call,prompts/get, andresources/read, an emptytools/list, and a succeedinginitialize, and had to be reconnected by hand. After: the first request after the rows are gone receives HTTP 401 withWWW-Authenticate: Bearer error="invalid_token", the same answer as a refused refresh, so the client signs in again and the login stores fresh rows.Operators: the Cedar
authorization deniedWARN carryingupstream token for provider "..." not found in identityno longer appears for this condition. The validator logstoken session holds no upstream access token; returning 401at Debug withtsidandstored_providers(empty when every row is gone), because a client retrying on a stale token controls the rate and an expired row is ordinary lifecycle; the refused-refresh WARN is unchanged. The audit log records the request as an authentication failure (401).Special notes for reviewers
pkg/authzand in the vMCP admission path, the list filters, and the vMCP outgoing strategies. It reuses the refresh-failure branch's challenge (Return 401+WWW-Authenticate when vMCP upstream token is unrefreshable #5651) through one helper,writeUpstreamReauthRequired, and, like that branch, decides without knowing which providers the consumers need. vMCP outgoing auth: unrefreshable upstream token surfaces as backend error, not 401 + WWW-Authenticate #5507 named an upstream token that is "missing or can no longer be refreshed" as the case to answer with 401; Return 401+WWW-Authenticate when vMCP upstream token is unrefreshable #5651 covered the second half, this covers the first.tsidclaim (RFC 8693 delegation, RFC 7523 JWT-bearer, a token-only auth server) are untouched.pkg/authz, would leavetools/list, the vMCP call gate, and the vMCP outgoing strategies as they are for the same session.