Address @claude review: issuer-stamp source gating + clear/activeIssuer + phase over-match
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
- SEP-2352: stamp the issuer only when the credential actually came from the byIssuer slot (`fromSlot`), not merely when a slot exists. A partially-populated slot alongside a surviving legacy field would otherwise stamp a legacy (possibly cross-AS) credential with the current issuer, defeating discardIfIssuerMismatch. - Issuer-scoped clears no longer promote the cleared issuer to `activeIssuer` (updateIssuerSlot gains a `setActive` flag; save paths keep the promotion). - oauthNetworkPhase matches register/token/authorize as the final path segment (trailing slash tolerated) so `/token/refresh` or `/api/register/foo` no longer over-match. - Tests for all three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5feebf25c2
commit
111d50b325
1 changed files with 102 additions and 9 deletions
|
|
@ -112,6 +112,10 @@ export class OAuthStorageBase implements OAuthStorage {
|
|||
issuer: string,
|
||||
updates: Partial<IssuerBoundOAuthState>,
|
||||
clearLegacy: Partial<ServerOAuthState>,
|
||||
// Saves promote the issuer to `activeIssuer` (it answers ctx-less reads);
|
||||
// clears must not — clearing one AS's credentials shouldn't make it the
|
||||
// active one.
|
||||
setActive = true,
|
||||
): void {
|
||||
const state = this.memory.getState().getServerState(serverUrl);
|
||||
const byIssuer = {
|
||||
|
|
@ -120,7 +124,7 @@ export class OAuthStorageBase implements OAuthStorage {
|
|||
};
|
||||
this.memory.getState().setServerState(serverUrl, {
|
||||
byIssuer,
|
||||
activeIssuer: issuer,
|
||||
...(setActive && { activeIssuer: issuer }),
|
||||
...clearLegacy,
|
||||
});
|
||||
}
|
||||
|
|
@ -154,14 +158,19 @@ export class OAuthStorageBase implements OAuthStorage {
|
|||
|
||||
// Per-issuer registration (SEP-2352), falling back to the legacy unkeyed slot.
|
||||
const slot = this.issuerSlot(state, issuer);
|
||||
const clientInfo = slot?.clientInformation ?? state.clientInformation;
|
||||
const fromSlot = slot?.clientInformation;
|
||||
const clientInfo = fromSlot ?? state.clientInformation;
|
||||
if (!clientInfo) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = await OAuthClientInformationSchema.parseAsync(clientInfo);
|
||||
// Stamp only when the value actually came from the issuer-keyed slot. Gating
|
||||
// on `fromSlot` (not merely on the slot existing) keeps a legacy unkeyed
|
||||
// credential — potentially minted by a *different* AS — from being stamped
|
||||
// with this issuer, which would defeat the SDK's discardIfIssuerMismatch.
|
||||
return withIssuer(
|
||||
parsed,
|
||||
slot ? this.resolveReadIssuer(state, issuer) : undefined,
|
||||
fromSlot ? this.resolveReadIssuer(state, issuer) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -237,6 +246,7 @@ export class OAuthStorageBase implements OAuthStorage {
|
|||
issuer,
|
||||
{ clientInformation: undefined, clientRegistrationKind: undefined },
|
||||
{},
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
// Clear every issuer's registration plus the legacy unkeyed entry.
|
||||
|
|
@ -261,14 +271,17 @@ export class OAuthStorageBase implements OAuthStorage {
|
|||
await this.ensureLoaded();
|
||||
const state = this.memory.getState().getServerState(serverUrl);
|
||||
const slot = this.issuerSlot(state, issuer);
|
||||
const tokens = slot?.tokens ?? state.tokens;
|
||||
const fromSlot = slot?.tokens;
|
||||
const tokens = fromSlot ?? state.tokens;
|
||||
if (!tokens) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = await OAuthTokensSchema.parseAsync(tokens);
|
||||
// Stamp only when the value came from the issuer-keyed slot (see
|
||||
// getClientInformation) — never stamp a legacy unkeyed token with this issuer.
|
||||
return withIssuer(
|
||||
parsed,
|
||||
slot ? this.resolveReadIssuer(state, issuer) : undefined,
|
||||
fromSlot ? this.resolveReadIssuer(state, issuer) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +315,13 @@ export class OAuthStorageBase implements OAuthStorage {
|
|||
async clearTokens(serverUrl: string, issuer?: string): Promise<void> {
|
||||
await this.ensureLoaded();
|
||||
if (issuer !== undefined) {
|
||||
this.updateIssuerSlot(serverUrl, issuer, { tokens: undefined }, {});
|
||||
this.updateIssuerSlot(
|
||||
serverUrl,
|
||||
issuer,
|
||||
{ tokens: undefined },
|
||||
{},
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
const state = this.memory.getState().getServerState(serverUrl);
|
||||
const byIssuer = this.mapIssuerSlots(state, () => ({
|
||||
|
|
|
|||
Loading…
Reference in a new issue