inspector/core/auth/index.ts
cliffhall 781aa69d19 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>
2026-08-28 02:40:28 -04:00

156 lines
3.8 KiB
TypeScript

// Types
export type {
OAuthStep,
AuthProtocol,
OAuthClientRegistrationKind,
MessageType,
StatusMessage,
OAuthFlowState,
OAuthConnectionState,
CallbackParams,
} from "./types.js";
export {
EMPTY_OAUTH_FLOW_STATE,
authProtocolFromEnterpriseManaged,
} from "./types.js";
export {
buildOAuthConnectionState,
hasPersistedOAuthServerState,
isServerOAuthConfigured,
protocolFromOAuthConfig,
} from "./connection-state.js";
export type { BuildOAuthConnectionStateParams } from "./connection-state.js";
export { ensureCimdClientRegistration } from "./cimd.js";
export { mcpAuth, type McpAuthOptions, type McpAuthResult } from "./mcpAuth.js";
export { computeScopeUnion, isStrictScopeSuperset } from "./scopes.js";
// Custom authorization-request parameters (#2018)
export {
RESERVED_AUTHORIZATION_PARAMS,
isReservedAuthorizationParam,
authorizationParamKeyError,
applyAuthorizationParams,
} from "./authorizationParams.js";
export type { ReservedAuthorizationParam } from "./authorizationParams.js";
// Authorization/token endpoint overrides (#1906)
export {
oauthEndpointUrlError,
normalizeOAuthEndpointOverrides,
isAuthorizationServerMetadata,
applyOAuthEndpointOverrides,
withOAuthEndpointOverrides,
} from "./endpointOverrides.js";
export type { OAuthEndpointOverrides } from "./endpointOverrides.js";
// Storage
export type {
OAuthStorage,
IdpSessionState,
SaveClientInformationOptions,
} from "./storage.js";
export { getServerSpecificKey, OAUTH_STORAGE_KEYS } from "./storage.js";
// Providers
export type {
OAuthProviderConfig,
RedirectUrlProvider,
OAuthNavigation,
OAuthNavigationCallback,
} from "./providers.js";
export {
MutableRedirectUrlProvider,
ConsoleNavigation,
CallbackNavigation,
BaseOAuthClientProvider,
} from "./providers.js";
// Utilities
export {
parseHttpUrl,
parseOAuthCallbackParams,
generateOAuthState,
parseOAuthState,
generateOAuthErrorDescription,
formatOAuthFailureDetail,
isUnauthorizedError,
} from "./utils.js";
export type {
AuthChallenge,
AuthChallengeReason,
AuthChallengeOutcome,
HandleAuthChallengeOptions,
ParseAuthChallengeContext,
WwwAuthenticateBearerParams,
} from "./challenge.js";
export {
AuthChallengeError,
AuthRecoveryRequiredError,
parseAuthChallengeFromError,
parseAuthChallengeFromResponse,
parseScopeString,
parseWwwAuthenticateBearer,
unionAuthorizationScopes,
isAuthChallengeError,
isConnectAuthRecoveryError,
findNestedAuthError,
EMA_STEP_UP_PENDING_URL,
} from "./challenge.js";
export {
isStandardOAuthStepUp,
isEmaStepUp,
isStepUpConfirmation,
stepUpConfirmMessage,
stepUpFollowUpMessage,
stepUpModalTitle,
stepUpAuthorizeActionLabel,
emaStepUpInProgressMessage,
emaStepUpSuccessMessage,
emaStepUpFailureMessage,
stepUpAdditionalScopes,
stepUpInsufficientScopeMessage,
oauthPreRedirectToastCopy,
isReAuthBannerReason,
reAuthBannerMessage,
lostAuthorizationStateTitle,
lostAuthorizationStateMessage,
lostAuthorizationStateActionLabel,
issuerMismatchTitle,
issuerMismatchMessage,
issuerBindingFailureCopy,
type OAuthInteractiveAuthKind,
} from "./oauthUx.js";
export {
findIssuerBindingFailure,
type IssuerBindingFailure,
} from "./issuerBinding.js";
// Discovery
export { discoverScopes } from "./discovery.js";
// RFC 7009 token revocation (#2144)
export {
DEFAULT_REVOCATION_TIMEOUT_MS,
aggregateOutcomes,
buildRevocationRequest,
revocationAuthMethods,
revokeStoredOAuthTokens,
revokeToken,
selectRevocableToken,
} from "./revocation.js";
export type {
RevocationRequestParams,
RevokeStoredOAuthTokensParams,
RevokeTokenParams,
TokenRevocationOutcome,
TokenRevocationSkipReason,
} from "./revocation.js";
// Logging (re-exported from core/logging)
export { silentLogger } from "../logging/index.js";