From d0b56d417d1c83fe1c8b23a9a82015df0b9a64d9 Mon Sep 17 00:00:00 2001 From: Chris Stockton Date: Fri, 13 Jun 2025 12:47:09 -0700 Subject: [PATCH] fix: invites should send another email when user exists * Added a test for double invites * Fixed control flow to send another invite Bug introduced in: https://github.com/supabase/auth/pull/2034 Fixes: https://github.com/supabase/auth/issues/2057 --- internal/api/invite.go | 60 ++++++++++++++++++++++--------------- internal/api/invite_test.go | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 24 deletions(-) diff --git a/internal/api/invite.go b/internal/api/invite.go index 797931004..1a8a0b2d1 100644 --- a/internal/api/invite.go +++ b/internal/api/invite.go @@ -37,38 +37,50 @@ func (a *API) Invite(w http.ResponseWriter, r *http.Request) error { if err != nil && !models.IsNotFoundError(err) { return apierrors.NewInternalServerError("Database error finding user").WithInternalError(err) } - if user != nil && user.IsConfirmed() { - return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeEmailExists, DuplicateEmailMsg) - } - signupParams := SignupParams{ - Email: params.Email, - Data: params.Data, - Aud: aud, - Provider: "email", - } + isCreate := user == nil + isConfirmed := user != nil && user.IsConfirmed() - user, err = signupParams.ToUserModel(false /* <- isSSOUser */) - if err != nil { - return err - } - if err := a.triggerBeforeUserCreated(r, db, user); err != nil { - return err - } + if isCreate { + signupParams := SignupParams{ + Email: params.Email, + Data: params.Data, + Aud: aud, + Provider: "email", + } - err = db.Transaction(func(tx *storage.Connection) error { - user, err = a.signupNewUser(tx, user) + // because params above sets no password, this method + // is not computationally hard so it can be used within + // a database transaction + user, err = signupParams.ToUserModel(false /* <- isSSOUser */) if err != nil { return err } - identity, err := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{ - Subject: user.ID.String(), - Email: user.GetEmail(), - })) - if err != nil { + + if err := a.triggerBeforeUserCreated(r, db, user); err != nil { return err } - user.Identities = []models.Identity{*identity} + } + + err = db.Transaction(func(tx *storage.Connection) error { + if !isCreate { + if isConfirmed { + return apierrors.NewUnprocessableEntityError(apierrors.ErrorCodeEmailExists, DuplicateEmailMsg) + } + } else { + user, err = a.signupNewUser(tx, user) + if err != nil { + return err + } + identity, err := a.createNewIdentity(tx, user, "email", structs.Map(provider.Claims{ + Subject: user.ID.String(), + Email: user.GetEmail(), + })) + if err != nil { + return err + } + user.Identities = []models.Identity{*identity} + } if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UserInvitedAction, "", map[string]interface{}{ "user_id": user.ID, diff --git a/internal/api/invite_test.go b/internal/api/invite_test.go index ff0baca6a..bd4fdd648 100644 --- a/internal/api/invite_test.go +++ b/internal/api/invite_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/gofrs/uuid" jwt "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -101,6 +102,57 @@ func (ts *InviteTestSuite) TestInvite() { assert.Equal(ts.T(), http.StatusOK, w.Code) } +func (ts *InviteTestSuite) TestInviteExists() { + // To allow us to send signup and invite request in succession + ts.Config.SMTP.MaxFrequency = 200 + + email := uuid.Must(uuid.NewV4()).String() + "@example.com" + + { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "email": email, + "data": map[string]interface{}{ + "a": 1, + }, + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), http.StatusOK, w.Code) + } + + { + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "email": email, + "data": map[string]interface{}{ + "a": 1, + }, + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "http://localhost/invite", &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + // Setup response recorder + w := httptest.NewRecorder() + + ts.API.handler.ServeHTTP(w, req) + assert.Equal(ts.T(), http.StatusOK, w.Code) + } +} + func (ts *InviteTestSuite) TestInviteAfterSignupShouldNotReturnSensitiveFields() { // To allow us to send signup and invite request in succession ts.Config.SMTP.MaxFrequency = 5