@@ -633,6 +633,9 @@ func (ts *UserTestSuite) TestUpdateUserEmailClearsStaleTokens() {
633633 userA .EmailChangeTokenNew = "email-change-new-hash"
634634 userA .EmailChangeSentAt = & now
635635 userA .EmailChangeConfirmStatus = 1
636+ userA .PhoneChange = "123456789"
637+ userA .PhoneChangeToken = "phone-change-token-hash"
638+ userA .PhoneChangeSentAt = & now
636639 userA .ReauthenticationToken = "reauthentication-token-hash"
637640 userA .ReauthenticationSentAt = & now
638641 require .NoError (ts .T (), ts .db .UpdateOnly (
@@ -646,6 +649,9 @@ func (ts *UserTestSuite) TestUpdateUserEmailClearsStaleTokens() {
646649 "email_change_token_new" ,
647650 "email_change_sent_at" ,
648651 "email_change_confirm_status" ,
652+ "phone_change" ,
653+ "phone_change_token" ,
654+ "phone_change_sent_at" ,
649655 "reauthentication_token" ,
650656 "reauthentication_sent_at" ,
651657 ))
@@ -667,6 +673,9 @@ func (ts *UserTestSuite) TestUpdateUserEmailClearsStaleTokens() {
667673 require .Empty (ts .T (), userA .EmailChangeTokenNew )
668674 require .Nil (ts .T (), userA .EmailChangeSentAt )
669675 require .Equal (ts .T (), 0 , userA .EmailChangeConfirmStatus )
676+ require .Empty (ts .T (), userA .PhoneChange )
677+ require .Empty (ts .T (), userA .PhoneChangeToken )
678+ require .Nil (ts .T (), userA .PhoneChangeSentAt )
670679 require .Empty (ts .T (), userA .ReauthenticationToken )
671680 require .Nil (ts .T (), userA .ReauthenticationSentAt )
672681
@@ -675,6 +684,49 @@ func (ts *UserTestSuite) TestUpdateUserEmailClearsStaleTokens() {
675684 require .Error (ts .T (), err )
676685}
677686
687+ func (ts * UserTestSuite ) TestUpdateUserEmailFromEmptyClearsStaleTokens () {
688+ // a user without an email or identities, e.g. an anonymous user
689+ userA , err := NewUser ("" , "" , "" , "authenticated" , nil )
690+ require .NoError (ts .T (), err )
691+ require .NoError (ts .T (), ts .db .Create (userA ))
692+
693+ identity , err := NewIdentity (userA , "google" , map [string ]any {
694+ "sub" : userA .ID .String (),
695+ "email" : "bar@example.com" ,
696+ "email_verified" : true ,
697+ })
698+ require .NoError (ts .T (), err )
699+ require .NoError (ts .T (), ts .db .Create (identity ))
700+
701+ // simulate a phone change requested before the identity was linked
702+ now := time .Now ()
703+ userA .PhoneChange = "123456789"
704+ userA .PhoneChangeToken = "phone-change-token-hash"
705+ userA .PhoneChangeSentAt = & now
706+ require .NoError (ts .T (), ts .db .UpdateOnly (
707+ userA ,
708+ "phone_change" ,
709+ "phone_change_token" ,
710+ "phone_change_sent_at" ,
711+ ))
712+ require .NoError (ts .T (), CreateOneTimeToken (ts .db , userA .ID , userA .PhoneChange , userA .PhoneChangeToken , PhoneChangeToken ))
713+
714+ // promoting an identity's email over an empty one is still a primary
715+ // email transition, so outstanding tokens must be revoked
716+ require .NoError (ts .T (), userA .UpdateUserEmailFromIdentities (ts .db ))
717+ require .Equal (ts .T (), identity .GetEmail (), userA .GetEmail ())
718+
719+ userA , err = FindUserByID (ts .db , userA .ID )
720+ require .NoError (ts .T (), err )
721+ require .Empty (ts .T (), userA .PhoneChange )
722+ require .Empty (ts .T (), userA .PhoneChangeToken )
723+ require .Nil (ts .T (), userA .PhoneChangeSentAt )
724+
725+ _ , err = FindOneTimeToken (ts .db , "phone-change-token-hash" , PhoneChangeToken )
726+ require .Error (ts .T (), err )
727+ require .True (ts .T (), IsNotFoundError (err ))
728+ }
729+
678730func (ts * UserTestSuite ) TestUpdateUserEmailFailure () {
679731 userA , err := NewUser ("" , "foo@example.com" , "" , "authenticated" , nil )
680732 require .NoError (ts .T (), err )
0 commit comments