Files
zitadel/internal/api/ui/login/mfa_init_sms.go
T
597f385ca4 feat: Add SMS country code selector to SMT OTP (LoginV1) (#11373)
<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved

- This adds rudimentary country code handling for LoginV1 SMS OTP
- Previously defaulting to CH with no prompt to user causing e.g UK
(+44) based numbers to be re-routed to CH. (+41)

# Additional Context

- Support for SMS OTP is not complete for LoginV2 (according to latest
support email)

Tested with Twilio on both UK + DE # . Appears to work. 

- Closes #8891 

<img width="544" height="485" alt="image"
src="https://github.com/user-attachments/assets/84d31bd8-8863-465f-8860-b51602439dfb"
/>

<img width="858" height="1298" alt="Screenshot 2026-01-16 at 11 23 22"
src="https://github.com/user-attachments/assets/4bf16f0b-2fec-4d63-95b0-f0b73f3a131a"
/>

---------

Co-authored-by: Livio Spring <[email protected]>
Co-authored-by: Silvan <[email protected]>
2026-01-22 06:47:54 +00:00

138 lines
4.5 KiB
Go

package login
import (
"net/http"
"github.com/zitadel/zitadel/internal/domain"
)
const (
tmplMFASMSInit = "mfainitsms"
)
type smsInitData struct {
userData
Edit bool
MFAType domain.MFAType
Phone string
CountryCode string
CountryCodes []domain.CountryCode
}
type smsInitFormData struct {
Edit bool `schema:"edit"`
Resend bool `schema:"resend"`
Phone string `schema:"phone"`
NewPhone string `schema:"newPhone"`
Code string `schema:"code"`
CountryCode string `schema:"countryCode"`
}
// handleRegisterOTPSMS checks if the user has a verified phone number and will directly add OTP SMS as 2FA.
// It will also add a successful OTP SMS check to the auth request.
// If there's no verified phone number, the potential last phone number will be used to render the registration page
func (l *Login) handleRegisterOTPSMS(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
user, err := l.query.GetNotifyUserByID(r.Context(), true, authReq.UserID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if user.VerifiedPhone == "" {
data := new(smsInitData)
data.Phone = user.LastPhone
data.Edit = user.LastPhone == ""
l.renderRegisterSMS(w, r, authReq, data, nil)
return
}
_, err = l.command.AddHumanOTPSMSWithCheckSucceeded(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
done := &mfaDoneData{
MFAType: domain.MFATypeOTPSMS,
}
l.renderMFAInitDone(w, r, authReq, done)
}
func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *smsInitData, err error) {
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", err)
data.profileData = l.getProfileData(authReq)
data.MFAType = domain.MFATypeOTPSMS
data.CountryCodes = domain.GetCountryCodes(translator.Lang(r))
if data.CountryCode == "" {
data.CountryCode = "CH" // default to Switzerland
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFASMSInit], data, nil)
}
// handleRegisterSMSCheck handles form submissions of the SMS registration.
// The user can be either in edit mode, where a phone number can be entered / changed.
// If a phone was set, the user can either switch to edit mode, have a resend of the code or verify the code by entering it.
// On successful code verification, the phone will be added to the user as well as his MFA
// and a successful OTP SMS check will be added to the auth request.
func (l *Login) handleRegisterSMSCheck(w http.ResponseWriter, r *http.Request) {
formData := new(smsInitFormData)
authReq, err := l.ensureAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
ctx := setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID)
// save the current state
data := &smsInitData{Phone: formData.Phone, CountryCode: formData.CountryCode}
if formData.Edit {
data.Edit = true
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
if formData.Resend {
_, err = l.command.CreateHumanPhoneVerificationCode(ctx, authReq.UserID, authReq.UserOrgID)
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
// if the user is currently in edit mode,
// he can either change the phone number
// or just return to the code verification again
if formData.Code == "" {
data.Phone = formData.NewPhone
if formData.NewPhone != formData.Phone {
// Normalize phone with selected country code before saving
normalizedPhone, normErr := domain.PhoneNumber(formData.NewPhone).NormalizeWithRegion(formData.CountryCode)
if normErr != nil {
data.Edit = true
l.renderRegisterSMS(w, r, authReq, data, normErr)
return
}
data.Phone = string(normalizedPhone)
_, err = l.command.ChangeUserPhone(ctx, authReq.UserID, string(normalizedPhone), l.userCodeAlg)
if err != nil {
// stay in edit more
data.Edit = true
}
}
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
_, err = l.command.VerifyUserPhone(ctx, authReq.UserID, formData.Code, l.userCodeAlg)
if err != nil {
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
_, err = l.command.AddHumanOTPSMSWithCheckSucceeded(ctx, authReq.UserID, authReq.UserOrgID, authReq)
if err != nil {
l.renderRegisterSMS(w, r, authReq, data, err)
return
}
done := &mfaDoneData{
MFAType: domain.MFATypeOTPSMS,
}
l.renderMFAInitDone(w, r, authReq, done)
}