From 58fcd5a8ef5df25cca2cb2063840f814bc6972e3 Mon Sep 17 00:00:00 2001 From: sidgaikwad Date: Fri, 25 Sep 2026 21:28:22 +0530 Subject: [PATCH] fix(verify): record a login, not a signup, for confirmed phone sign-ins smsVerify recorded a user_signedup audit event for every sms verification, including existing users whose phone is already confirmed. Each SMS sign-in by an existing user was counted as a new signup. Record login when the phone is already confirmed, as recoverVerify does for confirmed email users. ConfirmPhone still runs in both cases because it also clears the user's one-time tokens. Refs supabase/supabase#37583 --- internal/api/verify.go | 8 +++++++- internal/api/verify_ott_parity_test.go | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/api/verify.go b/internal/api/verify.go index ae0d05c42b..439c00c506 100644 --- a/internal/api/verify.go +++ b/internal/api/verify.go @@ -408,7 +408,13 @@ func (a *API) smsVerify(r *http.Request, conn *storage.Connection, user *models. switch params.Type { case smsVerification: - if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{ + // A phone that is already confirmed belongs to an existing user, + // so this verification is a sign-in, not a signup. + action := models.UserSignedUpAction + if user.IsPhoneConfirmed() { + action = models.LoginAction + } + if terr := models.NewAuditLogEntry(config.AuditLog, r, tx, user, action, "", map[string]interface{}{ "provider": PhoneProvider, }); terr != nil { return terr diff --git a/internal/api/verify_ott_parity_test.go b/internal/api/verify_ott_parity_test.go index c48b60bbc8..20f1656624 100644 --- a/internal/api/verify_ott_parity_test.go +++ b/internal/api/verify_ott_parity_test.go @@ -226,6 +226,14 @@ func (ts *VerifyTestSuite) TestVerifyOTPParityPhoneFlows() { Phone: parityPhone, } + phoneLoggedIn := otpParityOutcome{ + Status: http.StatusOK, + Action: string(models.LoginAction), + PhoneConfirmed: true, + Email: parityEmail, + Phone: parityPhone, + } + phoneChanged := otpParityOutcome{ Status: http.StatusOK, Action: string(models.UserModifiedAction), @@ -242,6 +250,16 @@ func (ts *VerifyTestSuite) TestVerifyOTPParityPhoneFlows() { requestBody: phoneOTPBody(smsVerification, parityPhone), expected: phoneSignedUp, }, + // An existing user signing in with an SMS code is a login. Only the + // verification that first confirms the phone is a signup. + "sms with a valid code signs a confirmed user in": { + seed: func(u *models.User) { + u.PhoneConfirmedAt = &now + ts.seedChallenge(u, models.ConfirmationToken, parityPhone, phoneHash, now, time.Hour) + }, + requestBody: phoneOTPBody(smsVerification, parityPhone), + expected: phoneLoggedIn, + }, "sms with an expired code is rejected": { seed: func(u *models.User) { ts.seedChallenge(u, models.ConfirmationToken, parityPhone, phoneHash, expired, -time.Hour)