fix(auth): bound OTP emails per recipient - #39
Open
johnhooks wants to merge 1 commit into
Open
Conversation
johnhooks
marked this pull request as ready for review
August 4, 2026 20:33
This was referenced Aug 4, 2026
An account that signs up and abandons email verification leaves a working credential behind. Posting those credentials to the sign-in endpoint made WorkOS mail a fresh verification code every time, so anyone holding them could drive unlimited mail at the address they registered. The only email bound on that route was 5 per minute, which is sustainable indefinitely, and the transient counter behind it was non-atomic, so a concurrent burst passed straight through. Sends are now charged against the recipient rather than only the requester. WorkOS mails the code as a side effect of authenticating and reports it afterwards, so the sign-in path reserves a slot before the call and refunds it when the outcome shows nothing went out; failed passwords and completed logins cost a recipient nothing. Magic-code send and password-reset start share one recipient bucket, so rotating between endpoints buys no extra mail. Send limits are layered over 5 minute, 1 hour, and 8 hour windows. A single fixed window is burstable across its boundary by construction, and the longer tiers are what cap a day's volume. Request limits stay at one 60 second window, since a daily cap on sign-in attempts would lock out users who mistype a password. Rate limiting also moves out of the AuthKit package into its own domain: a single-window interface with transient and object-cache implementations, plus a wrapper that stacks windows into a policy. Sites with a persistent object cache now get atomic counters, which closes the concurrent-burst bypass; sites without keep the read-modify-write path, bounded by the longer tiers. The change_email_rate_limit_* options are removed. Limits are class constants applied to every path that mails a caller-chosen address, so one policy now governs sign-in, magic code, password reset, and email change. Previously stored values are inert. Adds docs/rate-limiting.md covering the model, the per-endpoint rules, and what to do when adding a route that sends email.
johnhooks
force-pushed
the
refactor/rate-limiter-2
branch
from
August 4, 2026 20:37
75d6b2b to
26bc07f
Compare
This was referenced Aug 4, 2026
bordoni
approved these changes
Aug 5, 2026
redscar
reviewed
Aug 5, 2026
redscar
approved these changes
Aug 5, 2026
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.
An account that signs up and abandons email verification leaves a working credential behind. Posting those credentials to
POST /auth/password/authenticatemade WorkOS mail a fresh verification code every time, so anyone holding them could drive unlimited mail at the address they registered. The only email bound on that route was 5 per minute, sustainable indefinitely, and the transient counter behind it was non-atomic, so a concurrent burst passed straight through. This branch caps mail by who receives it, not just by who asks.Changes
WorkOS\RateLimit: a single-window interface with transient and object-cache implementations, plus a wrapper that stacks windows into a policy. Sites with a persistent object cache get atomic counters, which closes the concurrent-burst bypass; sites without keep the read-modify-write path, bounded by the longer tiers.retry_afteris arithmetic.Behavior changes to note
change_email_rate_limit_*options are removed. Limits are class constants applied to every path that mails a caller-chosen address, so one policy governs sign-in, magic code, password reset, and email change. Previously stored values are inert. Change-email initiate moves from 3 per hour to the shared send policy.Signup's verification mail deliberately stays off the send policy: a duplicate
create_userfails at WorkOS before the send is reached, so it isn't repeatable per address. Details indocs/rate-limiting.md.This is an alternative to
refactor/rate-limiter, which answers the same report with a larger rewrite (separate requester and recipient limiters over a pluggable counter store, an injected clock, and reservation objects). This branch reuses the existing limiter shape and stays closer to what is already deployed.