chatdesk-ui/auth_v2.169.0/internal/api/phone.go

170 lines
5.4 KiB
Go

package api
import (
"bytes"
"net/http"
"regexp"
"strings"
"text/template"
"time"
"github.com/supabase/auth/internal/hooks"
"github.com/pkg/errors"
"github.com/supabase/auth/internal/api/sms_provider"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/models"
"github.com/supabase/auth/internal/storage"
)
var e164Format = regexp.MustCompile("^[1-9][0-9]{1,14}$")
const (
phoneConfirmationOtp = "confirmation"
phoneReauthenticationOtp = "reauthentication"
)
func validatePhone(phone string) (string, error) {
phone = formatPhoneNumber(phone)
if isValid := validateE164Format(phone); !isValid {
return "", badRequestError(ErrorCodeValidationFailed, "Invalid phone number format (E.164 required)")
}
return phone, nil
}
// validateE164Format checks if phone number follows the E.164 format
func validateE164Format(phone string) bool {
return e164Format.MatchString(phone)
}
// formatPhoneNumber removes "+" and whitespaces in a phone number
func formatPhoneNumber(phone string) string {
return strings.ReplaceAll(strings.TrimPrefix(phone, "+"), " ", "")
}
// sendPhoneConfirmation sends an otp to the user's phone number
func (a *API) sendPhoneConfirmation(r *http.Request, tx *storage.Connection, user *models.User, phone, otpType string, channel string) (string, error) {
config := a.config
var token *string
var sentAt *time.Time
includeFields := []string{}
switch otpType {
case phoneChangeVerification:
token = &user.PhoneChangeToken
sentAt = user.PhoneChangeSentAt
user.PhoneChange = phone
includeFields = append(includeFields, "phone_change", "phone_change_token", "phone_change_sent_at")
case phoneConfirmationOtp:
token = &user.ConfirmationToken
sentAt = user.ConfirmationSentAt
includeFields = append(includeFields, "confirmation_token", "confirmation_sent_at")
case phoneReauthenticationOtp:
token = &user.ReauthenticationToken
sentAt = user.ReauthenticationSentAt
includeFields = append(includeFields, "reauthentication_token", "reauthentication_sent_at")
default:
return "", internalServerError("invalid otp type")
}
// intentionally keeping this before the test OTP, so that the behavior
// of regular and test OTPs is similar
if sentAt != nil && !sentAt.Add(config.Sms.MaxFrequency).Before(time.Now()) {
return "", tooManyRequestsError(ErrorCodeOverSMSSendRateLimit, generateFrequencyLimitErrorMessage(sentAt, config.Sms.MaxFrequency))
}
now := time.Now()
var otp, messageID string
if testOTP, ok := config.Sms.GetTestOTP(phone, now); ok {
otp = testOTP
messageID = "test-otp"
}
// not using test OTPs
if otp == "" {
// TODO(km): Deprecate this behaviour - rate limits should still be applied to autoconfirm
if !config.Sms.Autoconfirm {
// apply rate limiting before the sms is sent out
if ok := a.limiterOpts.Phone.Allow(); !ok {
return "", tooManyRequestsError(ErrorCodeOverSMSSendRateLimit, "SMS rate limit exceeded")
}
}
otp = crypto.GenerateOtp(config.Sms.OtpLength)
if config.Hook.SendSMS.Enabled {
input := hooks.SendSMSInput{
User: user,
SMS: hooks.SMS{
OTP: otp,
},
}
output := hooks.SendSMSOutput{}
err := a.invokeHook(tx, r, &input, &output)
if err != nil {
return "", err
}
} else {
smsProvider, err := sms_provider.GetSmsProvider(*config)
if err != nil {
return "", internalServerError("Unable to get SMS provider").WithInternalError(err)
}
message, err := generateSMSFromTemplate(config.Sms.SMSTemplate, otp)
if err != nil {
return "", internalServerError("error generating sms template").WithInternalError(err)
}
messageID, err := smsProvider.SendMessage(phone, message, channel, otp)
if err != nil {
return messageID, unprocessableEntityError(ErrorCodeSMSSendFailed, "Error sending %s OTP to provider: %v", otpType, err)
}
}
}
*token = crypto.GenerateTokenHash(phone, otp)
switch otpType {
case phoneConfirmationOtp:
user.ConfirmationSentAt = &now
case phoneChangeVerification:
user.PhoneChangeSentAt = &now
case phoneReauthenticationOtp:
user.ReauthenticationSentAt = &now
}
if err := tx.UpdateOnly(user, includeFields...); err != nil {
return messageID, errors.Wrap(err, "Database error updating user for phone")
}
var ottErr error
switch otpType {
case phoneConfirmationOtp:
if err := models.CreateOneTimeToken(tx, user.ID, user.GetPhone(), user.ConfirmationToken, models.ConfirmationToken); err != nil {
ottErr = errors.Wrap(err, "Database error creating confirmation token for phone")
}
case phoneChangeVerification:
if err := models.CreateOneTimeToken(tx, user.ID, user.PhoneChange, user.PhoneChangeToken, models.PhoneChangeToken); err != nil {
ottErr = errors.Wrap(err, "Database error creating phone change token")
}
case phoneReauthenticationOtp:
if err := models.CreateOneTimeToken(tx, user.ID, user.GetPhone(), user.ReauthenticationToken, models.ReauthenticationToken); err != nil {
ottErr = errors.Wrap(err, "Database error creating reauthentication token for phone")
}
}
if ottErr != nil {
return messageID, internalServerError("error creating one time token").WithInternalError(ottErr)
}
return messageID, nil
}
func generateSMSFromTemplate(SMSTemplate *template.Template, otp string) (string, error) {
var message bytes.Buffer
if err := SMSTemplate.Execute(&message, struct {
Code string
}{Code: otp}); err != nil {
return "", err
}
return message.String(), nil
}