Skip to content

Commit d1cf8d9

Browse files
hfJ0
andauthored
feat: refactor one-time tokens for performance (#1558)
Refactors all One-Time Tokens (OTP) used for sign-in with email, SMS, email confirmation, phone confirmation, change... to achieve: - Performance (as current method does not use an index due to the use of [partial indexes](https://github.com/supabase/auth/blob/master/migrations/20220429102000_add_unique_idx.up.sql#L10-L14) which [cannot be used in practice](https://www.postgresql.org/docs/current/indexes-partial.html)) - Future enhancements (such as OTP verification counters, adaptive OTP lengths, etc.) Summary of the change: - A new `one_time_tokens` table is added which uses a double-write mechanism with `users`. - Each new OTP is both written in the corresponding `users` column and as a new row in `one_time_tokens`. - Lookup for an OTP hash is performed first in `one_time_tokens` and if not found, using the traditional `users` approach. - In a few days, once all OTPs using the `users` columns have expired, a new change will be deployed which removes the `users` lookup. This completely solves the performance issue for looking up OTPs. - In a future change, the `one_time_tokens` table can be used to add a verification counter based on lookups on the `relates_to` (email or phone number) column, enabling new security features. --------- Co-authored-by: Joel Lee <[email protected]>
1 parent bfe4d98 commit d1cf8d9

File tree

8 files changed

+837
-285
lines changed

8 files changed

+837
-285
lines changed

internal/api/mail.go

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package api
22

33
import (
4-
"github.com/supabase/auth/internal/hooks"
5-
mail "github.com/supabase/auth/internal/mailer"
64
"net/http"
75
"strings"
86
"time"
97

8+
"github.com/supabase/auth/internal/hooks"
9+
mail "github.com/supabase/auth/internal/mailer"
10+
1011
"github.com/badoux/checkmail"
1112
"github.com/fatih/structs"
1213
"github.com/pkg/errors"
@@ -123,6 +124,13 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
123124
terr = tx.UpdateOnly(user, "recovery_token", "recovery_sent_at")
124125
if terr != nil {
125126
terr = errors.Wrap(terr, "Database error updating user for recovery")
127+
return terr
128+
}
129+
130+
terr = models.CreateOneTimeToken(tx, user.ID, user.GetEmail(), user.RecoveryToken, models.RecoveryToken)
131+
if terr != nil {
132+
terr = errors.Wrap(terr, "Database error creating recovery token in admin")
133+
return terr
126134
}
127135
case mail.InviteVerification:
128136
if user != nil {
@@ -170,6 +178,12 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
170178
terr = tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at", "invited_at")
171179
if terr != nil {
172180
terr = errors.Wrap(terr, "Database error updating user for invite")
181+
return terr
182+
}
183+
terr = models.CreateOneTimeToken(tx, user.ID, user.GetEmail(), user.ConfirmationToken, models.ConfirmationToken)
184+
if terr != nil {
185+
terr = errors.Wrap(terr, "Database error creating confirmation token for invite in admin")
186+
return terr
173187
}
174188
case mail.SignupVerification:
175189
if user != nil {
@@ -202,6 +216,12 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
202216
terr = tx.UpdateOnly(user, "confirmation_token", "confirmation_sent_at")
203217
if terr != nil {
204218
terr = errors.Wrap(terr, "Database error updating user for confirmation")
219+
return terr
220+
}
221+
terr = models.CreateOneTimeToken(tx, user.ID, user.GetEmail(), user.ConfirmationToken, models.ConfirmationToken)
222+
if terr != nil {
223+
terr = errors.Wrap(terr, "Database error creating confirmation token for signup in admin")
224+
return terr
205225
}
206226
case mail.EmailChangeCurrentVerification, mail.EmailChangeNewVerification:
207227
if !config.Mailer.SecureEmailChangeEnabled && params.Type == "email_change_current" {
@@ -228,6 +248,21 @@ func (a *API) adminGenerateLink(w http.ResponseWriter, r *http.Request) error {
228248
terr = tx.UpdateOnly(user, "email_change_token_current", "email_change_token_new", "email_change", "email_change_sent_at", "email_change_confirm_status")
229249
if terr != nil {
230250
terr = errors.Wrap(terr, "Database error updating user for email change")
251+
return terr
252+
}
253+
if user.EmailChangeTokenCurrent != "" {
254+
terr = models.CreateOneTimeToken(tx, user.ID, user.GetEmail(), user.EmailChangeTokenCurrent, models.EmailChangeTokenCurrent)
255+
if terr != nil {
256+
terr = errors.Wrap(terr, "Database error creating email change token current in admin")
257+
return terr
258+
}
259+
}
260+
if user.EmailChangeTokenNew != "" {
261+
terr = models.CreateOneTimeToken(tx, user.ID, user.EmailChange, user.EmailChangeTokenNew, models.EmailChangeTokenNew)
262+
if terr != nil {
263+
terr = errors.Wrap(terr, "Database error creating email change token new in admin")
264+
return terr
265+
}
231266
}
232267
default:
233268
return badRequestError(ErrorCodeValidationFailed, "Invalid email action link type requested: %v", params.Type)
@@ -290,6 +325,11 @@ func (a *API) sendConfirmation(r *http.Request, tx *storage.Connection, u *model
290325
return errors.Wrap(err, "Database error updating user for confirmation")
291326
}
292327

328+
err = models.CreateOneTimeToken(tx, u.ID, u.GetEmail(), u.ConfirmationToken, models.ConfirmationToken)
329+
if err != nil {
330+
return errors.Wrap(err, "Database error creating confirmation token")
331+
}
332+
293333
return nil
294334
}
295335

@@ -317,6 +357,11 @@ func (a *API) sendInvite(r *http.Request, tx *storage.Connection, u *models.User
317357
return errors.Wrap(err, "Database error updating user for invite")
318358
}
319359

360+
err = models.CreateOneTimeToken(tx, u.ID, u.GetEmail(), u.ConfirmationToken, models.ConfirmationToken)
361+
if err != nil {
362+
return errors.Wrap(err, "Database error creating confirmation token for invite")
363+
}
364+
320365
return nil
321366
}
322367

@@ -349,6 +394,11 @@ func (a *API) sendPasswordRecovery(r *http.Request, tx *storage.Connection, u *m
349394
return errors.Wrap(err, "Database error updating user for recovery")
350395
}
351396

397+
err = models.CreateOneTimeToken(tx, u.ID, u.GetEmail(), u.RecoveryToken, models.RecoveryToken)
398+
if err != nil {
399+
return errors.Wrap(err, "Database error creating recovery token")
400+
}
401+
352402
return nil
353403
}
354404

@@ -381,6 +431,11 @@ func (a *API) sendReauthenticationOtp(r *http.Request, tx *storage.Connection, u
381431
return errors.Wrap(err, "Database error updating user for reauthentication")
382432
}
383433

434+
err = models.CreateOneTimeToken(tx, u.ID, u.GetEmail(), u.ReauthenticationToken, models.ReauthenticationToken)
435+
if err != nil {
436+
return errors.Wrap(err, "Database error creating reauthentication token")
437+
}
438+
384439
return nil
385440
}
386441

@@ -416,6 +471,11 @@ func (a *API) sendMagicLink(r *http.Request, tx *storage.Connection, u *models.U
416471
return errors.Wrap(err, "Database error updating user for recovery")
417472
}
418473

474+
err = models.CreateOneTimeToken(tx, u.ID, u.GetEmail(), u.RecoveryToken, models.RecoveryToken)
475+
if err != nil {
476+
return errors.Wrap(err, "Database error creating recovery token")
477+
}
478+
419479
return nil
420480
}
421481

@@ -469,6 +529,20 @@ func (a *API) sendEmailChange(r *http.Request, tx *storage.Connection, u *models
469529
return errors.Wrap(err, "Database error updating user for email change")
470530
}
471531

532+
if u.EmailChangeTokenCurrent != "" {
533+
err = models.CreateOneTimeToken(tx, u.ID, u.GetEmail(), u.EmailChangeTokenCurrent, models.EmailChangeTokenCurrent)
534+
if err != nil {
535+
return errors.Wrap(err, "Database error creating email change token current")
536+
}
537+
}
538+
539+
if u.EmailChangeTokenNew != "" {
540+
err = models.CreateOneTimeToken(tx, u.ID, u.EmailChange, u.EmailChangeTokenNew, models.EmailChangeTokenNew)
541+
if err != nil {
542+
return errors.Wrap(err, "Database error creating email change token new")
543+
}
544+
}
545+
472546
return nil
473547
}
474548

internal/api/phone.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, r *http.Request, tx *st
9595
if err != nil {
9696
return "", err
9797
}
98+
9899
// Hook should only be called if SMS autoconfirm is disabled
99100
if !config.Sms.Autoconfirm && config.Hook.SendSMS.Enabled {
100101
input := hooks.SendSMSInput{
@@ -109,7 +110,6 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, r *http.Request, tx *st
109110
return "", err
110111
}
111112
} else {
112-
113113
messageID, err = smsProvider.SendMessage(phone, message, channel, otp)
114114
if err != nil {
115115
return messageID, err
@@ -128,7 +128,26 @@ func (a *API) sendPhoneConfirmation(ctx context.Context, r *http.Request, tx *st
128128
user.ReauthenticationSentAt = &now
129129
}
130130

131-
return messageID, errors.Wrap(tx.UpdateOnly(user, includeFields...), "Database error updating user for confirmation")
131+
if err := tx.UpdateOnly(user, includeFields...); err != nil {
132+
return messageID, errors.Wrap(err, "Database error updating user for phone")
133+
}
134+
135+
switch otpType {
136+
case phoneConfirmationOtp:
137+
if err := models.CreateOneTimeToken(tx, user.ID, user.GetPhone(), user.ConfirmationToken, models.ConfirmationToken); err != nil {
138+
return messageID, errors.Wrap(err, "Database error creating confirmation token for phone")
139+
}
140+
case phoneChangeVerification:
141+
if err := models.CreateOneTimeToken(tx, user.ID, user.PhoneChange, user.PhoneChangeToken, models.PhoneChangeToken); err != nil {
142+
return messageID, errors.Wrap(err, "Database error creating phone change token")
143+
}
144+
case phoneReauthenticationOtp:
145+
if err := models.CreateOneTimeToken(tx, user.ID, user.GetPhone(), user.ReauthenticationToken, models.ReauthenticationToken); err != nil {
146+
return messageID, errors.Wrap(err, "Database error creating reauthentication token for phone")
147+
}
148+
}
149+
150+
return messageID, nil
132151
}
133152

134153
func generateSMSFromTemplate(SMSTemplate *template.Template, otp string) (string, error) {

internal/api/verify.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,28 @@ func (a *API) emailChangeVerify(r *http.Request, conn *storage.Connection, param
479479
config := a.config
480480
if config.Mailer.SecureEmailChangeEnabled && user.EmailChangeConfirmStatus == zeroConfirmation && user.GetEmail() != "" {
481481
err := conn.Transaction(func(tx *storage.Connection) error {
482+
currentOTT, terr := models.FindOneTimeToken(tx, params.TokenHash, models.EmailChangeTokenCurrent)
483+
if terr != nil && !models.IsNotFoundError(terr) {
484+
return terr
485+
}
486+
487+
newOTT, terr := models.FindOneTimeToken(tx, params.TokenHash, models.EmailChangeTokenNew)
488+
if terr != nil && !models.IsNotFoundError(terr) {
489+
return terr
490+
}
491+
482492
user.EmailChangeConfirmStatus = singleConfirmation
483-
if params.Token == user.EmailChangeTokenCurrent || params.TokenHash == user.EmailChangeTokenCurrent {
493+
494+
if params.Token == user.EmailChangeTokenCurrent || params.TokenHash == user.EmailChangeTokenCurrent || (currentOTT != nil && params.TokenHash == currentOTT.TokenHash) {
484495
user.EmailChangeTokenCurrent = ""
485-
} else if params.Token == user.EmailChangeTokenNew || params.TokenHash == user.EmailChangeTokenNew {
496+
if terr := models.ClearOneTimeTokenForUser(tx, user.ID, models.EmailChangeTokenCurrent); terr != nil {
497+
return terr
498+
}
499+
} else if params.Token == user.EmailChangeTokenNew || params.TokenHash == user.EmailChangeTokenNew || (newOTT != nil && params.TokenHash == newOTT.TokenHash) {
486500
user.EmailChangeTokenNew = ""
501+
if terr := models.ClearOneTimeTokenForUser(tx, user.ID, models.EmailChangeTokenNew); terr != nil {
502+
return terr
503+
}
487504
}
488505
if terr := tx.UpdateOnly(user, "email_change_confirm_status", "email_change_token_current", "email_change_token_new"); terr != nil {
489506
return terr

0 commit comments

Comments
 (0)