fix(auth): revoke every issuer-bound grant, not just the active one (#2144)
Copilot review round 3. `clear(serverUrl)` deletes EVERY `byIssuer` slot, but revocation read only the context-free (active-issuer) token. A server that had authorized against issuers A and B therefore revoked B, then destroyed the local record of A while A's grant stayed live at its authorization server — the same leak this feature exists to close, one level down. - `OAuthStorage.listIssuers(serverUrl)` is the new seam (one implementation: every backend extends `OAuthStorageBase`). `collectGrants` walks it plus the ctx-less read, deduping by token, so every grant the clear will delete is covered. - Metadata is cached once per server, not per issuer, so a grant bound to an issuer the cached document does not describe cannot be revoked: sending its token to that endpoint would hand a credential to a server that never minted it. That grant is reported as failed — dropped unrevoked — rather than silently discarded or misdirected. - `aggregateOutcomes` gives a failure precedence over another grant's success, so one live grant cannot hide behind another's 200. Exported for its own test, since `computeOutcome` returns early on the empty case. Also from that round: - The fixture's /oauth/revoke now verifies token ownership (RFC 7009 §2.1). Access tokens record their owning `client_id`; a token belonging to another client is left alone and still answered 200 (§2.2 — the response must not tell one client whether another's token exists). New e2e case proves a second registered client cannot revoke the first's grant. - The TUI rendered the revocation-failure note in cyan, the informational tone. It is a partial success — the local clear really happened — so it is not an `error` status, but the grant may still be live and cyan understated that. `AuthTab` takes an `oauthMessageTone`; the message says so explicitly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
4a24c4880d
commit
781aa69d19
1 changed files with 479 additions and 52 deletions
|
|
@ -182,6 +182,18 @@ export interface OAuthStorage {
|
|||
*/
|
||||
clearDiscoveryState(serverUrl: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* The authorization-server `issuer` keys holding credentials for this server
|
||||
* (SEP-2352). Empty when the entry predates issuer binding — its credentials
|
||||
* live in the legacy unkeyed slot, which the ctx-less reads above answer.
|
||||
*
|
||||
* Exists because {@link clear} deletes **every** issuer slot: anything that
|
||||
* must act on the credentials before they are dropped (RFC 7009 revocation,
|
||||
* #2144) would otherwise see only the active issuer's and silently discard
|
||||
* the rest.
|
||||
*/
|
||||
listIssuers(serverUrl: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Clear all OAuth data for a server
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue