fix(oauth): persist the granted scope on every EMA mint

`trySilentEmaAuth` and `refreshEmaResourceTokens` saved tokens without
ever saving a scope. Both are reached from real paths -- initial EMA
authentication with a cached IdP session, refreshEnterpriseManagedTokens,
and EmaTransportOAuthProvider.tokens -- and each can be the moment a
newly configured or metadata-derived scope first takes effect, so a
silent token response left the previous stored scope standing.

Rather than a third copy of the rule, every EMA mint now ends in one
place: mintEmaResourceTokens reports the scope it requested, and
saveMintedTokens writes tokens and the RFC 6749 5.1 scope together.
The manager's EMA branch drops its own saveScope, which would now be a
redundant second write of the same value; the step-up branch keeps its
own, since it falls back to the challenge's authorizationScopes union
and runs last.

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:
cliffhall 2026-08-25 01:32:40 -04:00
commit c6029e1cbb

View file

@ -376,26 +376,13 @@ export class OAuthManager {
const config = scopeForMint
? { ...emaConfig, scope: scopeForMint }
: emaConfig;
const { tokens, requestedScope } =
await completeEmaIdpAuthorizationAndMint(
config,
authorizationCode,
iss,
);
// The mint reports what it actually asked for, which is neither
// `pendingAuthorizationScope` (undefined on the ordinary leg) nor
// always `scopeForMint` — with no configured or stored scope the
// resource metadata's `scopes_supported` is what gets requested.
const scopeToPersist = resolvePersistedScopeAfterGrant(
tokens.scope,
requestedScope,
// The EMA flow persists the granted scope alongside the tokens, for
// every mint rather than only this one — see saveMintedTokens.
const { tokens } = await completeEmaIdpAuthorizationAndMint(
config,
authorizationCode,
iss,
);
if (scopeToPersist) {
await this.requireStorage().saveScope(
this.getServerUrl(),
scopeToPersist,
);
}
this.pendingAuthorizationScope = undefined;
const completedAt = Date.now();
this.oauthFlowState = {