Skip to content

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

Description

@aspiers

Problem

better-auth distinguishes expired OTPs from wrong OTPs when verifying (OTP_EXPIRED "OTP expired" vs INVALID_OTP, see better-auth 1.4.18 dist/plugins/email-otp/routes.mjs:294), but in production we never see 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:1018), handled by toNodeHandler(betterAuthInstance) (src/index.ts:48) with no HTTP access logging.
  • better-auth's onError only logs 4xx APIErrors when logger.level is set to error/warn/debug in the betterAuth() options (better-auth/dist/api/index.mjs:182-193). Our createBetterAuth() (src/better-auth.ts) does not configure logger or onAPIError, so "OTP expired" failures are completely silent server-side.

Only the secondary /account/verify-otp route logs verification failures (routes/account-login.ts:118).

This matters right now: we have production complaints of OTP emails arriving after the 10-minute expiry, and the "OTP expired" vs "Invalid OTP" split is exactly the signal that separates genuinely-late emails from users retyping old codes — and we're dropping it.

Proposed change

In createBetterAuth(), configure better-auth so 4xx verification failures reach our pino logs, either:

  • onAPIError: { onError(e, ctx) { ... } } routing through our createLogger('auth:better-auth') at warn with the error message and request path, or
  • logger: { level: 'warn', log: (level, msg, ...) => ... } bridged to pino.

onAPIError.onError is preferable: full control, and we can include the email identifier from the request context where safely available.

Why

Counting OTP expired vs Invalid OTP occurrences over time (at warn, visible at prod's default info level) directly answers how many users hit the expiry window, and correlating those with email-send timestamps (see companion issue on EmailSender logging) attributes them to delivery delay vs user behaviour.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions