Skip to content

phone_change_token OTP code from SMS doesn't match stored token hash #40797

@rmsageb

Description

@rmsageb

Bug report

  • I confirm this is a bug with Supabase, not with my own application.
  • I confirm I have searched the Docs, GitHub Discussions, and Discord.

Bug Description

When using auth.updateUser({ phone }) to send a phone change OTP, the 6-digit code sent via SMS does not match the token hash stored in auth.one_time_tokens, causing verification to always fail with 403: Token has expired or is invalid.

  • If helpful, broader context is that users create an account with email initially. I want to enable them to subsequently sign in with a phone number and receive OTP via SMS.
  • Also worth mentioning, it did randomly work twice yesterday during testing (with intermittent failures in between). I haven't been able to get any additional successes since then.

Important

Update: Tested with Supabase's "Test Phone Numbers and OTPs" feature:
Added test phone number with pre-registered OTP code
Verification succeeded immediately
This confirms our verification implementation is correct
The bug is specifically in Supabase's OTP generation/sending pipeline when using real SMS

Steps to Reproduce

  1. Call supabase.auth.updateUser({ phone: "[REDACTED]" })
  2. Receive SMS with code (e.g., [6-digit-code])
  3. Call supabase.auth.verifyOtp({ phone: "[REDACTED]", token: "[6-digit-code]", type: "phone_change" })
  4. Result: AuthApiError: Token has expired or is invalid with code: 'otp_expired'

Evidence

Token in database:
SELECT * FROM auth.one_time_tokens WHERE id = '[token-id]';- relates_to: "[REDACTED]"

  • token_type: "phone_change_token"
  • created_at: "2025-11-25 08:10:07"
  • token_hash: "8379b9c0a87dab57cc7ade11499cec5535c288105ff53fcf454b30cf"

SMS sent by Supabase (via Twilio):

  • Message SID: [message-sid]
  • Body: "[REDACTED]: Your code is [6-digit-code]"
  • Sent at: 2025-11-25 08:10:07 PST (matches token creation)

Code verification:
SELECT crypt('[6-digit-code]', token_hash) = token_hash AS code_matches;
-- Returns: falseServer logs:

  • Verification attempted within 33ms of token creation (not expired)
  • Phone format matches: [REDACTED] = relates_to
  • Type matches: phone_change = phone_change_token
  • Error: AuthApiError: Token has expired or is invalid with code: 'otp_expired'
  • Request ID: 9a3fabc5932ceb2a-SJC

Expected Behavior
The OTP code sent in SMS should match the token hash stored in auth.one_time_tokens, allowing verification to succeed.

Actual Behavior
The SMS code does not match the stored token hash, causing verification to always fail immediately (not due to expiration).

Environment

  • Supabase JS: @supabase/[email protected]
  • Supabase SSR: @supabase/[email protected]
  • SMS Provider: Twilio (configured in Supabase dashboard)
  • SMS Template: "[REDACTED]: Your code is {{.Code }}"

Additional Context
This happens consistently with every OTP attempt. The code is being used immediately (within seconds), so expiration is not the issue. The mismatch between the SMS code and token hash suggests a bug in Supabase's OTP generation/sending pipeline.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions