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

supabase / gotrue / 8135412125

04 Mar 2024 03:34AM UTC coverage: 65.142% (+0.1%) from 65.009%
8135412125

push

github

web-flow
fix: refactor request params to use generics (#1464)

## What kind of change does this PR introduce?
* Introduce a new method `retrieveRequestParams` which makes use of
generics to parse a request
* This will help to simplify parsing a request from:
```go

params := RequestParams{}
body, err := getBodyBytes(r)
if err != nil {
  return nil, badRequestError("Could not read body").WithInternalError(err)
}

if err := json.Unmarshal(body, &params); err != nil {
  return nil, badRequestError("Could not decode request params: %v", err)
}
```
to 
```go
params := &Request{}
err := retrieveRequestParams(req, params)
```

## TODO
- [x] Add type constraint instead of using `any`

48 of 69 new or added lines in 19 files covered. (69.57%)

19 existing lines in 14 files now uncovered.

7806 of 11983 relevant lines covered (65.14%)

59.29 hits per line

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

73.5
/internal/api/verify.go
1
package api
2

3
import (
4
        "context"
5
        "errors"
6
        "net/http"
7
        "net/url"
8
        "strconv"
9
        "strings"
10
        "time"
11

12
        "github.com/fatih/structs"
13
        "github.com/sethvargo/go-password/password"
14
        "github.com/supabase/auth/internal/api/provider"
15
        "github.com/supabase/auth/internal/api/sms_provider"
16
        "github.com/supabase/auth/internal/crypto"
17
        "github.com/supabase/auth/internal/models"
18
        "github.com/supabase/auth/internal/observability"
19
        "github.com/supabase/auth/internal/storage"
20
        "github.com/supabase/auth/internal/utilities"
21
)
22

23
const (
24
        signupVerification      = "signup"
25
        recoveryVerification    = "recovery"
26
        inviteVerification      = "invite"
27
        magicLinkVerification   = "magiclink"
28
        emailChangeVerification = "email_change"
29
        smsVerification         = "sms"
30
        phoneChangeVerification = "phone_change"
31
        // includes signupVerification and magicLinkVerification
32
        emailOTPVerification = "email"
33
)
34

35
const (
36
        zeroConfirmation int = iota
37
        singleConfirmation
38
)
39

40
// Only applicable when SECURE_EMAIL_CHANGE_ENABLED
41
const singleConfirmationAccepted = "Confirmation link accepted. Please proceed to confirm link sent to the other email"
42

43
// VerifyParams are the parameters the Verify endpoint accepts
44
type VerifyParams struct {
45
        Type       string `json:"type"`
46
        Token      string `json:"token"`
47
        TokenHash  string `json:"token_hash"`
48
        Email      string `json:"email"`
49
        Phone      string `json:"phone"`
50
        RedirectTo string `json:"redirect_to"`
51
}
52

53
func (p *VerifyParams) Validate(r *http.Request) error {
57✔
54
        var err error
57✔
55
        if p.Type == "" {
58✔
56
                return badRequestError("Verify requires a verification type")
1✔
57
        }
1✔
58
        switch r.Method {
56✔
59
        case http.MethodGet:
32✔
60
                if p.Token == "" {
32✔
61
                        return badRequestError("Verify requires a token or a token hash")
×
62
                }
×
63
                // TODO: deprecate the token query param from GET /verify and use token_hash instead (breaking change)
64
                p.TokenHash = p.Token
32✔
65
        case http.MethodPost:
24✔
66
                if (p.Token == "" && p.TokenHash == "") || (p.Token != "" && p.TokenHash != "") {
25✔
67
                        return badRequestError("Verify requires either a token or a token hash")
1✔
68
                }
1✔
69
                if p.Token != "" {
37✔
70
                        if isPhoneOtpVerification(p) {
19✔
71
                                p.Phone, err = validatePhone(p.Phone)
5✔
72
                                if err != nil {
5✔
73
                                        return err
×
74
                                }
×
75
                                p.TokenHash = crypto.GenerateTokenHash(p.Phone, p.Token)
5✔
76
                        } else if isEmailOtpVerification(p) {
17✔
77
                                p.Email, err = validateEmail(p.Email)
8✔
78
                                if err != nil {
8✔
79
                                        return unprocessableEntityError("Invalid email format").WithInternalError(err)
×
80
                                }
×
81
                                p.TokenHash = crypto.GenerateTokenHash(p.Email, p.Token)
8✔
82
                        } else {
1✔
83
                                return badRequestError("Only an email address or phone number should be provided on verify")
1✔
84
                        }
1✔
85
                } else if p.TokenHash != "" {
18✔
86
                        if p.Email != "" || p.Phone != "" || p.RedirectTo != "" {
9✔
87
                                return badRequestError("Only the token_hash and type should be provided")
×
88
                        }
×
89
                }
90
        default:
×
91
                return nil
×
92
        }
93
        return nil
54✔
94
}
95

96
// Verify exchanges a confirmation or recovery token to a refresh token
97
func (a *API) Verify(w http.ResponseWriter, r *http.Request) error {
51✔
98
        params := &VerifyParams{}
51✔
99
        switch r.Method {
51✔
100
        case http.MethodGet:
31✔
101
                params.Token = r.FormValue("token")
31✔
102
                params.Type = r.FormValue("type")
31✔
103
                params.RedirectTo = utilities.GetReferrer(r, a.config)
31✔
104
                if err := params.Validate(r); err != nil {
31✔
105
                        return err
×
106
                }
×
107
                return a.verifyGet(w, r, params)
31✔
108
        case http.MethodPost:
20✔
109
                if err := retrieveRequestParams(r, params); err != nil {
20✔
NEW
110
                        return err
×
UNCOV
111
                }
×
112
                if err := params.Validate(r); err != nil {
20✔
113
                        return err
×
114
                }
×
115
                return a.verifyPost(w, r, params)
20✔
116
        default:
×
117
                return unprocessableEntityError("Only GET and POST methods are supported.")
×
118
        }
119
}
120

121
func (a *API) verifyGet(w http.ResponseWriter, r *http.Request, params *VerifyParams) error {
31✔
122
        ctx := r.Context()
31✔
123
        db := a.db.WithContext(ctx)
31✔
124
        config := a.config
31✔
125

31✔
126
        var (
31✔
127
                user        *models.User
31✔
128
                grantParams models.GrantParams
31✔
129
                err         error
31✔
130
                token       *AccessTokenResponse
31✔
131
                authCode    string
31✔
132
        )
31✔
133

31✔
134
        grantParams.FillGrantParams(r)
31✔
135

31✔
136
        flowType := models.ImplicitFlow
31✔
137
        var authenticationMethod models.AuthenticationMethod
31✔
138
        if strings.HasPrefix(params.Token, PKCEPrefix) {
37✔
139
                flowType = models.PKCEFlow
6✔
140
                authenticationMethod, err = models.ParseAuthenticationMethod(params.Type)
6✔
141
                if err != nil {
6✔
142
                        return err
×
143
                }
×
144
        }
145
        err = db.Transaction(func(tx *storage.Connection) error {
62✔
146
                var terr error
31✔
147
                user, terr = a.verifyTokenHash(tx, params)
31✔
148
                if terr != nil {
38✔
149
                        return terr
7✔
150
                }
7✔
151
                switch params.Type {
24✔
152
                case signupVerification, inviteVerification:
14✔
153
                        user, terr = a.signupVerify(r, ctx, tx, user)
14✔
154
                case recoveryVerification, magicLinkVerification:
6✔
155
                        user, terr = a.recoverVerify(r, tx, user)
6✔
156
                case emailChangeVerification:
4✔
157
                        user, terr = a.emailChangeVerify(r, tx, params, user)
4✔
158
                        if user == nil && terr == nil {
6✔
159
                                // when double confirmation is required
2✔
160
                                rurl, err := a.prepRedirectURL(singleConfirmationAccepted, params.RedirectTo, flowType)
2✔
161
                                if err != nil {
2✔
162
                                        return err
×
163
                                }
×
164
                                http.Redirect(w, r, rurl, http.StatusSeeOther)
2✔
165
                                return nil
2✔
166
                        }
167
                default:
×
168
                        return unprocessableEntityError("Unsupported verification type")
×
169
                }
170

171
                if terr != nil {
22✔
172
                        return terr
×
173
                }
×
174

175
                if terr := user.UpdateAppMetaDataProviders(tx); terr != nil {
22✔
176
                        return terr
×
177
                }
×
178

179
                // Reload user model from db.
180
                // This is important for refreshing the data in any generated columns like IsAnonymous.
181
                if terr := tx.Reload(user); err != nil {
22✔
182
                        return terr
×
183
                }
×
184
                if isImplicitFlow(flowType) {
39✔
185
                        token, terr = a.issueRefreshToken(ctx, tx, user, models.OTP, grantParams)
17✔
186

17✔
187
                        if terr != nil {
17✔
188
                                return terr
×
189
                        }
×
190

191
                        if terr = a.setCookieTokens(config, token, false, w); terr != nil {
17✔
192
                                return internalServerError("Failed to set JWT cookie. %s", terr)
×
193
                        }
×
194
                } else if isPKCEFlow(flowType) {
10✔
195
                        if authCode, terr = issueAuthCode(tx, user, authenticationMethod); terr != nil {
5✔
196
                                return badRequestError("No associated flow state found. %s", terr)
×
197
                        }
×
198
                }
199
                return nil
22✔
200
        })
201

202
        if err != nil {
38✔
203
                var herr *HTTPError
7✔
204
                if errors.As(err, &herr) {
14✔
205
                        rurl, err := a.prepErrorRedirectURL(herr, r, params.RedirectTo, flowType)
7✔
206
                        if err != nil {
7✔
207
                                return err
×
208
                        }
×
209
                        http.Redirect(w, r, rurl, http.StatusSeeOther)
7✔
210
                        return nil
7✔
211
                }
212
        }
213
        rurl := params.RedirectTo
24✔
214
        if isImplicitFlow(flowType) && token != nil {
41✔
215
                q := url.Values{}
17✔
216
                q.Set("type", params.Type)
17✔
217
                rurl = token.AsRedirectURL(rurl, q)
17✔
218
        } else if isPKCEFlow(flowType) {
30✔
219
                rurl, err = a.prepPKCERedirectURL(rurl, authCode)
6✔
220
                if err != nil {
6✔
221
                        return err
×
222
                }
×
223
        }
224
        http.Redirect(w, r, rurl, http.StatusSeeOther)
24✔
225
        return nil
24✔
226
}
227

228
func (a *API) verifyPost(w http.ResponseWriter, r *http.Request, params *VerifyParams) error {
20✔
229
        ctx := r.Context()
20✔
230
        db := a.db.WithContext(ctx)
20✔
231
        config := a.config
20✔
232

20✔
233
        var (
20✔
234
                user        *models.User
20✔
235
                grantParams models.GrantParams
20✔
236
                token       *AccessTokenResponse
20✔
237
        )
20✔
238
        var isSingleConfirmationResponse = false
20✔
239

20✔
240
        grantParams.FillGrantParams(r)
20✔
241

20✔
242
        err := db.Transaction(func(tx *storage.Connection) error {
40✔
243
                var terr error
20✔
244
                aud := a.requestAud(ctx, r)
20✔
245

20✔
246
                if isUsingTokenHash(params) {
28✔
247
                        user, terr = a.verifyTokenHash(tx, params)
8✔
248
                } else {
20✔
249
                        user, terr = a.verifyUserAndToken(tx, params, aud)
12✔
250
                }
12✔
251
                if terr != nil {
25✔
252
                        return terr
5✔
253
                }
5✔
254

255
                switch params.Type {
15✔
256
                case signupVerification, inviteVerification:
5✔
257
                        user, terr = a.signupVerify(r, ctx, tx, user)
5✔
258
                case recoveryVerification, magicLinkVerification:
2✔
259
                        user, terr = a.recoverVerify(r, tx, user)
2✔
260
                case emailChangeVerification:
6✔
261
                        user, terr = a.emailChangeVerify(r, tx, params, user)
6✔
262
                        if user == nil && terr == nil {
10✔
263
                                isSingleConfirmationResponse = true
4✔
264
                                return nil
4✔
265
                        }
4✔
266
                case smsVerification, phoneChangeVerification:
2✔
267
                        user, terr = a.smsVerify(r, tx, user, params)
2✔
268
                default:
×
269
                        return unprocessableEntityError("Unsupported verification type")
×
270
                }
271

272
                if terr != nil {
11✔
273
                        return terr
×
274
                }
×
275

276
                if terr := user.UpdateAppMetaDataProviders(tx); terr != nil {
11✔
277
                        return terr
×
278
                }
×
279

280
                // Reload user model from db.
281
                // This is important for refreshing the data in any generated columns like IsAnonymous.
282
                if terr := tx.Reload(user); terr != nil {
11✔
283
                        return terr
×
284
                }
×
285
                token, terr = a.issueRefreshToken(ctx, tx, user, models.OTP, grantParams)
11✔
286
                if terr != nil {
11✔
287
                        return terr
×
288
                }
×
289

290
                if terr = a.setCookieTokens(config, token, false, w); terr != nil {
11✔
291
                        return internalServerError("Failed to set JWT cookie. %s", terr)
×
292
                }
×
293
                return nil
11✔
294
        })
295
        if err != nil {
25✔
296
                return err
5✔
297
        }
5✔
298
        if isSingleConfirmationResponse {
19✔
299
                return sendJSON(w, http.StatusOK, map[string]string{
4✔
300
                        "msg":  singleConfirmationAccepted,
4✔
301
                        "code": strconv.Itoa(http.StatusOK),
4✔
302
                })
4✔
303
        }
4✔
304
        return sendJSON(w, http.StatusOK, token)
11✔
305
}
306

307
func (a *API) signupVerify(r *http.Request, ctx context.Context, conn *storage.Connection, user *models.User) (*models.User, error) {
19✔
308
        if user.EncryptedPassword == "" && user.InvitedAt != nil {
21✔
309
                // sign them up with temporary password, and require application
2✔
310
                // to present the user with a password set form
2✔
311
                password, err := password.Generate(64, 10, 0, false, true)
2✔
312
                if err != nil {
2✔
313
                        return nil, err
×
314
                }
×
315

316
                if err := user.SetPassword(ctx, password); err != nil {
2✔
317
                        return nil, err
×
318
                }
×
319
        }
320

321
        err := conn.Transaction(func(tx *storage.Connection) error {
38✔
322
                var terr error
19✔
323
                if user.EncryptedPassword == "" && user.InvitedAt != nil {
19✔
324
                        if terr = user.UpdatePassword(tx, nil); terr != nil {
×
325
                                return internalServerError("Error storing password").WithInternalError(terr)
×
326
                        }
×
327
                }
328

329
                if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", nil); terr != nil {
19✔
330
                        return terr
×
331
                }
×
332

333
                if terr = user.Confirm(tx); terr != nil {
19✔
334
                        return internalServerError("Error confirming user").WithInternalError(terr)
×
335
                }
×
336
                return nil
19✔
337
        })
338
        if err != nil {
19✔
339
                return nil, err
×
340
        }
×
341
        return user, nil
19✔
342
}
343

344
func (a *API) recoverVerify(r *http.Request, conn *storage.Connection, user *models.User) (*models.User, error) {
8✔
345
        err := conn.Transaction(func(tx *storage.Connection) error {
16✔
346
                var terr error
8✔
347
                if terr = user.Recover(tx); terr != nil {
8✔
348
                        return terr
×
349
                }
×
350
                if !user.IsConfirmed() {
14✔
351
                        if terr = models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", nil); terr != nil {
6✔
352
                                return terr
×
353
                        }
×
354

355
                        if terr = user.Confirm(tx); terr != nil {
6✔
356
                                return terr
×
357
                        }
×
358
                } else {
2✔
359
                        if terr = models.NewAuditLogEntry(r, tx, user, models.LoginAction, "", nil); terr != nil {
2✔
360
                                return terr
×
361
                        }
×
362
                }
363
                return nil
8✔
364
        })
365

366
        if err != nil {
8✔
367
                return nil, internalServerError("Database error updating user").WithInternalError(err)
×
368
        }
×
369
        return user, nil
8✔
370
}
371

372
func (a *API) smsVerify(r *http.Request, conn *storage.Connection, user *models.User, params *VerifyParams) (*models.User, error) {
3✔
373

3✔
374
        err := conn.Transaction(func(tx *storage.Connection) error {
6✔
375

3✔
376
                if params.Type == smsVerification {
4✔
377
                        if terr := models.NewAuditLogEntry(r, tx, user, models.UserSignedUpAction, "", nil); terr != nil {
1✔
378
                                return terr
×
379
                        }
×
380
                        if terr := user.ConfirmPhone(tx); terr != nil {
1✔
381
                                return internalServerError("Error confirming user").WithInternalError(terr)
×
382
                        }
×
383
                } else if params.Type == phoneChangeVerification {
4✔
384
                        if terr := models.NewAuditLogEntry(r, tx, user, models.UserModifiedAction, "", nil); terr != nil {
2✔
385
                                return terr
×
386
                        }
×
387
                        if identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), "phone"); terr != nil {
4✔
388
                                if !models.IsNotFoundError(terr) {
2✔
389
                                        return terr
×
390
                                }
×
391
                                // confirming the phone change should create a new phone identity if the user doesn't have one
392
                                if _, terr = a.createNewIdentity(tx, user, "phone", structs.Map(provider.Claims{
2✔
393
                                        Subject:       user.ID.String(),
2✔
394
                                        Phone:         params.Phone,
2✔
395
                                        PhoneVerified: true,
2✔
396
                                })); terr != nil {
2✔
397
                                        return terr
×
398
                                }
×
399
                        } else {
×
400
                                if terr := identity.UpdateIdentityData(tx, map[string]interface{}{
×
401
                                        "phone":          params.Phone,
×
402
                                        "phone_verified": true,
×
403
                                }); terr != nil {
×
404
                                        return terr
×
405
                                }
×
406
                        }
407
                        if terr := user.ConfirmPhoneChange(tx); terr != nil {
2✔
408
                                return internalServerError("Error confirming user").WithInternalError(terr)
×
409
                        }
×
410
                }
411

412
                if terr := tx.Load(user, "Identities"); terr != nil {
3✔
413
                        return internalServerError("Error refetching identities").WithInternalError(terr)
×
414
                }
×
415
                return nil
3✔
416
        })
417
        if err != nil {
3✔
418
                return nil, err
×
419
        }
×
420
        return user, nil
3✔
421
}
422

423
func (a *API) prepErrorRedirectURL(err *HTTPError, r *http.Request, rurl string, flowType models.FlowType) (string, error) {
11✔
424
        u, perr := url.Parse(rurl)
11✔
425
        if perr != nil {
11✔
426
                return "", err
×
427
        }
×
428
        q := u.Query()
11✔
429

11✔
430
        // Maintain separate query params for hash and query
11✔
431
        hq := url.Values{}
11✔
432
        log := observability.GetLogEntry(r)
11✔
433
        errorID := getRequestID(r.Context())
11✔
434
        err.ErrorID = errorID
11✔
435
        log.WithError(err.Cause()).Info(err.Error())
11✔
436
        if str, ok := oauthErrorMap[err.Code]; ok {
22✔
437
                hq.Set("error", str)
11✔
438
                q.Set("error", str)
11✔
439
        }
11✔
440
        hq.Set("error_code", strconv.Itoa(err.Code))
11✔
441
        hq.Set("error_description", err.Message)
11✔
442

11✔
443
        q.Set("error_code", strconv.Itoa(err.Code))
11✔
444
        q.Set("error_description", err.Message)
11✔
445
        if flowType == models.PKCEFlow {
13✔
446
                // Additionally, may override existing error query param if set to PKCE.
2✔
447
                u.RawQuery = q.Encode()
2✔
448
        }
2✔
449
        // Left as hash fragment to comply with spec.
450
        u.Fragment = hq.Encode()
11✔
451
        return u.String(), nil
11✔
452
}
453

454
func (a *API) prepRedirectURL(message string, rurl string, flowType models.FlowType) (string, error) {
6✔
455
        u, perr := url.Parse(rurl)
6✔
456
        if perr != nil {
6✔
457
                return "", perr
×
458
        }
×
459
        hq := url.Values{}
6✔
460
        q := u.Query()
6✔
461
        hq.Set("message", message)
6✔
462
        if flowType == models.PKCEFlow {
9✔
463
                q.Set("message", message)
3✔
464
        }
3✔
465
        u.RawQuery = q.Encode()
6✔
466
        u.Fragment = hq.Encode()
6✔
467
        return u.String(), nil
6✔
468
}
469

470
func (a *API) prepPKCERedirectURL(rurl, code string) (string, error) {
10✔
471
        u, err := url.Parse(rurl)
10✔
472
        if err != nil {
10✔
473
                return "", err
×
474
        }
×
475
        q := u.Query()
10✔
476
        q.Set("code", code)
10✔
477
        u.RawQuery = q.Encode()
10✔
478
        return u.String(), nil
10✔
479
}
480

481
func (a *API) emailChangeVerify(r *http.Request, conn *storage.Connection, params *VerifyParams, user *models.User) (*models.User, error) {
10✔
482
        config := a.config
10✔
483
        if config.Mailer.SecureEmailChangeEnabled && user.EmailChangeConfirmStatus == zeroConfirmation && user.GetEmail() != "" {
16✔
484
                err := conn.Transaction(func(tx *storage.Connection) error {
12✔
485
                        user.EmailChangeConfirmStatus = singleConfirmation
6✔
486
                        if params.Token == user.EmailChangeTokenCurrent || params.TokenHash == user.EmailChangeTokenCurrent {
9✔
487
                                user.EmailChangeTokenCurrent = ""
3✔
488
                        } else if params.Token == user.EmailChangeTokenNew || params.TokenHash == user.EmailChangeTokenNew {
9✔
489
                                user.EmailChangeTokenNew = ""
3✔
490
                        }
3✔
491
                        if terr := tx.UpdateOnly(user, "email_change_confirm_status", "email_change_token_current", "email_change_token_new"); terr != nil {
6✔
492
                                return terr
×
493
                        }
×
494
                        return nil
6✔
495
                })
496
                if err != nil {
6✔
497
                        return nil, err
×
498
                }
×
499
                return nil, nil
6✔
500
        }
501

502
        // one email is confirmed at this point if GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED is enabled
503
        err := conn.Transaction(func(tx *storage.Connection) error {
8✔
504
                if terr := models.NewAuditLogEntry(r, tx, user, models.UserModifiedAction, "", nil); terr != nil {
4✔
505
                        return terr
×
506
                }
×
507

508
                if identity, terr := models.FindIdentityByIdAndProvider(tx, user.ID.String(), "email"); terr != nil {
7✔
509
                        if !models.IsNotFoundError(terr) {
3✔
510
                                return terr
×
511
                        }
×
512
                        // confirming the email change should create a new email identity if the user doesn't have one
513
                        if _, terr = a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{
3✔
514
                                Subject:       user.ID.String(),
3✔
515
                                Email:         user.EmailChange,
3✔
516
                                EmailVerified: true,
3✔
517
                        })); terr != nil {
3✔
518
                                return terr
×
519
                        }
×
520
                } else {
1✔
521
                        if terr := identity.UpdateIdentityData(tx, map[string]interface{}{
1✔
522
                                "email":          user.EmailChange,
1✔
523
                                "email_verified": true,
1✔
524
                        }); terr != nil {
1✔
525
                                return terr
×
526
                        }
×
527
                }
528
                if user.IsAnonymous {
5✔
529
                        user.IsAnonymous = false
1✔
530
                        if terr := tx.UpdateOnly(user, "is_anonymous"); terr != nil {
1✔
531
                                return terr
×
532
                        }
×
533
                }
534
                if terr := tx.Load(user, "Identities"); terr != nil {
4✔
535
                        return internalServerError("Error refetching identities").WithInternalError(terr)
×
536
                }
×
537
                if terr := user.ConfirmEmailChange(tx, zeroConfirmation); terr != nil {
4✔
538
                        return internalServerError("Error confirm email").WithInternalError(terr)
×
539
                }
×
540

541
                return nil
4✔
542
        })
543
        if err != nil {
4✔
544
                return nil, err
×
545
        }
×
546

547
        return user, nil
4✔
548
}
549

550
func (a *API) verifyTokenHash(conn *storage.Connection, params *VerifyParams) (*models.User, error) {
39✔
551
        config := a.config
39✔
552

39✔
553
        var user *models.User
39✔
554
        var err error
39✔
555
        switch params.Type {
39✔
556
        case emailOTPVerification:
1✔
557
                // need to find user by confirmation token or recovery token with the token hash
1✔
558
                user, err = models.FindUserByConfirmationOrRecoveryToken(conn, params.TokenHash)
1✔
559
        case signupVerification, inviteVerification:
19✔
560
                user, err = models.FindUserByConfirmationToken(conn, params.TokenHash)
19✔
561
        case recoveryVerification, magicLinkVerification:
8✔
562
                user, err = models.FindUserByRecoveryToken(conn, params.TokenHash)
8✔
563
        case emailChangeVerification:
11✔
564
                user, err = models.FindUserByEmailChangeToken(conn, params.TokenHash)
11✔
565
        default:
×
566
                return nil, badRequestError("Invalid email verification type")
×
567
        }
568

569
        if err != nil {
41✔
570
                if models.IsNotFoundError(err) {
4✔
571
                        return nil, expiredTokenError("Email link is invalid or has expired").WithInternalError(err)
2✔
572
                }
2✔
573
                return nil, internalServerError("Database error finding user from email link").WithInternalError(err)
×
574
        }
575

576
        if user.IsBanned() {
42✔
577
                return nil, unauthorizedError("Error confirming user").WithInternalMessage("user is banned")
5✔
578
        }
5✔
579

580
        var isExpired bool
32✔
581
        switch params.Type {
32✔
582
        case emailOTPVerification:
1✔
583
                sentAt := user.ConfirmationSentAt
1✔
584
                params.Type = "signup"
1✔
585
                if user.RecoveryToken == params.TokenHash {
2✔
586
                        sentAt = user.RecoverySentAt
1✔
587
                        params.Type = "magiclink"
1✔
588
                }
1✔
589
                isExpired = isOtpExpired(sentAt, config.Mailer.OtpExp)
1✔
590
        case signupVerification, inviteVerification:
16✔
591
                isExpired = isOtpExpired(user.ConfirmationSentAt, config.Mailer.OtpExp)
16✔
592
        case recoveryVerification, magicLinkVerification:
6✔
593
                isExpired = isOtpExpired(user.RecoverySentAt, config.Mailer.OtpExp)
6✔
594
        case emailChangeVerification:
9✔
595
                isExpired = isOtpExpired(user.EmailChangeSentAt, config.Mailer.OtpExp)
9✔
596
        }
597

598
        if isExpired {
33✔
599
                return nil, expiredTokenError("Email link is invalid or has expired").WithInternalMessage("email link has expired")
1✔
600
        }
1✔
601

602
        return user, nil
31✔
603
}
604

