Commit graph inspector/clients/cli/__tests__/relogin-revocation.test.ts
Author SHA1 Message Date
cliffhall
a4ee263365 fix(auth): refuse a grant whose authorization server cannot be established (#2144)
Copilot review round 15.

The issuer check still failed open for the legacy unkeyed grant. "Unkeyed"
means its authorization server is UNKNOWN, not that the cached endpoint is
proven to own it: server metadata is a single slot a later discovery
overwrites, while a pre-SEP-2352 token survives until the first issuer-stamped
save. So a clear could post an old bearer credential to an authorization server
that never minted it.

Such a grant is now cleared without being revoked, and reported as such with a
message saying re-authorizing makes future clears revocable. The cost is that
a pre-issuer-binding entry gets no revocation — which is exactly the behavior
it had before this feature existed, so nothing regresses — and it earns
revocation back on the next authorization. Not sending is the only answer that
cannot be wrong.

Every seed in the suite is now issuer-bound, which is what a real flow writes;
the two tests that were about the legacy grant now assert the refusal, since
that is the behavior worth pinning.

Also renamed the CLI test that still described the pre-round-13 ordering.

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 06:18:58 -04:00
cliffhall
4a24c4880d fix(auth): address Copilot review round 2 on the RFC 7009 revocation (#2144)
- `revokeStoredKeys`' pre-read of `getTokens` sat outside the best-effort
  catch. `getTokens` parses through `OAuthTokensSchema`, so a persisted token
  that no longer validates rejected and abandoned the local delete `--relogin`
  promises — over a grant that could not have been revoked anyway. Caught and
  retained as a failed outcome; the clear proceeds.
- The multi-key bookkeeping was wrong twice. A token is now marked spent only
  on a `revoked` outcome, so a failed or unsupported attempt no longer skips a
  duplicate entry under the other key that may hold the credentials or metadata
  that would have worked. And a failure now outranks an earlier success for
  reporting, so a stale grant left live cannot be hidden behind the first key's
  200.
- `--no-revoke` is now rejected without `--relogin`, ahead of the short-circuit
  returns, matching the parser's existing stance on accepted-but-inert flags
  (`--strict`). On its own it reads as "this run will not revoke anything",
  true only because nothing was being cleared.
- Extracted the App draft merge as `utils/serverWithDraftSettings`, so the rule
  has a test. The hook tests receive whatever they are handed and cannot tell a
  draft from a persisted entry, and `App.test.tsx`'s settings harness connects
  a stdio server where the OAuth section never renders.

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 01:47:48 -04:00
cliffhall
7f27d7e9c7 fix(auth): address Copilot review round 1 on the RFC 7009 revocation (#2144)
- RFC 8414 §2 defaults an omitted `revocation_endpoint_auth_methods_supported`
  to `client_secret_basic`; it does not inherit the token endpoint's list.
  Inheriting made metadata advertising only `client_secret_post` there send
  POST credentials to an endpoint that never advertised the method.
- The 5s timeout was inert on the web paths: `createRemoteFetch` re-issues the
  call as a POST to `/api/fetch` and drops `init.signal`, and the backend's
  outbound fetch gets none either, so a wedged AS could hold the teardown open
  indefinitely. A wall-clock race now enforces the deadline; the signal is kept
  because it genuinely cancels the direct-fetch paths.
- The settings-modal Clear read `settingsModalTarget`, which comes from the
  persisted `servers` list, while edits live in `settingsDraft` until the save
  debounce. Toggling the checkbox and clearing immediately used the previous
  value. It now merges the draft.
- `--relogin` clears both key spellings, so it now revokes from both rather
  than stopping at the first populated one — a stale entry under the other
  spelling is a live grant. The normalised key goes first, matching
  `findStoredServerState`, and a second key holding the same token is skipped.
- The fixture's /oauth/revoke now ENFORCES RFC 7009 §2.1 client
  authentication (both RFC 6749 §2.3.1 forms), so the e2e's claim that the
  Inspector authenticates correctly is actually tested; two negative cases
  assert a wrong secret and an unidentified client are refused.
- Dropped the `as unknown as` casts from the tests: a typed `InspectorLogger`
  double, and a spy on the real storage instead of a spread-and-cast.
- Added CLI-level tests through `runCli` for `--no-revoke`, the per-server
  `oauth.revokeOnClear`, and the stderr warning, and TUI tests for the
  forwarded opt-out and the rendered failure.

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 01:30:09 -04:00