cloudflare / cloudflare/workers-oauth-provider
Bound total KV work when revoking existing grants during authorization
- Dominant language
- TypeScript
- Stars
- 1.9k
- Forks
- 134
- PR merge metrics
- No merged PRs in 30d
Description
## Problem
PR #231 improves `completeAuthorization()` by adding `revokeExistingGrantsBatchSize`, which bounds the number of grant keys fetched per KV `list()` page while revoking existing grants for the same user+client.
That is useful, but it does **not** fully solve the operational problem that motivated the PR: a user with many grants can still cause one authorization request to scan every grant for that user and perform one KV read per grant summary.
Today, grants are keyed as:
```text
grant:${userId}:${grantId}
```
To revoke old grants for the same user+client, `completeAuthorization()` calls `listUserGrants(userId)`, pages through `grant:${userId}:`, reads each grant record, then filters by `clientId` in application code.
That means total work is still O(number of grants for user), not O(number of grants for user+client), and it can still exceed Worker/KV subrequest budgets for high-cardinality users. PR #231 only reduces per-page fanout; it does not bound total work per invocation.
## Why this matters
`completeAuthorization()` runs in the interactive authorization path. If a user has thousands of grants, one re-authorization can still:
- perform thousands of KV reads in a single Worker invocation
- risk Cloudflare subrequest limits
- increase latency on the authorization redirect path
- potentially fail before old grants are revoked, leaving stale tokens valid
This is especially relevant because `revokeExistingGrants` defaults to `true` to prevent stale `props` from causing re-auth loops.
## Current behavior after PR #231
Improved:
- each KV list page defaults to 50 keys
- custom page sizes are validated as positive integers
- page sizes above KV's 1000-key maximum are clamped
Still not solved:
- the loop still walks all pages for `grant:${userId}:`
- each grant key still requires a KV `get()` to inspect `clientId`
- revocation candidates are collected before the new grant is written, then all matching old grants are revoked afterward
- a large number of old matching grants can still trigger many revocations in one invocation
## Possible approaches
### Option 1: Add a secondary user+client grant index
Maintain an additional key per grant that can be listed by user+client, for example:
```text
grant-client-index:${userId}:${clientId}:${grantId}
```
Then `completeAuthorization()` can list only grants for the exact user+client pair and avoid scanning grants for other clients.
Things to design:
- write index entries whenever grants are created
- delete index entries when grants are revoked or purged
- handle index entries whose grant record has expired or is missing
- decide whether index entries need TTLs matching grant TTLs
- preserve behavior for existing grants created before the index exists
Migration/backward compatibility options:
- best-effort fallback to the old user-wide scan only when no index entries are found
- dual path for one release: scan old format and create/delete index entries opportunistically
- accept that old pre-index grants may need purge/cleanup before the bound is fully effective
Because this changes KV storage shape, release planning should consider the repo guidance around schema changes and token/grant compatibility.
### Option 2: Make cleanup bounded and resumable
Keep the current key shape, but cap the amount of scan/revocation work done during one `completeAuthorization()` call and continue cleanup later.
Possible mechanisms:
- store a pending cleanup cursor/job in KV
- expose a helper method or scheduled task to continue cleanup
- use `ctx.waitUntil()` where available for post-response cleanup, while still respecting invocation/subrequest limits
- make the authorization path create the new grant promptly, then perform bounded best-effort old-grant revocation
Things to design:
- stale tokens may remain valid until cleanup catches up
- failure/retry semantics for cleanup jobs
- idempotency across repeated authorizations
- observability/result reporting for incomplete cleanup
### Option 3: Change grant storage to make user+client uniqueness direct
If the intended invariant is at most one active grant per user+client, consider storing current grants under a deterministic user+client key instead of random grant IDs, or keeping a current-grant pointer.
This is likely the largest behavior/storage change and needs careful analysis because existing tokens encode grant IDs and refresh token behavior depends on grant records.
## Acceptance criteria
A complete fix should make `completeAuthorization()` revocation work bounded in total, not only per KV page.
Suggested criteria:
- Re-authorizing a user with thousands of grants for other clients does not scan/read all of those unrelated grants.
- The number of KV operations in one authorization request is bounded by a documented constant or by the number of grants for the same user+client, not total grants for the user.
- Existing stale grants for the same user+client are still revoked reliably, or any delayed/best-effort cleanup semantics are explicitly documented and tested.
- Tests cover high-cardinality users with many unrelated client grants.
- Tests cover multiple old grants for the same user+client.
- Tests cover missing/expired grant records and stale index entries if an index is added.
- README/API docs explain any new cleanup/index behavior that users need to know about.
- Storage schema documentation is updated if new KV keys are introduced.
## Related work
- Follow-up from #231
Contributor guide
Research direction
Start at completeAuthorization() and listUserGrants(), then review PR #231 and the repository guidance for grant and token compatibility. Compare the proposed indexing and bounded-cleanup approaches against the acceptance criteria, including high-cardinality users, multiple matching grants, missing records, and documented storage or cleanup behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authorization, cloud
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100