• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / gotrue / 8370607605

21 Mar 2024 06:24AM UTC coverage: 65.015% (-0.2%) from 65.241%
8370607605

Pull #1474

github

J0
fix: split error codes
Pull Request #1474: feat: add custom sms hook

76 of 169 new or added lines in 12 files covered. (44.97%)

68 existing lines in 1 file now uncovered.

8006 of 12314 relevant lines covered (65.02%)

59.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.94
/internal/api/signup.go
1
package api
2

3
import (
4
        "context"
5
        "fmt"
6
        "net/http"
7
        "time"
8

9
        "github.com/fatih/structs"
10
        "github.com/gofrs/uuid"
11
        "github.com/pkg/errors"
12
        "github.com/supabase/auth/internal/api/provider"
13
        "github.com/supabase/auth/internal/api/sms_provider"
14
        "github.com/supabase/auth/internal/metering"
15
        "github.com/supabase/auth/internal/models"
16
        "github.com/supabase/auth/internal/storage"
17
        "github.com/supabase/auth/internal/utilities"
18
)
19

20
// SignupParams are the parameters the Signup endpoint accepts
21
type SignupParams struct {
22
        Email               string                 `json:"email"`
23
        Phone               string                 `json:"phone"`
24
        Password            string                 `json:"password"`
25
        Data                map[string]interface{} `json:"data"`
26
        Provider            string                 `json:"-"`
27
        Aud                 string                 `json:"-"`
28
        Channel             string                 `json:"channel"`
29
        CodeChallengeMethod string                 `json:"code_challenge_method"`
30
        CodeChallenge       string                 `json:"code_challenge"`
31
}
32

33
func (a *API) validateSignupParams(ctx context.Context, p *SignupParams) error {
21✔
34
        config := a.config
21✔
35

21✔
36
        if p.Password == "" {
21✔
37
                return badRequestError(ErrorCodeValidationFailed, "Signup requires a valid password")
×
38
        }
×
39

40
        if err := a.checkPasswordStrength(ctx, p.Password); err != nil {
21✔
41
                return err
×
42
        }
×
43
        if p.Email != "" && p.Phone != "" {
21✔
44
                return badRequestError(ErrorCodeValidationFailed, "Only an email address or phone number should be provided on signup.")
×
45
        }
×
46
        if p.Provider == "phone" && !sms_provider.IsValidMessageChannel(p.Channel, config.Sms.Provider) {
21✔
47
                return badRequestError(ErrorCodeValidationFailed, InvalidChannelError)
×
48
        }
×
49
        // PKCE not needed as phone signups already return access token in body
50
        if p.Phone != "" && p.CodeChallenge != "" {
21✔
51
                return badRequestError(ErrorCodeValidationFailed, "PKCE not supported for phone signups")
×
52
        }
×
53
        if err := validatePKCEParams(p.CodeChallengeMethod, p.CodeChallenge); err != nil {
21✔
54
                return err
×
55
        }
×
56

57
        return nil
21✔
58
}
59

60
func (p *SignupParams) ConfigureDefaults() {
21✔
61
        if p.Email != "" {
37✔
62
                p.Provider = "email"
16✔
63
        } else if p.Phone != "" {
26✔
64
                p.Provider = "phone"
5✔
65
        }
5✔
66
        if p.Data == nil {
34✔
67
                p.Data = make(map[string]interface{})
13✔
68
        }
13✔
69

70
        // For backwards compatibility, we default to SMS if params Channel is not specified
71
        if p.Phone != "" && p.Channel == "" {
25✔
72
                p.Channel = sms_provider.SMSProvider
4✔
73
        }
4✔
74
}
75

