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
|
|
@ -28,6 +28,14 @@ interface AuthTabProps {
|
|||
inspectorClient: InspectorClient | null;
|
||||
oauthStatus: "idle" | "authenticating" | "error";
|
||||
oauthMessage: string | null;
|
||||
/**
|
||||
* How to colour {@link oauthMessage} on the `idle` status. Defaults to
|
||||
* `"info"` (cyan). `"warning"` exists for the one message that reports a
|
||||
* *partial* success: the OAuth state really was cleared, so this is not an
|
||||
* error, but the grant may still be live at the authorization server and
|
||||
* cyan would understate that (#2144).
|
||||
*/
|
||||
oauthMessageTone?: "info" | "warning";
|
||||
oauthRevision: number;
|
||||
pendingStepUp?: {
|
||||
challenge: AuthChallenge;
|
||||
|
|
@ -57,6 +65,7 @@ export function AuthTab({
|
|||
inspectorClient,
|
||||
oauthStatus,
|
||||
oauthMessage,
|
||||
oauthMessageTone = "info",
|
||||
oauthRevision,
|
||||
pendingStepUp,
|
||||
onAuthorizeStepUp,
|
||||
|
|
@ -190,7 +199,9 @@ export function AuthTab({
|
|||
<Text color="red">{oauthMessage}</Text>
|
||||
)}
|
||||
{oauthStatus === "idle" && oauthMessage && (
|
||||
<Text color="cyan">{oauthMessage}</Text>
|
||||
<Text color={oauthMessageTone === "warning" ? "yellow" : "cyan"}>
|
||||
{oauthMessage}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{pendingStepUp ? (
|
||||
|
|
|
|||
Loading…
Reference in a new issue