Repository navigation
fix(2fa): issue MFA codes with two_factor.otp_lifetime, not the client's passwordless lifetime #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
932b38e
a7e0e77
a478328
cdc5451
7d587a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import React, {useState, useEffect} from 'react'; | ||
| import React, {useState, useEffect, useRef} from 'react'; | ||
| import Button from '@material-ui/core/Button'; | ||
| import Link from '@material-ui/core/Link'; | ||
| import FormControlLabel from '@material-ui/core/FormControlLabel'; | ||
|
|
@@ -26,13 +26,19 @@ const TwoFactorForm = ({ | |
|
|
||
| const {secondsLeft, expired} = useOtpCountdown(otpLifetime, codeVersion); | ||
| const [cooldown, setCooldown] = useState(0); | ||
| // A failed resend may resolve after the parent swapped this form out | ||
| // (session expired -> password flow); don't touch state once unmounted. | ||
| const mounted = useRef(true); | ||
|
|
||
| // 1s ticker for the resend cooldown (the expiry countdown lives in the hook). | ||
| useEffect(() => { | ||
| const timer = setInterval(() => { | ||
| setCooldown(prev => (prev > 0 ? prev - 1 : 0)); | ||
| }, 1000); | ||
| return () => clearInterval(timer); | ||
| return () => { | ||
| clearInterval(timer); | ||
| mounted.current = false; | ||
| }; | ||
| }, []); | ||
|
|
||
| const handleSubmit = (ev) => { | ||
|
|
@@ -46,10 +52,14 @@ const TwoFactorForm = ({ | |
| if (cooldown > 0 || disableInput) return; | ||
| setCooldown(RESEND_COOLDOWN_SECONDS); | ||
| // A successful resend resets the expiry countdown through the parent's | ||
| // codeVersion bump; failures are surfaced by the parent as well. | ||
| // codeVersion bump. A failed one is surfaced inline by the parent, so | ||
| // release the link right away instead of locking it for a cooldown | ||
| // that only makes sense when a code was actually sent. | ||
| const result = onResend(); | ||
| if (result && typeof result.catch === 'function') { | ||
| result.catch(() => {}); | ||
| result.catch(() => { | ||
| if (mounted.current) setCooldown(0); | ||
|
Comment on lines
+60
to
+61
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid scenario, leaving it as is. It needs a resend request that is still pending after the 30-second cooldown ran out (the XHR timeout is 60 s), a second click, and the abort of the first request landing while the second is in flight. The worst outcome is the link becoming clickable a few seconds early while the server-side rate limit (5 per 15 min) still bounds the resends. The screen state the second request produces (200 with a fresh countdown, or an inline error) replaces the transient one within seconds. If this shows up in practice, the fix is to keep the latest resend promise in a ref and release the cooldown only when the rejected promise is that one. |
||
| }); | ||
| } | ||
| }; | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.