605
// verifyUserAndToken verifies the token associated to the user based on the verify type
606
func (a *API) verifyUserAndToken(conn *storage.Connection, params *VerifyParams, aud string) (*models.User, error) {
12✔
607
        config := a.config
12✔
608

12✔
609
        var user *models.User
12✔
610
        var err error
12✔
611
        tokenHash := params.TokenHash
12✔
612

12✔
613
        switch params.Type {
12✔
614
        case phoneChangeVerification:
2✔
615
                user, err = models.FindUserByPhoneChangeAndAudience(conn, params.Phone, aud)
2✔
616
        case smsVerification:
3✔
617
                user, err = models.FindUserByPhoneAndAudience(conn, params.Phone, aud)
3✔
618
        case emailChangeVerification:
1✔
619
                // Since the email change could be trigger via the implicit or PKCE flow,
1✔
620
                // the query used has to also check if the token saved in the db contains the pkce_ prefix
1✔
621
                user, err = models.FindUserForEmailChange(conn, params.Email, tokenHash, aud, config.Mailer.SecureEmailChangeEnabled)
1✔
622
        default:
6✔
623
                user, err = models.FindUserByEmailAndAudience(conn, params.Email, aud)
6✔
624
        }
625

626
        if err != nil {
12✔
627
                if models.IsNotFoundError(err) {
×
628
                        return nil, notFoundError(err.Error()).WithInternalError(err)
×
629
                }
×
630
                return nil, internalServerError("Database error finding user").WithInternalError(err)
×
631
        }
632

633
        if user.IsBanned() {
12✔
634
                return nil, unauthorizedError("Error confirming user").WithInternalMessage("user is banned")
×
635
        }
×
636

637
        var isValid bool
12✔
638

12✔
639
        smsProvider, _ := sms_provider.GetSmsProvider(*config)
12✔
640
        switch params.Type {
12✔
641
        case emailOTPVerification:
1✔
642
                // if the type is emailOTPVerification, we'll check both the confirmation_token and recovery_token columns
1✔
643
                if isOtpValid(tokenHash, user.ConfirmationToken, user.ConfirmationSentAt, config.Mailer.OtpExp) {
2✔
644
                        isValid = true
1✔
645
                        params.Type = signupVerification
1✔
646
                } else if isOtpValid(tokenHash, user.RecoveryToken, user.RecoverySentAt, config.Mailer.OtpExp) {
1✔
647
                        isValid = true
×
648
                        params.Type = magicLinkVerification
×
649
                } else {
×
650
                        isValid = false
×
651
                }
×
652
        case signupVerification, inviteVerification:
4✔
653
                isValid = isOtpValid(tokenHash, user.ConfirmationToken, user.ConfirmationSentAt, config.Mailer.OtpExp)
4✔
654
        case recoveryVerification, magicLinkVerification:
1✔
655
                isValid = isOtpValid(tokenHash, user.RecoveryToken, user.RecoverySentAt, config.Mailer.OtpExp)
1✔
656
        case emailChangeVerification:
1✔
657
                isValid = isOtpValid(tokenHash, user.EmailChangeTokenCurrent, user.EmailChangeSentAt, config.Mailer.OtpExp) ||
1✔
658
                        isOtpValid(tokenHash, user.EmailChangeTokenNew, user.EmailChangeSentAt, config.Mailer.OtpExp)
1✔
659
        case phoneChangeVerification, smsVerification:
5✔
660
                phone := params.Phone
5✔
661
                sentAt := user.ConfirmationSentAt
5✔
662
                expectedToken := user.ConfirmationToken
5✔
663
                if params.Type == phoneChangeVerification {
7✔
664
                        phone = user.PhoneChange
2✔
665
                        sentAt = user.PhoneChangeSentAt
2✔
666
                        expectedToken = user.PhoneChangeToken
2✔
667
                }
2✔
668
                if config.Sms.IsTwilioVerifyProvider() {
5✔
669
                        if testOTP, ok := config.Sms.GetTestOTP(params.Phone, time.Now()); ok {
×
670
                                if params.Token == testOTP {
×
671
                                        return user, nil
×
672
                                }
×
673
                        }
674
                        if err := smsProvider.(*sms_provider.TwilioVerifyProvider).VerifyOTP(phone, params.Token); err != nil {
×
675
                                return nil, expiredTokenError("Token has expired or is invalid").WithInternalError(err)
×
676
                        }
×
677
                        return user, nil
×
678
                }
679
                isValid = isOtpValid(tokenHash, expectedToken, sentAt, config.Sms.OtpExp)
5✔
680
        }
681

682
        if !isValid {
16✔
683
                return nil, expiredTokenError("Token has expired or is invalid").WithInternalMessage("token has expired or is invalid")
4✔
684
        }
4✔
685
        return user, nil
8✔
686
}
687

