Skip to content

fix(server): keep account closure going past one bad grant or request - #868

Merged
osanderson merged 1 commit into
mainfrom
fix/registry-inbox-robustness
Nov 15, 2026
Merged

osanderson merged 1 commit into
mainfrom
fix/registry-inbox-robustness

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Fixes the CIBA Lows 1, 2 and 5 and the grant-registry Lows 2, 3 and 4 from the v0.74.0 security review, plus three Info items. It doesn't depend on #866 or #867.

Changes

Finding Before After
CIBA lookup during a store outage LookupBackchannelInteraction answered it as invalid_request, so the inbox showed empty and DenyBackchannelInteractions returned (0, nil) An outage is server_error; a client no longer registered is unauthorized_client (as LookupInteraction answers)
One bad entry An unreadable record, or a pending request from a deregistered client, failed the whole listing, and with it erasure, until it expired A deregistered client's request leaves the inbox. RevokeSubjectGrants / DenyBackchannelInteractions act on every readable entry, carry on past failures, and report every failure joined, with the count. RevokeListedGrant revokes a readable grant whatever the others' state
Code reuse Revoked the refresh token, but its registry entry stayed listed The entry is removed too
Stale CIBA entry A decided request whose removal failed stayed listed until expiry DenyBackchannelInteractions clears it when it finds the request already decided
Forged or moved entries Not cross-checked A grant entry whose refresh-token hash doesn't derive its key isn't listed, so RevokeListedGrant can't be steered at another token. An inbox entry whose request is bound to another subject than the one it's filed under isn't listed
Docs Overclaimed Dependencies.BackchannelInbox now says a handle can still end a request for anyone; only approval is bound to the user. RevokeSubjectGrants and GETTING_STARTED now say that account closure misses grants still in flight (an unexchanged code, an unpolled CIBA approval): refuse the subject at sign-in, and revoke again once those lifetimes pass

ListGrants and ListBackchannelInteractions still fail on an unreadable entry rather than hide it. Only the bulk revoke and deny calls carry on past one.

Tests

  • Code reuse: removes the reused code's registry entry.
  • Bulk revoke: RevokeSubjectGrants revokes the readable grant past an unreadable entry, and attempts every grant when deletes fail. RevokeListedGrant works past an unreadable entry.
  • Forged entries: mismatched-hash entries aren't listed or revocable.
  • CIBA:
    • a deregistered client's request is skipped;
    • a backchannel-store outage fails List and Deny;
    • Deny clears a stale decided entry;
    • a moved inbox entry isn't listed;
    • LookupBackchannelInteraction's error codes cover an unregistered client, an unavailable client store, an unreadable request and an unavailable backchannel store.
  • Updated tests: the crafted registry entries in the tests now derive their keys as the server does (server.GrantEntryKey, from a test-only export_test.go).
  • Checks: all modules pass at Go 1.26.11; golangci-lint is clean.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Nov 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

From the v0.74.0 security review:

- LookupBackchannelInteraction answered a backchannel store outage as
  an unknown handle, so ListBackchannelInteractions showed an empty
  inbox and DenyBackchannelInteractions reported (0, nil) during one.
  An outage is now server_error, and a request whose client is no
  longer registered unauthorized_client, as LookupInteraction answers.
- One bad entry failed the whole listing, and with it
  RevokeSubjectGrants and DenyBackchannelInteractions, for as long as
  it lived: an unreadable record, or a pending request from a
  deregistered client. A deregistered client's request now leaves the
  inbox. RevokeSubjectGrants and DenyBackchannelInteractions act on
  every readable entry and carry on past one that fails, then report
  every failure; RevokeListedGrant revokes a readable grant whatever
  the others' state. ListGrants and ListBackchannelInteractions still
  fail on an unreadable entry rather than hide it.
- Reusing an authorization code now also removes its grant from the
  registry, and DenyBackchannelInteractions clears an entry whose
  request it finds already decided.
- A grant entry whose refresh token hash doesn't derive its key, or an
  inbox entry whose request is bound to another subject than the one
  it's filed under (as a database writer could arrange), isn't listed.
- Docs: a handle can end a CIBA request for anyone, though only its
  user can approve it; and account closure misses grants still in
  flight (an unexchanged code, an unpolled CIBA approval), so refuse the
  subject at sign-in and revoke again once their lifetimes pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@osanderson
osanderson force-pushed the fix/registry-inbox-robustness branch from 9e1364a to 74e068b Compare November 15, 2026 16:52
@sonarqubecloud

Copy link
Copy Markdown

@osanderson
osanderson merged commit d296a72 into main Nov 15, 2026
31 checks passed
@osanderson
osanderson deleted the fix/registry-inbox-robustness branch November 15, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant