cloudflare / cloudflare/workers-oauth-provider

Bound total KV work when revoking existing grants during authorization

Open
#232 0 comments 0 reactions 0 assignees View on GitHub
v1
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.