Skip to content

Sweep dynamic registrations nobody authorized after a day - #3255

Merged
jeremy merged 1 commit into
mainfrom
oauth-prune-unauthorized-registrations
Oct 29, 2026
Merged

jeremy merged 1 commit into
mainfrom
oauth-prune-unauthorized-registrations

Conversation

@jeremy

@jeremy jeremy commented Oct 29, 2026

Copy link
Copy Markdown
Member

Answers the dynamic-registration volume finding on #3247 (thread).

POST /oauth/clients is unauthenticated and limited to 10 a minute per address. Each registration can hold ten 2,000-character redirect URIs, and a registration nobody authorized was kept for UNUSED_RETENTION, 30 days. This shortens that one case to 24 hours, so the rows such registrations can pile up are bounded by the registration rate over a day rather than a month.

What counts as never authorized

Oauth::Client.never_authorized: a dynamically registered client whose updated_at still equals its created_at, and which holds no access token (stale keeps its where.missing(:access_tokens)). Nothing has touched the row since registration wrote it. The code already touches a client:

  • when the user approves consent, before the code is issued (Oauth::AuthorizationsController#create);
  • when a grant is created or destroyed (Identity::AccessToken#touch_oauth_client).

So a client that ever got a code or a grant, including one whose grant has since been revoked or lapsed, has updated_at > created_at and keeps the 30-day rule counted from its last grant activity, as before. Denying consent touches nothing, so a denied registration lapses after a day too.

If some future write updates a dynamically registered client, it moves updated_at, so the client falls back to 30 days. That errs toward keeping rows, never toward deleting one.

What's untouched

  • Metadata document (CIMD) clients were already swept after 30 days unused, and still are. They aren't created through POST /oauth/clients, so they keep 30 days.
  • Trusted, first-party and operator-provisioned clients (dynamically_registered: false) were never swept and still aren't. That includes the personal access issuer.
  • The lock-and-recheck in destroy_if_still_unused re-runs the whole stale test, so a consent or grant that lands between picking a client and destroying it still spares it.

Schedule

cleanup_oauth_clients moves from daily (04:12) to hourly (minute 14), so a never-authorized registration lives at most about 25 hours. The sweep scans oauth_clients, which this change keeps small.

Accepted loss

If someone registers and is still on the consent screen when the registration turns 24 hours old, their approval gets "Unknown client". MCP clients register again on reconnect, so the cost is one retry.

Not doing: an account-wide or global registration budget

A global cap swaps a storage problem for an availability one. Someone registering from many addresses could use it up, and then real Claude or Cursor users couldn't register at all, which is worse than some extra rows. With the per-address limit and a one-day life, the row count is already bounded by the request rate, and the rows are small and grant nothing without a user's consent.

Tests

  • A never-authorized registration 25 hours old is swept, and one 23 hours old is kept.
  • After a day, these are all kept: a registration with a live grant, one whose grant came and went, and one with approved consent.
  • After a day, these unauthorized clients are all kept: trusted, operator-provisioned, metadata document, the trusted fixture and the personal access issuer.
  • Integration: approving consent takes a 25-hour-old registration out of stale.
  • One existing test changed. Its "recent" client registered 29 days ago and was never touched, so the new rule now sweeps it. It now registers 30 days ago and gets touched 29 days ago.

Each new test was checked by breaking what it covers: dropping the updated_at = created_at test, dropping the dynamically_registered restriction, setting the retention to 30 days, setting it to 22 hours, and dropping where.missing(:access_tokens). Each break made at least one test fail. The OAuth model and integration tests pass on SQLite and MySQL 8.4. Nothing tests the hourly schedule.

Registration is unauthenticated and limited only per address, so its rows
were bounded by ten a minute per address over thirty days. A registration
whose row nothing has touched since it was written, no consent approved and
no grant issued or ended, now lapses after 24 hours, and the sweep runs
hourly so that holds. Anything that was ever authorized, metadata document
clients and operator-provisioned clients keep the rules they had.
@jeremy
jeremy requested a balanced review from Copilot October 29, 2026 02:42
@jeremy

jeremy commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

Please review 6c87529, the code and the security review both.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T05:15:19.318788Z 6c87529 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-29T05:16:07.289689Z 6c87529 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

No active review issues block the change to shorten retention for unused OAuth registrations.

No open issues 🎉

What changed in this PR

Reduces storage growth from unused OAuth registrations by shortening retention for never-authorized dynamic clients from 30 days to 24 hours, while preserving existing retention and exemptions for other clients.

Changes:

  • Adds a shorter retention rule for untouched dynamic registrations with no access tokens.
  • Runs OAuth client cleanup hourly instead of daily.
  • Adds model and integration coverage for expiry, consent, grants, and exempt clients.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
test/​models/​oauth/​client_test.rb Tests one-day expiry and preservation of authorized and exempt clients.
test/​integration/​oauth_flow_test.rb Verifies consent removes a registration from short-retention cleanup eligibility.
config/​recurring.yml Schedules OAuth client cleanup hourly at minute 14.
app/​models/​oauth/​client.rb Adds the 24-hour retention rule and incorporates it into stale-client selection.

Review effort: Balanced

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 6c87529a73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 6c87529a73

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@jeremy
jeremy marked this pull request as ready for review October 29, 2026 05:14
@jeremy
jeremy merged commit e3d6f17 into main Oct 29, 2026
16 of 18 checks passed
@jeremy
jeremy deleted the oauth-prune-unauthorized-registrations branch October 29, 2026 05:14
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.

2 participants