Distinguish OAuth account-link rate-limited retries from genuine sends - #3567
Merged
Freika merged 1 commit intoSep 10, 2026
Conversation
Freika
deleted the
detail/bug-fix/distinguish-oauth-account-link-rate-limited-retrie-a7050c
branch
September 10, 2026 20:30
pull Bot
pushed a commit
to AmirulAndalib/dawarich
that referenced
this pull request
Sep 13, 2026
Follow-up to Freika#3567, which merged before these review fixes landed. The rate-limit cache held a boolean, so a retry fifty-five minutes into the window was still told to wait a full hour. It now stores the send time and the exception carries the remaining seconds, clamped to the window. The web fallback and the API controllers share one helper for acquiring the limit, so a send from either path is accounted the same way, and the new 429 is described in the rswag specs so it reaches the generated contract that the mobile client reads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detail bug report: View on Detail
Bug
OAuth account-link verification emails suppressed by the per-account rate-limit cache were reported to users as if they had been sent.
Auth::FindOrCreateOauthUser#send_verification_emailrate-limits viaRails.cache.write(..., unless_exist: true), which returnsfalsewhen the key already exists. Thatfalsesentinel was computed and then discarded —send_verification_emailreturnednilon rate-limit,handle_email_collisionraisedLinkVerificationSentunconditionally, and callers couldn't tell a suppressed send from a genuine one:Auth::AccountLinksController#email_fallbackflashed the affirmative "We sent a confirmation link to X" notice even when no mailer job was enqueued.Api::V1::Auth::{Google,Apple}ControllerreturnedHTTP 202 error: 'verification_sent'for both genuine sends and rate-limited retries.So every rate-limited retry misled the user. A narrow but real subset of web-flow users (email-link path, 15min < t < 1hour after the original send, original link unclicked past its TTL, original email inaccessible, password forgotten) were fully stranded with no accurate recovery signal.
Fix
Propagate the rate-limit state to callers and branch the user-facing message on it.
Auth::FindOrCreateOauthUser::LinkVerificationSentnow carries arate_limited:attribute (defaultfalse).send_verification_emailreturns:sentor:rate_limited;handle_email_collisionuses that sentinel to setrate_limitedon the exception.Auth::AccountLinksController#email_fallbackflashes the existingconfirmation_link_sentnotice only when a job was actually enqueued; otherwise it flashes a newaccount_link_rate_limitedalert.Api::V1::Auth::{Google,Apple}Controllerbranch one.rate_limited: genuine send →HTTP 202 error: 'verification_sent'; rate-limited retry →HTTP 429 error: 'verification_rate_limited'with aRetry-After: 3600header.This does not regress the enumeration posture: account existence is already disclosed on the first collision response, and the rate-limit state is self-known to the requester (their own prior request wrote the cache key). A distinct
verification_rate_limitedresponse leaks nothing new — unlike the Devise paranoid pattern this isn't an anonymous probe.Testing
LinkVerificationSent.rate_limitedisfalseon genuine sends and:raise_onlyflows andtrueon rate-limited retries, that the webemail_fallbackflashes the notice vs. the distinct alert correctly, and that the API controllers return429withRetry-After: "3600"and no second mailer on the rate-limited retry.from_omniauthraise-only flow, the password-challenge#confirmpath (still links despite the rate-limit), and the expired-tokenAuth::VerifyAccountLinkTokenrejection. Swagger contracts for the Google/Apple auth endpoints still generate.rails zeitwerk:checkandrubocopon the changed files are clean.End-to-end smoke verification (not versioned — the Google/Apple id_token verifiers require real Google/Apple-issued tokens, so the verifiers were stubbed in-process while a real Puma server handled the HTTP requests):
127.0.0.1:33333, and issued realNet::HTTP::Postrequests. First Google/Apple collision within the hour →HTTP 202 error: 'verification_sent', noRetry-After. Second collision →HTTP 429 error: 'verification_rate_limited',Retry-After: "3600", existing user'sprovider/uidunchanged.ActionDispatch::IntegrationTestdrove the full Rack stack via OmniAuth test-mode and the real/users/auth/openid_connect/callbackroute. Genuineemail_fallback→ notice set, alert nil, 1 mailer enqueued; rate-limited retry → notice nil, distinct alert set, 0 mailers;#confirmlinked the identity even with the rate-limit key pre-seeded; clicking the originally-emailed link after its 15-min TTL showed "Link invalid or expired." and the password challenge still linked afterward.Targeted at
devper the repo's contributing guide. No issue number was provided for reference.Automatic Fixes PRs can be configured here.