76
func (params *SignupParams) ToUserModel(isSSOUser bool) (user *models.User, err error) {
58✔
77
        switch params.Provider {
58✔
78
        case "email":
18✔
79
                user, err = models.NewUser("", params.Email, params.Password, params.Aud, params.Data)
18✔
80
        case "phone":
5✔
81
                user, err = models.NewUser(params.Phone, "", params.Password, params.Aud, params.Data)
5✔
82
        case "anonymous":
10✔
83
                user, err = models.NewUser("", "", "", params.Aud, params.Data)
10✔
84
                user.IsAnonymous = true
10✔
85
        default:
25✔
86
                // handles external provider case
25✔
87
                user, err = models.NewUser("", params.Email, params.Password, params.Aud, params.Data)
25✔
88
        }
89
        if err != nil {
58✔
90
                err = internalServerError("Database error creating user").WithInternalError(err)
×
91
                return
×
92
        }
×
93
        user.IsSSOUser = isSSOUser
58✔
94
        if user.AppMetaData == nil {
116✔
95
                user.AppMetaData = make(map[string]interface{})
58✔
96
        }
58✔
97

98
        user.Identities = make([]models.Identity, 0)
58✔
99

58✔
100
        if params.Provider != "anonymous" {
106✔
101
                // TODO: Deprecate "provider" field
48✔
102
                user.AppMetaData["provider"] = params.Provider
48✔
103

48✔
104
                user.AppMetaData["providers"] = []string{params.Provider}
48✔
105
        }
48✔
106

107
        return user, nil
58✔
108
}
109

110
// Signup is the endpoint for registering a new user
111
func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
21✔
112
        ctx := r.Context()
21✔
113
        config := a.config
21✔
114
        db := a.db.WithContext(ctx)
21✔
115

21✔
116
        if config.DisableSignup {
21✔
117
                return unprocessableEntityError(ErrorCodeSignupDisabled, "Signups not allowed for this instance")
×
118
        }
×
119

120
        params := &SignupParams{}
21✔
121
        if err := retrieveRequestParams(r, params); err != nil {
21✔
122
                return err
×
123
        }
×
124

125
        params.ConfigureDefaults()
21✔
126

21✔
127
        if err := a.validateSignupParams(ctx, params); err != nil {
21✔
128
                return err
×
129
        }
×
130

131
        var err error
21✔
132
        flowType := getFlowFromChallenge(params.CodeChallenge)
21✔
133

21✔
134
        var user *models.User
21✔
135
        var grantParams models.GrantParams
21✔
136

21✔
137
        grantParams.FillGrantParams(r)
21✔
138

21✔
139
        params.Aud = a.requestAud(ctx, r)
21✔
140

21✔
141
        switch params.Provider {
21✔
142
        case "email":
16✔
143
                if !config.External.Email.Enabled {
16✔
144
                        return badRequestError(ErrorCodeEmailProviderDisabled, "Email signups are disabled")
×
145
                }
×
146
                params.Email, err = validateEmail(params.Email)
16✔
147
                if err != nil {
16✔
148
                        return err
×
149
                }
×
150
                user, err = models.IsDuplicatedEmail(db, params.Email, params.Aud, nil)
16✔
151
        case "phone":
5✔
152
                if !config.External.Phone.Enabled {
5✔
153
                        return badRequestError(ErrorCodePhoneProviderDisabled, "Phone signups are disabled")
×
154
                }
×
155
                params.Phone, err = validatePhone(params.Phone)
5✔
156
                if err != nil {
5✔
157
                        return err
×
158
                }
×
159
                user, err = models.FindUserByPhoneAndAudience(db, params.Phone, params.Aud)
5✔
160
        default:
×
161
                msg := ""
×
162
                if config.External.Email.Enabled && config.External.Phone.Enabled {
×
163
                        msg = "Sign up only available with email or phone provider"
×
164
                } else if config.External.Email.Enabled {
×
165
                        msg = "Sign up only available with email provider"
×
166
                } else if config.External.Phone.Enabled {
×
167
                        msg = "Sign up only available with phone provider"
×
168
                } else {
×
169
                        msg = "Sign up with this provider not possible"
×
170
                }
×
171

172
                return badRequestError(ErrorCodeValidationFailed, msg)
×
173
        }