688
// isOtpValid checks the actual otp sent against the expected otp and ensures that it's within the valid window
689
func isOtpValid(actual, expected string, sentAt *time.Time, otpExp uint) bool {
14✔
690
        if expected == "" || sentAt == nil {
15✔
691
                return false
1✔
692
        }
1✔
693
        return !isOtpExpired(sentAt, otpExp) && ((actual == expected) || ("pkce_"+actual == expected))
13✔
694
}
695

696
func isOtpExpired(sentAt *time.Time, otpExp uint) bool {
45✔
697
        return time.Now().After(sentAt.Add(time.Second * time.Duration(otpExp)))
45✔
698
}
45✔
699

700
// isPhoneOtpVerification checks if the verification came from a phone otp
701
func isPhoneOtpVerification(params *VerifyParams) bool {
14✔
702
        return params.Phone != "" && params.Email == ""
14✔
703
}
14✔
704

705
// isEmailOtpVerification checks if the verification came from an email otp
706
func isEmailOtpVerification(params *VerifyParams) bool {
9✔
707
        return params.Phone == "" && params.Email != ""
9✔
708
}
9✔
709

710
func isUsingTokenHash(params *VerifyParams) bool {
20✔
711
        return params.TokenHash != "" && params.Token == "" && params.Phone == "" && params.Email == ""
20✔
712
}
20✔
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