This issue is related to Gmail's safe-links protection feature, which scans links in emails for security purposes.
Root CauseWhen Gmail scans the password reset button/link, it fires two GET requests to the /password/change endpoint:
One request from Gmail's link scanner One request from the actual user clickEach request triggers the MFA code generation, resulting in two codes being sent.
Why FusionAuth's Normal Protections May Not ApplyFusionAuth has built-in protections against email link scanners consuming codes or triggering duplicate actions. Starting with version 1.49.0, FusionAuth introduced a mandatory confirmation page that requires user interaction when a link request doesn't originate from the same browser that initiated the workflow. This prevents most link scanners (including aggressive ones like Outlook Safe Links) from consuming codes.
However, this protection may not engage if:
Custom theme is missing the confirmation template — If you're using a custom theme, you need to copy the "Confirmation required" template (/confirmation-required) into your theme to benefit from this feature. Without it, the protection doesn't work. The user recently changed their password and has "confirmation trust" already established in their browser A proxy between the client and FusionAuth is not properly handling cookies that prevent link checker issues The state parameter is being mangled in a custom theme, causing unexpected behavior SolutionsAdd the Confirmation Required Template to Your Custom Theme — This is likely the primary fix. The confirmation required template was added in version 1.49.0 and must be manually added to custom themes. Copy it from the default theme to ensure link scanners are blocked from triggering duplicate MFA codes.
Upgrade to the Latest Version of FusionAuth, which includes the most recent fixes for link scanner issues
Review Custom Theme Modifications — Compare your theme files against the stock FusionAuth theme to ensure the state parameter and form structure haven't been altered in ways that bypass protections
Check Proxy Configuration — Ensure any proxy in front of FusionAuth is correctly forwarding cookies and not causing duplicate page renders
Test with Default Theme — Verify the issue doesn't occur with default templates to isolate whether customizations are the cause (you've already confirmed this resolves the issue)
Why Subsequent Attempts WorkAfter the first password reset attempt, browser state and cookies are properly established, preventing the duplicate request issue on subsequent attempts from the same session.
Why Button vs Link MattersThe difference between button and link behavior may be related to how your custom theme implements these elements. Buttons might use different form submission methods or lack the postMethod=true parameter that converts GET requests to POST requests (which link scanners typically don't replicate).
Related Documentation Confirmation Required Template Variables — Details on the confirmation required template introduced in version 1.49.0 Email Link Workflow Improvements (FusionAuth 1.49) — Explains the confirmation page feature designed to prevent link scanners from consuming codes FusionAuth Cookies Reference — Information about cookies used by FusionAuth's hosted login pages GitHub Issue #2253: Email MFA codes sent multiple times during password resets — Related issue about duplicate MFA codes Community Discussion: Password Reset Expired Link - Safelinks Users — Discussion about link checker issues with password reset flows