174

175
        if err != nil && !models.IsNotFoundError(err) {
21✔
176
                return internalServerError("Database error finding user").WithInternalError(err)
×
177
        }
×
178

179
        var signupUser *models.User
21✔
180
        if user == nil {
36✔
181
                // always call this outside of a database transaction as this method
15✔
182
                // can be computationally hard and block due to password hashing
15✔
183
                signupUser, err = params.ToUserModel(false /* <- isSSOUser */)
15✔
184
                if err != nil {
15✔
185
                        return err
×
186
                }
×
187
        }
188

189
        err = db.Transaction(func(tx *storage.Connection) error {
42✔
190
                var terr error
21✔
191
                if user != nil {
27✔
192
                        if (params.Provider == "email" && user.IsConfirmed()) || (params.Provider == "phone" && user.IsPhoneConfirmed()) {
7✔
193
                                return UserExistsError
1✔
194
                        }
1✔
195
                        // do not update the user because we can't be sure of their claimed identity
196
                } else {
15✔
197
                        user, terr = a.signupNewUser(tx, signupUser)
15✔
198
                        if terr != nil {
15✔
199
                                return terr
×
200
                        }
×
201
                }
202
                identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), "email")
20✔
203
                if terr != nil {
38✔
204
                        if !models.IsNotFoundError(terr) {
18✔
205
                                return terr
×
206
                        }
×
207
                        identityData := structs.Map(provider.Claims{
18✔
208
                                Subject: user.ID.String(),
18✔
209
                                Email:   user.GetEmail(),
18✔
210
                        })
18✔
211
                        for k, v := range params.Data {
21✔
212
                                if _, ok := identityData[k]; !ok {
6✔
213
                                        identityData[k] = v
3✔
214
                                }
3✔
215
                        }
216
                        identity, terr = a.createNewIdentity(tx, user, params.Provider, identityData)
18✔
217
                        if terr != nil {
18✔
218
                                return terr
×
219
                        }
×
220
                        if terr := user.RemoveUnconfirmedIdentities(tx, identity); terr != nil {
18✔
221
                                return terr
×
222
                        }
×
223
                }
224
                user.Identities = []models.Identity{*identity}
20✔
225

20✔
226
                if params.Provider == "email" && !user.IsConfirmed() {
35✔
227
                        if config.Mailer.Autoconfirm {
23✔
228
                                if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{
8✔
229
                                        "provider": params.Provider,
8✔
230
                                }); terr != nil {
8✔
231
                                        return terr
×
232
                                }
×
233
                                if terr = user.Confirm(tx); terr != nil {
8✔
234
                                        return internalServerError("Database error updating user").WithInternalError(terr)
×
235
                                }
×
236
                        } else {
7✔
237
                                mailer := a.Mailer(ctx)
7✔
238
                                referrer := utilities.GetReferrer(r, config)
7✔
239
                                if terr = models.NewAuditLogEntry(r, tx, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{
7✔
240
                                        "provider": params.Provider,
7✔
241
                                }); terr != nil {
7✔
242
                                        return terr
×
243
                                }
×
244
                                if isPKCEFlow(flowType) {
8✔
245
                                        _, terr := generateFlowState(tx, params.Provider, models.EmailSignup, params.CodeChallengeMethod, params.CodeChallenge, &user.ID)
1✔
246
                                        if terr != nil {
1✔
247
                                                return terr
×
248
                                        }
×
249
                                }
250
                                externalURL := getExternalHost(ctx)
7✔
251
                                if terr = sendConfirmation(tx, user, mailer, config.SMTP.MaxFrequency, referrer, externalURL, config.Mailer.OtpLength, flowType); terr != nil {
7✔
252
                                        if errors.Is(terr, MaxFrequencyLimitError) {
×
253
                                                now := time.Now()
×
254
                                                left := user.ConfirmationSentAt.Add(config.SMTP.MaxFrequency).Sub(now) / time.Second
×
255
                                                return tooManyRequestsError(ErrorCodeOverEmailSendRateLimit, fmt.Sprintf("For security purposes, you can only request this after %d seconds.", left))
×
256
                                        }
×
257
                                        return internalServerError("Error sending confirmation mail").WithInternalError(terr)
×
258
                                }
259
                        }
