Skip to content

fix(auth-service): stop "Invalid OTP" flash on successful sign-in - #134

Merged
aspiers merged 7 commits into
mainfrom
fix/otp-double-submit-flash
May 1, 2026
Merged

fix(auth-service): stop "Invalid OTP" flash on successful sign-in#134
aspiers merged 7 commits into
mainfrom
fix/otp-double-submit-flash

Conversation

@s-adamantine

@s-adamantine s-adamantine commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes two distinct OTP-verify UX bugs and clears two Sonar findings the new test code triggered.

Bug 1 — "Invalid OTP" red flash on a successful sign-in

The verify form auto-submits when the 6th digit lands. A second submit fired shortly after (Enter, mobile SMS autofill firing input on multiple boxes, paste+input race) called /sign-in/email-otp again with the now-consumed code. The response was "Invalid OTP", which rendered briefly while the first call's success-path redirect to /auth/complete was unloading the page — visible as a red flash during an otherwise successful sign-in.

Fix is a verifying-flag latch on the verify-form submit handler. The guard short-circuits before any in-flight state is set, so duplicate events drop cleanly. The latch is left set on success — verifyOtp triggers window.location.href, and we don't want a late event to re-open the form mid-navigation.

Bug 2 — Auto-submit spam loop after an invalid OTP

When the OTP really was wrong, the boxes stayed populated. The auto-submit handler fires whenever hiddenCode.value.length === otpBoxes.length, so editing a single digit (replace one wrong char) immediately resubmitted the still-mostly-wrong code on every keystroke — fast enough to trip the per-IP rate limiter (60 req/min) and lock the user out with HTTP 429.

Fix clears the boxes on the error branch and refocuses the first one. The length drops to 0, the user retypes 6 digits, auto-submit fires once when the 6th lands as originally intended.

Sonar fixes

The new regression tests tripped two Sonar findings, both addressed:

  • S7721 — hoisted the renderDefault test helper to module scope (was inside describe).
  • S5852 (security hotspot) — replaced an unbounded-quantifier regex (/^\s*verifying = false;/gm) with a String#split substring count. Same invariant pinned, no ReDoS surface.

Commits

  • bce65b5 fix(auth-service): drop duplicate OTP verify submits that flashed "Invalid OTP"
  • 3d659a2 test(auth-service): hoist renderDefault helper to module scope
  • ef55606 test(auth-service): replace ReDoS-flagged regex with substring count
  • 56c6665 fix(auth-service): clear OTP boxes on verify error

Test plan

  • pnpm test — 842 passed (40 in login-page.test.ts, including 4 regression tests pinning latch placement, error-only reset, and clear-on-error behaviour)
  • pnpm lint — clean
  • pnpm format:check — clean
  • pnpm typecheck — clean
  • SonarCloud quality gate — passing
  • Verified regression: stashing the latch fix made the latch tests fail; restoring it made them pass
  • Manual smoke: type OTP, paste OTP, mobile SMS autofill — confirm no "Invalid OTP" flash and clean redirect to /auth/complete
  • Manual smoke: enter a wrong OTP — confirm boxes clear, focus jumps to box 1, retyping does not auto-spam the server

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Removed false "Invalid OTP" message appearing after correct OTP submission in sign-in flow.
    • OTP input fields now clear on incorrect entry with focus automatically returning to the first field.
    • Prevented repeated auto-submission attempts while retyping OTP codes.
  • Style

    • Centered flash message banners (error and success notifications) within their containers.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@changeset-bot

changeset-bot Bot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9392adc

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Apr 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview, Comment May 1, 2026 1:00am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a client-side verifying latch to the login-page OTP submit flow to prevent double submissions and transient "Invalid OTP" flashes; clears OTP inputs and focuses the first box on error; adds tests validating latch and error-path behavior; updates release notes and flash CSS class usage.

Changes

Cohort / File(s) Summary
Release Notes
​.changeset/otp-verify-double-submit-flash.md
Adds a changeset describing OTP UI behavior changes: no brief "Invalid OTP" flash on success, cleared+focused inputs on error, centered flash banners, and a stable flash-msg base class with error/success modifiers.
OTP Double-Submit Protection & Flash UI
packages/auth-service/src/routes/login-page.ts
Introduces a shared verifying latch in the inline client script; submit handler early-returns if set, sets verifying = true, disables UI, awaits verifyOtp, shows showError/showSuccess appropriately, clears/focuses OTP boxes on error, and intentionally leaves the latch set on success (navigation). Also refactors flash messaging to flash-msg with error/success modifiers.
Regression Tests
packages/auth-service/src/__tests__/login-page.test.ts
Adds tests that parse rendered inline script to assert a single verifying declaration, presence of early-return if (verifying) return;, that verifying = true is set before awaiting verification, and that verifying = false plus clearOtpBoxes() occur only in the error branch; verifies showError(result.error) on failure.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client Script
  participant API as verifyOtp Endpoint
  participant Nav as Browser Navigation

  Client->>Client: onSubmit() checks verifying
  alt verifying == true
    Client-->>Client: return early (ignore)
  else verifying == false
    Client->>Client: set verifying = true, disable UI, label "Verifying..."
    Client->>API: await verifyOtp(currentEmail, otp)
    alt API returns error
      Client->>Client: showError(error), clearOtpBoxes(), focus first box
      Client->>Client: set verifying = false
    else API returns success
      Client->>Nav: window.location.href = '/auth/complete'
      %% intentionally do not reset verifying or re-enable UI
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • aspiers
  • Kzoeps

Poem

🐇 I held the latch with careful paw,
Stopped wild resubmits, eased the flaw.
When codes are wrong I clear and steer,
When checks pass through, I let you veer. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main fix: stopping the erroneous Invalid OTP flash message on successful sign-in, which is a primary objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/otp-double-submit-flash

Comment @coderabbitai help to get the list of available commands and usage tips.

@railway-app

railway-app Bot commented Apr 30, 2026

Copy link
Copy Markdown

🚅 Deployed to the ePDS-pr-134 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/auth-service ✅ Success (View Logs) Web May 1, 2026 at 12:58 am
@certified-app/demo ✅ Success (View Logs) Web Apr 30, 2026 at 9:04 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Apr 30, 2026 at 9:03 pm
@certified-app/pds-core ✅ Success (View Logs) Web Apr 30, 2026 at 9:03 pm

@coveralls-official

coveralls-official Bot commented Apr 30, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 25197094103

Coverage remained the same at 50.397%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 2740
Covered Lines: 1391
Line Coverage: 50.77%
Relevant Branches: 1669
Covered Branches: 831
Branch Coverage: 49.79%
Branches in Coverage %: Yes
Coverage Strength: 5.23 hits per line

💛 - Coveralls

s-adamantine and others added 4 commits May 1, 2026 00:36
…valid OTP"

The verify form auto-submits when the 6th digit lands. A second submit
triggered shortly after (Enter, mobile SMS autofill firing input on
multiple boxes, paste+input race) called /sign-in/email-otp again with
a now-consumed code; the response was "Invalid OTP", which rendered
briefly while the first call's success-path redirect was unloading the
page. Visible symptom: red "Invalid OTP" flash during an otherwise
successful sign-in.

Fix is a verifying-flag latch on the verify-form submit handler. The
guard short-circuits before the in-flight state is set, so a duplicate
event drops cleanly. The latch is left set on success — verifyOtp
triggers a window.location.href redirect, and we don't want a late
input/Enter event to re-open the form mid-navigation and fire a second
verify on the consumed OTP.

Adds three structural regression tests in login-page.test.ts that pin
the guard placement and the error-only reset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sonar S7721 flagged the helper inside the OTP latch regression
describe block. Move it above the describe so it isn't recreated
per call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sonar S5852 hotspot on `/^\s*verifying = false;/gm`. The pattern
isn't actually catastrophic (anchored, single quantifier), but the
intent — count `verifying = false;` assignments — is clearer with
String#split. Test 1 already pins the declaration count separately,
so total substring count of 2 (decl + reset) is equivalent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After an invalid code the boxes stayed populated. The auto-submit
handler fires whenever total length === 6, so editing a single digit
(replace one wrong char) immediately re-submitted the still-mostly-
wrong code on every keystroke — fast enough to trip the per-IP rate
limiter and lock the user out with HTTP 429.

Clear the boxes on the error branch and refocus the first one. The
length drops to 0; the user retypes 6 digits; auto-submit fires once
when the 6th digit lands, as originally intended.

Adds a regression test pinning the clearOtpBoxes() call inside the
error branch and amends the existing changeset to describe both the
flash fix and the spam fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Invalid OTP" / "Code resent" banner was text-align:left inside
a 100%-wide coloured box, leaving most of the row empty.

Centre the text and reshape the CSS:

- New base class .flash-msg (padding, radius, margin, text-align:center)
- Modifier classes .flash-msg.error (red) and .flash-msg.success (green)
- Container ships with class="flash-msg"; helpers toggle the modifier
- Resend-success no longer overrides colour/background via inline
  style; showSuccess() adds the .success class instead

The .flash-msg / .flash-msg.error / .flash-msg.success surface gives
client CSS a stable hook to restyle either variant without fighting
inline styles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aspiers and others added 2 commits May 1, 2026 00:59
End-user paragraph was overly verbose. Trim it. The flash-msg /
error/success class surface is for client app developers writing
custom CSS, not operators — move that note under the existing
Client app developers audience instead of Operators.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the skill's rules:
- summary in plain language (End users is in the audience list)
- per-audience sections don't restate the summary
- dense End users section becomes bullets (3 distinct points)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented May 1, 2026

Copy link
Copy Markdown

@aspiers
aspiers added this pull request to the merge queue May 1, 2026
Merged via the queue into main with commit b8e4349 May 1, 2026
14 of 15 checks passed
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-134 May 1, 2026 01:07 Destroyed
@aspiers
aspiers deleted the fix/otp-double-submit-flash branch May 1, 2026 01:08
@aspiers

aspiers commented May 1, 2026

Copy link
Copy Markdown
Contributor

Fixed #125.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

resending OTP should clear the existing OTP form

2 participants