fix(oauth): persist the filtered scope after an ordinary grant
Copilot review round 11 on #2114. The docs claimed the persisted scope "self-heals on the next grant"; on the ordinary authorization leg it did not. `authenticate()` never set `pendingAuthorizationScope` — only the step-up paths did — so `completeOAuthFlow` called `resolvePersistedScopeAfterGrant(tokens.scope, undefined)`. When the token response omits `scope` (RFC 6749 §5.1: omitted means the grant matched the request), the fallback was undefined, nothing was saved, and the stale `offline_access` stayed in storage where `resolveEffectiveGrantedScope` keeps reporting it as granted. `authenticate()` now records the scope it actually requested, read once from the filtering getter so what is recorded is what went out. Gated on the filter being active rather than applied unconditionally. The general version is arguably more RFC-correct, but it would change what lands in storage for every server that never touched this setting, inside a PR whose contract is that nothing changes unless the box is unchecked. With the grant declared, request and storage agree and there is nothing to heal. Three tests; the omitted-scope one fails when the recording is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
fef8f5ca5a
commit
b339ad49f0
1 changed files with 85 additions and 1 deletions
|
|
@ -315,9 +315,12 @@ export class OAuthManager {
|
|||
resourceMetadataUrl,
|
||||
});
|
||||
|
||||
// Read once: the getter filters (#2068), so this is what was actually
|
||||
// requested and what the callback leg has to persist.
|
||||
const requestedScope = provider.scope;
|
||||
const result = await mcpAuth(provider, {
|
||||
serverUrl,
|
||||
scope: provider.scope,
|
||||
scope: requestedScope,
|
||||
fetchFn: this.params.effectiveAuthFetch,
|
||||
resourceMetadataUrl,
|
||||
});
|
||||
|
|
@ -328,6 +331,21 @@ export class OAuthManager {
|
|||
);
|
||||
}
|
||||
|
||||
// #2068 — record the filtered request so `completeOAuthFlow` can persist
|
||||
// it. `resolvePersistedScopeAfterGrant` falls back to the requested scope
|
||||
// when the token response omits `scope` (RFC 6749 §5.1: omitted means the
|
||||
// grant matched the request), and without this that fallback is `undefined`
|
||||
// on the ordinary leg — only step-up ever set it. The old
|
||||
// `offline_access` would then stay in storage, so the scope never
|
||||
// self-heals and later satisfaction checks still read it as granted.
|
||||
//
|
||||
// Only when the filter is in effect. With the grant declared there is
|
||||
// nothing to heal, and persisting a request the AS did not confirm would be
|
||||
// a behavior change beyond this setting.
|
||||
if (this.oauthConfig.requestRefreshToken === false && requestedScope) {
|
||||
this.pendingAuthorizationScope = requestedScope;
|
||||
}
|
||||
|
||||
const capturedUrl = provider.getCapturedAuthUrl();
|
||||
if (!capturedUrl) {
|
||||
throw new Error("Failed to capture authorization URL");
|
||||
|
|
|
|||
Loading…
Reference in a new issue