From 5b513a9a1536eb3dd73048d9e2f406da4760bc6e Mon Sep 17 00:00:00 2001 From: Abhinav Dubey Date: Thu, 6 Aug 2026 16:49:38 +0530 Subject: [PATCH] fix: persist user ID across before-user-created hook for external providers --- internal/api/external.go | 20 ++++++-------------- internal/api/hooks.go | 10 +++++++--- internal/api/provider/provider.go | 4 +++- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/internal/api/external.go b/internal/api/external.go index e87589d8e0..504a955ac2 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -334,26 +334,18 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. Data: identityData, } - // This is a little bit of a hack. Let me explain: When - // is_sso_user == true, it allows there to be different user - // rows with the same email address. Initially it was added to - // support SSO accounts, but at this point renaming the column - // or adding a new one requires re-indexing the table which is - // expensive and introduces a potentially unnecessary API - // surface change. It therefore set to true for other linking - // domains, not just SSO ones. This enables different linking - // domains to co-exist, such as when using - // GOTRUE_EXPERIMENTAL_PROVIDER_LINKING_DOMAINS="provider_a=social,provider_b=social". isSSOUser := decision.LinkingDomain != "default" - // because params above sets no password, this method is not - // computationally hard so it can be used within a database - // transaction user, terr = params.ToUserModel(isSSOUser) if terr != nil { return 0, nil, terr } + // Re-use the user ID populated during triggerBeforeUserCreatedExternal + if userData.UserID != uuid.Nil { + user.ID = userData.UserID + } + if user, terr = a.signupNewUser(tx, user); terr != nil { return 0, nil, terr } @@ -362,7 +354,7 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http. return 0, nil, terr } user.Identities = append(user.Identities, *identity) - + case models.AccountExists: user = decision.User identity = decision.Identities[0] diff --git a/internal/api/hooks.go b/internal/api/hooks.go index 7b02f4889b..ade7fac04d 100644 --- a/internal/api/hooks.go +++ b/internal/api/hooks.go @@ -21,8 +21,6 @@ func (a *API) triggerAfterUserCreated( return nil } - // We still check tx because we want to make sure we aren't calling this - // trigger in code paths that haven't actually created the user yet. if err := checkTX(conn); err != nil { return err } @@ -113,6 +111,12 @@ func (a *API) triggerBeforeUserCreatedExternal( if err != nil { return err } + + // Attach pre-generated user ID to userData so downstream callbacks re-use it + if userData != nil && user != nil { + userData.UserID = user.ID + } + return a.triggerBeforeUserCreated(r, db, user) } @@ -122,4 +126,4 @@ func checkTX(conn *storage.Connection) error { "unable to trigger hooks during transaction") } return nil -} +} \ No newline at end of file diff --git a/internal/api/provider/provider.go b/internal/api/provider/provider.go index c3ab0781f2..405bb61332 100644 --- a/internal/api/provider/provider.go +++ b/internal/api/provider/provider.go @@ -10,6 +10,7 @@ import ( "os" "time" + "github.com/gofrs/uuid" "github.com/supabase/auth/internal/utilities" "golang.org/x/oauth2" ) @@ -131,6 +132,7 @@ type Email struct { // UserProvidedData is a struct that contains the user's data returned from the oauth provider type UserProvidedData struct { + UserID uuid.UUID // Persists the pre-generated User ID across hook triggers and account creation (Issue #41309) Emails []Email Metadata *Claims } @@ -182,4 +184,4 @@ func makeRequest(ctx context.Context, tok *oauth2.Token, g *oauth2.Config, url s } return nil -} +} \ No newline at end of file