260
                } else if params.Provider == "phone" && !user.IsPhoneConfirmed() {
10✔
261
                        if config.Sms.Autoconfirm {
5✔
262
                                if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", map[string]interface{}{
×
263
                                        "provider": params.Provider,
×
264
                                        "channel":  params.Channel,
×
265
                                }); terr != nil {
×
266
                                        return terr
×
267
                                }
×
268
                                if terr = user.ConfirmPhone(tx); terr != nil {
×
269
                                        return internalServerError("Database error updating user").WithInternalError(terr)
×
270
                                }
×
271
                        } else {
5✔
272
                                if terr = models.NewAuditLogEntry(r, tx, user, models.UserConfirmationRequestedAction, "", map[string]interface{}{
5✔
273
                                        "provider": params.Provider,
5✔
274
                                }); terr != nil {
5✔
275
                                        return terr
×
276
                                }
×
277
                                smsProvider, terr := sms_provider.GetSmsProvider(*config)
5✔
278
                                if terr != nil {
10✔
279
                                        return internalServerError("Unable to get SMS provider").WithInternalError(terr)
5✔
280
                                }
5✔
NEW
281
                                if _, terr := a.sendPhoneConfirmation(ctx, r, tx, user, params.Phone, phoneConfirmationOtp, smsProvider, params.Channel); terr != nil {
×
282
                                        return unprocessableEntityError(ErrorCodeSMSSendFailed, "Error sending confirmation sms: %v", terr).WithInternalError(terr)
×
283
                                }
×
284
                        }
285
                }
286

287
                return nil
15✔
288
        })
289

290
        if err != nil {
27✔
291
                reason := ErrorCodeOverEmailSendRateLimit
6✔
292
                if params.Provider == "phone" {
11✔
293
                        reason = ErrorCodeOverSMSSendRateLimit
5✔
294
                }
5✔
295

296
                if errors.Is(err, MaxFrequencyLimitError) {
6✔
297
                        return tooManyRequestsError(reason, "For security purposes, you can only request this once every minute")
×
298
                } else if errors.Is(err, UserExistsError) {
7✔
299
                        err = db.Transaction(func(tx *storage.Connection) error {
2✔
300
                                if terr := models.NewAuditLogEntry(r, tx, user, models.UserRepeatedSignUpAction, "", map[string]interface{}{
1✔
301
                                        "provider": params.Provider,
1✔
302
                                }); terr != nil {
1✔
303
                                        return terr
×
304
                                }
×
305
                                return nil
1✔
306
                        })
307
                        if err != nil {
1✔
308
                                return err
×
309
                        }
×
310
                        if config.Mailer.Autoconfirm || config.Sms.Autoconfirm {
1✔
311
                                return unprocessableEntityError(ErrorCodeUserAlreadyExists, "User already registered")
×
312
                        }
×
313
                        sanitizedUser, err := sanitizeUser(user, params)
1✔
314
                        if err != nil {
1✔
315
                                return err
×
316
                        }
×
317
                        return sendJSON(w, http.StatusOK, sanitizedUser)
1✔
318
                }
319
                return err
5✔
320
        }
321

322
        // handles case where Mailer.Autoconfirm is true or Phone.Autoconfirm is true
