Skip to content

fix: attempt-scoped flow limits, and login_methods on the registration email OTP path - #486

Merged
Bccorb merged 3 commits into
mainfrom
fix/flow-limits-and-registration-otp
Oct 23, 2026
Merged

Bccorb merged 3 commits into
mainfrom
fix/flow-limits-and-registration-otp

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Closes #468. Closes #470.

#468: limits and links belong to the attempt

  • The OTP and magic link identity limiters key perIdentity on the identifier plus the ephemeral token's jti (or the requester's IP when there is none), so another attempt for the same address keeps its own allowance.
  • All attempts for one identifier together are capped at 4x perIdentity (20 per window by default), so minting fresh attempts cannot message an address without bound. Hardcoded multiplier rather than a new config key, to avoid a types release; FLOW_RATE_LIMITS keeps its shape.
  • POST /magic-link expires only the same attempt's earlier links.
  • docs/security-posture.md (new "Code and link sends per address") and docs/configuration.md.

#470: registration email OTP honours login_methods

  • /otp/verify-email-otp and /otp/generate-email-otp answer 403 login_method_disabled for an already verified account when email_otp is off (this also covers the passkey-required rule). Unverified accounts still get their registration code.
  • accountVerification: true only for an actual unverified-to-verified change. Decoy responders refuse the same way.

For review

  • With email_otp off, the 403 on the registration path tells a caller a verified account exists. That was already true for passkey-required and phishing-resistant-only accounts; the alternative is a silent 200 with no send.
  • /registration/register still mails an (unusable for sign-in) code to an existing verified account when email_otp is off. Left as is.
  • With default limits, magicLink.perIp (20) equals the cross-attempt cap, so one network location can still reach the magic link cap for an address; documented.
  • A successful magic link sign-in does not expire other attempts' pending links (cross-device use still needs confirmation).

Checks

typecheck, lint, prettier, build, coverage (142 files, 2003 tests; statements 98.9%, branches 95.41%). Tests added fail with the src changes reverted (10 for #470, 18 for #468).

verify-email-otp and generate-email-otp answer 403 login_method_disabled for a verified account

unless email_otp is a login method. accountVerification is set only for an actual change.

Decoys mirror the refusal. Closes #470.
…piry to the attempt

A stranger who knows an address can no longer use up or void the owner's codes and links.
@Bccorb
Bccorb merged commit 83b1726 into main Oct 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant