Skip to content

Add drift tolerance to one time password checks - #9437

Open
gbp wants to merge 1 commit into
developfrom
fix-two-factor-transient-test-error
Open

Add drift tolerance to one time password checks#9437
gbp wants to merge 1 commit into
developfrom
fix-two-factor-transient-test-error

Conversation

@gbp

@gbp gbp commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Codes were verified with no drift, so one was rejected the instant its 30 seconds window ended.

RFC 6238 recommends comparing against past timestamps within the transmission delay, allowing at most one step.

Default the drift to 15 seconds - half a step. so we accepts the code that has just expired for the first 15 seconds of the next step.

Nothing becomes replayable: authenticate_totp still passes otp_last_used_at as after, so ROTP drops any step at or before the one already used, and OtpRateLimit still caps repeat attempts.

This fixes flaky tests where codes rolled over into the next window.

Codes were verified with no drift, so one was rejected the instant its
30 seconds window ended.

RFC 6238 recommends comparing against past timestamps within the
transmission delay, allowing at most one step.

Default the drift to 15 seconds - half a step. so we accepts the code
that has just expired for the first 15 seconds of the next step.

Nothing becomes replayable: authenticate_totp still passes
otp_last_used_at as `after`, so ROTP drops any step at or before the
one already used, and OtpRateLimit still caps repeat attempts.

This fixes flaky tests where codes rolled over into the next window.
@gbp
gbp requested a review from chrismytton August 6, 2026 09:28

@chrismytton chrismytton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Just one suggestion for a potential code simplification, but no blockers.

Comment thread spec/models/user_spec.rb
Comment on lines +1122 to +1126
def expiring_code
code = nil
travel_to(period_end - 1.second) { code = user.otp_code }
code
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like #travel_to returns its block's value, so this can be simplified a bit.

Suggested change
def expiring_code
code = nil
travel_to(period_end - 1.second) { code = user.otp_code }
code
end
def expiring_code
travel_to(period_end - 1.second) { user.otp_code }
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants