package api import ( "context" "net/http" "time" "github.com/fatih/structs" "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/supabase/auth/internal/api/provider" "github.com/supabase/auth/internal/api/sms_provider" "github.com/supabase/auth/internal/metering" "github.com/supabase/auth/internal/models" "github.com/supabase/auth/internal/storage" ) // SignupParams are the parameters the Signup endpoint accepts type SignupParams struct { Email string `json:"email"` Phone string `json:"phone"` Password string `json:"password"` Data map[string]interface{} `json:"data"` Provider string `json:"-"` Aud string `json:"-"` Channel string `json:"channel"` CodeChallengeMethod string `json:"code_challenge_method"` CodeChallenge string `json:"code_challenge"` } func (a *API) validateSignupParams(ctx context.Context, p *SignupParams) error { config := a.config if p.Password == "" { return badRequestError(ErrorCodeValidationFailed, "Signup requires a valid password") } if err := a.checkPasswordStrength(ctx, p.Password); err != nil { return err } if p.Email != "" && p.Phone != "" { return badRequestError(ErrorCodeValidationFailed, "Only an email address or phone number should be provided on signup.") } if p.Provider == "phone" && !sms_provider.IsValidMessageChannel(p.Channel, config) { return badRequestError(ErrorCodeValidationFailed, InvalidChannelError) } // PKCE not needed as phone signups already return access token in body if p.Phone != "" && p.CodeChallenge != "" { return badRequestError(ErrorCodeValidationFailed, "PKCE not supported for phone signups") } if err := validatePKCEParams(p.CodeChallengeMethod, p.CodeChallenge); err != nil { return err } return nil } func (p *SignupParams) ConfigureDefaults() { if p.Email != "" { p.Provider = "email" } else if p.Phone != "" { p.Provider = "phone" } if p.Data == nil { p.Data = make(map[string]interface{}) } // For backwards compatibility, we default to SMS if params Channel is not specified if p.Phone != "" && p.Channel == "" { p.Channel = sms_provider.SMSProvider } } func (params *SignupParams) ToUserModel(isSSOUser bool) (user *models.User, err error) { switch params.Provider { case "email": user, err = models.NewUser("", params.Email, params.Password, params.Aud, params.Data) case "phone": user, err = models.NewUser(params.Phone, "", params.Password, params.Aud, params.Data) case "anonymous": user, err = models.NewUser("", "", "", params.Aud, params.Data) user.IsAnonymous = true default: // handles external provider case user, err = models.NewUser("", params.Email, params.Password, params.Aud, params.Data) } if err != nil { err = internalServerError("Database error creating user").WithInternalError(err) return } user.IsSSOUser = isSSOUser if user.AppMetaData == nil { user.AppMetaData = make(map[string]interface{}) } user.Identities = make([]models.Identity, 0) if params.Provider != "anonymous" { // TODO: Deprecate "provider" field user.AppMetaData["provider"] = params.Provider user.AppMetaData["providers"] = []string{params.Provider} } return user, nil } // Signup is the endpoint for registering a new user func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() config := a.config db := a.db.WithContext(ctx) if config.DisableSignup { return unprocessableEntityError(ErrorCodeSignupDisabled, "Signups not allowed for this instance") } params := &SignupParams{} if err := retrieveRequestParams(r, params); err != nil { return err } params.ConfigureDefaults() if err := a.validateSignupParams(ctx, params); err != nil { return err } var err error flowType := getFlowFromChallenge(params.CodeChallenge) var user *models.User var grantParams models.GrantParams grantParams.FillGrantParams(r) params.Aud = a.requestAud(ctx, r) switch params.Provider { case "email": if !config.External.Email.Enabled { return badRequestError(ErrorCodeEmailProviderDisabled, "Email signups are disabled") } params.Email, err = a.validateEmail(params.Email) if err != nil { return err } user, err = models.IsDuplicatedEmail(db, params.Email, params.Aud, nil) case "phone": if !config.External.Phone.Enabled { return badRequestError(ErrorCodePhoneProviderDisabled, "Phone signups are disabled") } params.Phone, err = validatePhone(params.Phone) if err != nil { return err } user, err = models.FindUserByPhoneAndAudience(db, params.Phone, params.Aud) default: msg := "" if config.External.Email.Enabled && config.External.Phone.Enabled { msg = "Sign up only available with email or phone provider" } else if config.External.Email.Enabled { msg = "Sign up only available with email provider" } else if config.External.Phone.Enabled { msg = "Sign up only available with phone provider" } else { msg = "Sign up with this provider not possible" } return badRequestError(ErrorCodeValidationFailed, msg) } if err != nil && !models.IsNotFoundError(err) { return internalServerError("Database error finding user").WithInternalError(err) } var signupUser *models.User if user == nil { // always call this outside of a database transaction as this method // can be computationally hard and block due to password hashing signupUser, err = params.ToUserModel(false /* <- isSSOUser */) if err != nil { return err } } err = db.Transaction(func(tx *storage.Connection) error { var terr error if user != nil { if (params.Provider == "email" && user.IsConfirmed()) || (params.Provider == "phone" && user.IsPhoneConfirmed()) { return UserExistsError } // do not update the user because we can't be sure of their claimed identity } else { user, terr = a.signupNewUser(tx, signupUser) if terr != nil { return terr } } identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), params.Provider) if terr != nil { if !models.IsNotFoundError(terr) { return terr } identityData := structs.Map(provider.Claims{ Subject: user.ID.String(), Email: user.GetEmail(), }) for k, v := range params.Data { if _, ok := identityData[k]; !ok { identityData[k] = v } } identity, terr = a.createNewIdentity(tx, user, params.Provider, identityData) if terr != nil { return terr } if terr := user.RemoveUnconfirmedIdentities(tx, identity); terr != nil { return terr } } user.Identities = []models.Identity{*identity} if params.Provider == "email" && !user.IsConfirmed() { if config.Mailer.Autoconfirm { if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr } if terr = user.Confirm(tx); terr != nil { return internalServerError("Database error updating user").WithInternalError(terr) } } else { if terr = models.NewAuditLogEntry(r, tx, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr } if isPKCEFlow(flowType) { _, terr := generateFlowState(tx, params.Provider, models.EmailSignup, params.CodeChallengeMethod, params.CodeChallenge, &user.ID) if terr != nil { return terr } } if terr = a.sendConfirmation(r, tx, user, flowType); terr != nil { return terr } } } else if params.Provider == "phone" && !user.IsPhoneConfirmed() { if config.Sms.Autoconfirm { if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{ "provider": params.Provider, "channel": params.Channel, }); terr != nil { return terr } if terr = user.ConfirmPhone(tx); terr != nil { return internalServerError("Database error updating user").WithInternalError(terr) } } else { if terr = models.NewAuditLogEntry(r, tx, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr } if _, terr := a.sendPhoneConfirmation(r, tx, user, params.Phone, phoneConfirmationOtp, params.Channel); terr != nil { return terr } } } return nil }) if err != nil { if errors.Is(err, UserExistsError) { err = db.Transaction(func(tx *storage.Connection) error { if terr := models.NewAuditLogEntry(r, tx, user, models.UserRepeatedSignUpAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr } return nil }) if err != nil { return err } if config.Mailer.Autoconfirm || config.Sms.Autoconfirm { return unprocessableEntityError(ErrorCodeUserAlreadyExists, "User already registered") } sanitizedUser, err := sanitizeUser(user, params) if err != nil { return err } return sendJSON(w, http.StatusOK, sanitizedUser) } return err } // handles case where Mailer.Autoconfirm is true or Phone.Autoconfirm is true if user.IsConfirmed() || user.IsPhoneConfirmed() { var token *AccessTokenResponse err = db.Transaction(func(tx *storage.Connection) error { var terr error if terr = models.NewAuditLogEntry(r, tx, user, models.LoginAction, "", map[string]interface{}{ "provider": params.Provider, }); terr != nil { return terr } token, terr = a.issueRefreshToken(r, tx, user, models.PasswordGrant, grantParams) if terr != nil { return terr } return nil }) if err != nil { return err } metering.RecordLogin("password", user.ID) return sendJSON(w, http.StatusOK, token) } if user.HasBeenInvited() { // Remove sensitive fields user.UserMetaData = map[string]interface{}{} user.Identities = []models.Identity{} } return sendJSON(w, http.StatusOK, user) } // sanitizeUser removes all user sensitive information from the user object // Should be used whenever we want to prevent information about whether a user is registered or not from leaking func sanitizeUser(u *models.User, params *SignupParams) (*models.User, error) { now := time.Now() u.ID = uuid.Must(uuid.NewV4()) u.Role, u.EmailChange = "", "" u.CreatedAt, u.UpdatedAt, u.ConfirmationSentAt = now, now, &now u.LastSignInAt, u.ConfirmedAt, u.EmailChangeSentAt, u.EmailConfirmedAt, u.PhoneConfirmedAt = nil, nil, nil, nil, nil u.Identities = make([]models.Identity, 0) u.UserMetaData = params.Data u.Aud = params.Aud // sanitize app_metadata u.AppMetaData = map[string]interface{}{ "provider": params.Provider, "providers": []string{params.Provider}, } // sanitize param fields switch params.Provider { case "email": u.Phone = "" case "phone": u.Email = "" default: u.Phone, u.Email = "", "" } return u, nil } func (a *API) signupNewUser(conn *storage.Connection, user *models.User) (*models.User, error) { config := a.config err := conn.Transaction(func(tx *storage.Connection) error { var terr error if terr = tx.Create(user); terr != nil { return internalServerError("Database error saving new user").WithInternalError(terr) } if terr = user.SetRole(tx, config.JWT.DefaultGroupName); terr != nil { return internalServerError("Database error updating user").WithInternalError(terr) } return nil }) if err != nil { return nil, err } // there may be triggers or generated column values in the database that will modify the // user data as it is being inserted. thus we load the user object // again to fetch those changes. if err := conn.Reload(user); err != nil { return nil, internalServerError("Database error loading user after sign-up").WithInternalError(err) } return user, nil }