Skip to content

e2e: a loopback provider's credential is never renewed, and why - #259

Merged
dinstein merged 1 commit into
mainfrom
oauth-refresh
Aug 8, 2026
Merged

e2e: a loopback provider's credential is never renewed, and why#259
dinstein merged 1 commit into
mainfrom
oauth-refresh

Conversation

@dinstein

@dinstein dinstein commented Aug 8, 2026

Copy link
Copy Markdown
Owner

The OAuth suite covered getting a credential and not keeping one:
oauthlogin_test.go issues an hour-long token, so nothing in it reaches the
renewal machinery. Renewal is where the failure a user actually meets lives —
a login is verified the minute it is performed, while a refresh that does not
work presents days later as a server that worked yesterday.

Trying to cover it turned up why it was uncovered. Signing in and staying
signed in disagree about the same server.
--allow-local and
provenance=local are honoured by both login paths; no renewal path has an
equivalent, so the oauthflow screen refuses the refresh — for both gateway
renewers and the daemon's — before it makes a request.

The user-visible consequence: a self-hosted OAuth MCP server works after login
and stops the moment its token expires, permanently, with only a WARN in the
gateway log. It also bounds this suite — no e2e can demonstrate a successful
refresh, because the only authorization server a test may run is on loopback
and TLS does not help.

So this lands the refusal as a pinned, fail-closed property, and records the
asymmetry in docs/modules/oauth.md where someone looks for it. It does not
change the security control
— that is a decision to take deliberately, not
in passing.

  • 1. Pin the refusal, and document the asymmetry as a known gap

The OAuth e2e covered obtaining a credential and never keeping one — the
existing fixture issues an hour-long token, so no test reached either gateway
renewer. Covering that turned up the reason it had not been: signing in and
staying signed in disagree about the same server. `auth login --allow-local`
and `server add --local` are honoured by both login paths, which derive
AllowLoopback from provenance; neither renewal path can, because
gateway/auth.go builds the oauthflow client with the flag unset and its
factory never sees the entry. The screen refuses before making a request, so
the authorization server is never contacted.

What that costs a user is a self-hosted OAuth server that works after login
and stops at the first expiry, permanently, with one WARN in the gateway log.
What it costs this suite is the success path: the only authorization server a
test may run is on loopback, and TLS does not help because the carve-out is
gated on AllowLoopback whatever the scheme.

The new case pins the refusal instead, asserting that the token endpoint is
not reached at all rather than reached and turned away — a distinction the
counter can see and a log line cannot. docs/modules/oauth.md carries the
asymmetry as gap 5, including that the trade is open: the screen exists
because a loopback OAuth endpoint is usually a misconfiguration, and --local
is exactly the operator saying this one is not. Changing that is a
security-control decision and is deliberately not taken here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dinstein
dinstein merged commit f0b8ce9 into main Aug 8, 2026
3 checks passed
@dinstein
dinstein deleted the oauth-refresh branch August 8, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant