Skip to content

feat(auth-service): log better-auth OTP verification failures#186

Open
aspiers wants to merge 2 commits into
mainfrom
fix/log-otp-verification-failures
Open

feat(auth-service): log better-auth OTP verification failures#186
aspiers wants to merge 2 commits into
mainfrom
fix/log-otp-verification-failures

Conversation

@aspiers

@aspiers aspiers commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

Fixes #184.

better-auth distinguishes expired OTPs from wrong OTPs when verifying (OTP expired vs Invalid OTP), but in production we never saw either:

  • The main OAuth login flow posts directly from the browser to /api/auth/sign-in/email-otp (packages/auth-service/src/routes/login-page.ts), handled by toNodeHandler(betterAuthInstance) with no HTTP access logging.
  • better-auth's built-in onError only logs 4xx APIErrors when logger.level is set in the betterAuth() options. createBetterAuth() configured neither logger nor onAPIError, so "OTP expired" failures were completely silent server-side.

Only the secondary /account/verify-otp route logged verification failures.

Change

Wire onAPIError.onError in createBetterAuth() to a small exported helper logBetterAuthApiError():

  • Logs 4xx APIErrors at warn (visible at prod's default info level) under the auth:better-auth logger, with the status, statusCode, and reason message (OTP expired / Invalid OTP / Too many attempts).
  • Skips 3xx redirects (better-auth filters FOUND before this runs anyway) and 5xx errors (already logged by better-auth's own handler) to avoid noise and double-logging.
  • Ignores non-APIError values.

The email address is deliberately not logged: better-auth invokes onAPIError.onError with the instance-wide auth context, not the per-request endpoint context, so the request body is not reliably available there. The issue's "where safely available" caveat anticipated this.

The handler is extracted into a pure, exported function so it can be unit-tested directly without instantiating a full better-auth instance — matching the existing test style in this package.

Why

Counting OTP expired vs Invalid OTP occurrences over time directly answers how many users hit the 10-minute expiry window, and correlating those with email-send timestamps attributes them to delivery delay vs user behaviour. This is the signal that separates genuinely-late emails from users retyping old codes, and we were dropping it.

Testing

  • New unit test log-better-auth-api-error.test.ts (7 cases): expired/invalid/too-many-attempts all logged at warn with the right reason; body.message fallback to error.message; 5xx, 3xx redirects, and non-APIError values all skipped.
  • pnpm typecheck — clean.
  • pnpm lint — clean.
  • Full auth-service suite — 426 passing.

🤖 Generated with Claude Code

The main OAuth login flow posts directly from the browser to
`/api/auth/sign-in/email-otp`, handled by better-auth's node handler,
which we mount with no HTTP access logging. On verification failure
better-auth throws an `APIError` whose message is the exact reason —
"OTP expired", "Invalid OTP" or "Too many attempts" — but these
reached no log at all, because `createBetterAuth()` configures neither
`logger` nor `onAPIError`.

Wire `onAPIError.onError` to a small `logBetterAuthApiError()` helper
that logs 4xx `APIError`s at `warn` (visible at prod's default `info`
level) with the status and reason message. 3xx redirects are filtered
by better-auth before this runs; 5xx errors are already logged by its
own handler, so both are skipped to avoid duplicate noise.

Counting `OTP expired` vs `Invalid OTP` over time separates genuinely
late emails from users retyping stale codes.

Closes #184

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 6, 2026 14:28
@vercel

vercel Bot commented Jul 6, 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 Jul 6, 2026 2:36pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: de5456e

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

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@aspiers, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6bc8155-373e-4bb5-8267-8a4abb19134e

📥 Commits

Reviewing files that changed from the base of the PR and between 9765fd8 and de5456e.

📒 Files selected for processing (3)
  • .changeset/log-otp-verification-failures.md
  • packages/auth-service/src/__tests__/log-better-auth-api-error.test.ts
  • packages/auth-service/src/better-auth.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/log-otp-verification-failures

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coveralls-official

Copy link
Copy Markdown

Coverage Report for CI Build 28799043777

Coverage increased (+0.1%) to 56.169%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (4 of 5 lines covered, 80.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
packages/auth-service/src/better-auth.ts 5 4 80.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 2956
Covered Lines: 1652
Line Coverage: 55.89%
Relevant Branches: 1826
Covered Branches: 1034
Branch Coverage: 56.63%
Branches in Coverage %: Yes
Coverage Strength: 5.88 hits per line

💛 - Coveralls

Copilot AI 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.

Pull request overview

This PR improves observability in @certified-app/auth-service by surfacing better-auth email OTP verification failures (e.g. “OTP expired” vs “Invalid OTP”) into the service’s own pino logs via onAPIError, so production can distinguish delivery delay from user error.

Changes:

  • Added an exported helper logBetterAuthApiError() and wired it into betterAuth({ onAPIError: { onError } }) to log 4xx APIErrors at warn.
  • Added unit tests covering logging/skip behavior for 4xx vs 3xx/5xx and non-APIError values.
  • Added a changeset documenting the new operator-facing logging behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/auth-service/src/better-auth.ts Adds logBetterAuthApiError() and hooks better-auth onAPIError to emit structured warn logs for OTP verification failures.
packages/auth-service/src/__tests__/log-better-auth-api-error.test.ts Unit tests verifying which APIErrors are logged vs skipped and validating logged fields.
.changeset/log-otp-verification-failures.md Changeset describing the new warn-level logging for OTP failure reasons for operators.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/auth-service/src/better-auth.ts
Comment on lines +31 to +35
/**
* Surface better-auth 4xx client errors in our own logs.
*
* The main OAuth login flow posts straight from the browser to
* /api/auth/sign-in/email-otp, handled by better-auth's node handler. On

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in de5456e — updated the module header to note the new onAPIError logging hook and dropped the stale "foundation-only, no existing behavior is changed" line.

- Include the APIError object (`err`) in the warn log so its cause and
  stack are available when debugging unexpected 4xx errors, matching the
  `{ err, ... }` convention used elsewhere in the package.
- Update the module header comment: the module now changes runtime
  behaviour (adds warn logging), so drop the stale "foundation-only,
  no existing behavior is changed" note.

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

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@railway-app

railway-app Bot commented Jul 6, 2026

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
@certified-app/auth-service ✅ Success (View Logs) Web Jul 6, 2026 at 2:36 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Jul 6, 2026 at 2:30 pm
@certified-app/pds-core ✅ Success (View Logs) Web Jul 6, 2026 at 2:30 pm
@certified-app/demo ✅ Success (View Logs) Web Jul 6, 2026 at 2:29 pm

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.

Surface better-auth OTP verification failures (expired vs invalid) in server logs

2 participants