323
        if user.IsConfirmed() || user.IsPhoneConfirmed() {
23✔
324
                var token *AccessTokenResponse
8✔
325
                err = db.Transaction(func(tx *storage.Connection) error {
16✔
326
                        var terr error
8✔
327
                        if terr = models.NewAuditLogEntry(r, tx, user, models.LoginAction, "", map[string]interface{}{
8✔
328
                                "provider": params.Provider,
8✔
329
                        }); terr != nil {
8✔
330
                                return terr
×
331
                        }
×
332
                        token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant, grantParams)
8✔
333

8✔
334
                        if terr != nil {
8✔
335
                                return terr
×
336
                        }
×
337

338
                        if terr = a.setCookieTokens(config, token, false, w); terr != nil {
8✔
339
                                return internalServerError("Failed to set JWT cookie. %s", terr)
×
340
                        }
×
341
                        return nil
8✔
342
                })
343
                if err != nil {
8✔
344
                        return err
×
345
                }
×
346
                metering.RecordLogin("password", user.ID)
8✔
347
                return sendJSON(w, http.StatusOK, token)
8✔
348
        }
349
        if user.HasBeenInvited() {
8✔
350
                // Remove sensitive fields
1✔
351
                user.UserMetaData = map[string]interface{}{}
1✔
352
                user.Identities = []models.Identity{}
1✔
353
        }
1✔
354
        return sendJSON(w, http.StatusOK, user)
7✔
355
}
356

357
// sanitizeUser removes all user sensitive information from the user object
358
// Should be used whenever we want to prevent information about whether a user is registered or not from leaking
359
func sanitizeUser(u *models.User, params *SignupParams) (*models.User, error) {
1✔
360
        now := time.Now()
1✔
361

1✔
362
        u.ID = uuid.Must(uuid.NewV4())
1✔
363

1✔
364
        u.Role = ""
1✔
365
        u.CreatedAt, u.UpdatedAt, u.ConfirmationSentAt = now, now, &now
1✔
366
        u.LastSignInAt, u.ConfirmedAt, u.EmailConfirmedAt, u.PhoneConfirmedAt = nil, nil, nil, nil
1✔
367
        u.Identities = make([]models.Identity, 0)
1✔
368
        u.UserMetaData = params.Data
1✔
369
        u.Aud = params.Aud
1✔
370

1✔
371
        // sanitize app_metadata
1✔
372
        u.AppMetaData = map[string]interface{}{
1✔
373
                "provider":  params.Provider,
1✔
374
                "providers": []string{params.Provider},
1✔
375
        }
1✔
376

1✔
377
        // sanitize param fields
1✔
378
        switch params.Provider {
1✔
379
        case "email":
1✔
380
                u.Phone = ""
1✔
381
        case "phone":
×
382
                u.Email = ""
×
383
        default:
×
384
                u.Phone, u.Email = "", ""
×
385
        }
386

387
        return u, nil
1✔
388
}
389

390
func (a *API) signupNewUser(conn *storage.Connection, user *models.User) (*models.User, error) {
55✔
391
        config := a.config
55✔
392

55✔
393
        err := conn.Transaction(func(tx *storage.Connection) error {
110✔
394
                var terr error
55✔
395
                if terr = tx.Create(user); terr != nil {
55✔
396
                        return internalServerError("Database error saving new user").WithInternalError(terr)
×
397
                }
×
398
                if terr = user.SetRole(tx, config.JWT.DefaultGroupName); terr != nil {
55✔
399
                        return internalServerError("Database error updating user").WithInternalError(terr)
×
400
                }
×
401
                return nil
55✔
402
        })
403
        if err != nil {
55✔
404
                return nil, err
×
405
        }
×
406

407
        // there may be triggers or generated column values in the database that will modify the
408
        // user data as it is being inserted. thus we load the user object
409
        // again to fetch those changes.
410
        if err := conn.Reload(user); err != nil {
55✔
411
                return nil, internalServerError("Database error loading user after sign-up").WithInternalError(err)
×
412
        }
×
413

414
        return user, nil
55✔
415
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc