diff --git a/api/admin.go b/api/admin.go index 5d7a681216..fb85bcd13e 100644 --- a/api/admin.go +++ b/api/admin.go @@ -236,7 +236,7 @@ func (a *API) adminUserCreate(w http.ResponseWriter, r *http.Request) error { if user.AppMetaData == nil { user.AppMetaData = make(map[string]interface{}) } - user.AppMetaData["provider"] = "email" + user.AppMetaData["provider"] = []string{"email"} err = a.db.Transaction(func(tx *storage.Connection) error { if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserSignedUpAction, map[string]interface{}{ diff --git a/api/admin_test.go b/api/admin_test.go index 36bd35aaea..06e11b02d5 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -54,7 +54,10 @@ func (ts *AdminTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + identities, err := models.FindIdentitiesByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error retrieving identities") + + token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} @@ -69,7 +72,11 @@ func (ts *AdminTestSuite) makeSuperAdmin(email string) string { func (ts *AdminTestSuite) makeSystemUser() string { u := models.NewSystemUser(uuid.Nil, ts.Config.JWT.Aud) u.Role = "service_role" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + + identities, err := models.FindIdentitiesByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error retrieving identities") + + token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} @@ -278,7 +285,7 @@ func (ts *AdminTestSuite) TestAdminUserCreate() { require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) assert.Equal(ts.T(), "test1@example.com", data.GetEmail()) assert.Equal(ts.T(), "123456789", data.GetPhone()) - assert.Equal(ts.T(), "email", data.AppMetaData["provider"]) + assert.Equal(ts.T(), []interface{}{"email"}, data.AppMetaData["provider"]) } // TestAdminUserGet tests API /admin/user route (GET) diff --git a/api/audit_test.go b/api/audit_test.go index b6a3fdd08b..a797233044 100644 --- a/api/audit_test.go +++ b/api/audit_test.go @@ -51,7 +51,9 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + identities, err := models.FindIdentitiesByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error retrieving identities") + token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} diff --git a/api/external.go b/api/external.go index 1ea3c7fe9b..28cfbd3f22 100644 --- a/api/external.go +++ b/api/external.go @@ -15,7 +15,6 @@ import ( "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" "github.com/sirupsen/logrus" - "golang.org/x/oauth2" ) // ExternalProviderClaims are the JWT claims sent as the state in the external oauth provider signup flow @@ -86,16 +85,6 @@ func (a *API) ExternalProviderRedirect(w http.ResponseWriter, r *http.Request) e if err != nil { return internalServerError("Error storing request token in session").WithInternalError(err) } - case *provider.AppleProvider: - opts := make([]oauth2.AuthCodeOption, 0, 1) - opts = append(opts, oauth2.SetAuthURLParam("response_mode", "form_post")) - authURL = externalProvider.Config.AuthCodeURL(tokenString, opts...) - if authURL != "" { - if u, err := url.Parse(authURL); err == nil { - u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%20") - authURL = u.String() - } - } default: authURL = p.AuthCodeURL(tokenString) } @@ -152,53 +141,86 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re } } else { aud := a.requestAud(ctx, r) - - // search user using all available emails var emailData provider.Email - for _, e := range userData.Emails { - if e.Verified || config.Mailer.Autoconfirm { - user, terr = models.FindUserByEmailAndAudience(tx, instanceID, e.Email, aud) - if terr != nil && !models.IsNotFoundError(terr) { - return internalServerError("Error checking for duplicate users").WithInternalError(terr) - } - - if user != nil { - emailData = e - break - } + var identityData map[string]interface{} + if userData.Metadata != nil { + identityData, terr = userData.Metadata.ToMap() + if terr != nil { + return terr } } - if user == nil { - if config.DisableSignup { - return forbiddenError("Signups not allowed for this instance") - } - - // prefer primary email for new signups - emailData = userData.Emails[0] - for _, e := range userData.Emails { - if e.Primary { - emailData = e - break + var identity *models.Identity + // check if identity exists + if identity, terr = models.FindIdentityByIdAndProvider(tx, userData.Metadata.Subject, providerType); terr != nil { + if models.IsNotFoundError(terr) { + // search user using all available emails + for _, e := range userData.Emails { + if e.Verified || config.Mailer.Autoconfirm { + user, terr = models.FindUserByEmailAndAudience(tx, instanceID, e.Email, aud) + if terr != nil && !models.IsNotFoundError(terr) { + return internalServerError("Error checking for duplicate users").WithInternalError(terr) + } + if user != nil { + emailData = e + break + } + } } - } - - params := &SignupParams{ - Provider: providerType, - Email: emailData.Email, - Aud: aud, - Data: make(map[string]interface{}), - } - for k, v := range userData.Metadata { - if v != "" { - params.Data[k] = v + if user != nil { + if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil { + return terr + } + if terr = user.UpdateAppMetaDataProvider(tx); terr != nil { + return terr + } + } else { + if config.DisableSignup { + return forbiddenError("Signups not allowed for this instance") + } + + // prefer primary email for new signups + emailData = userData.Emails[0] + for _, e := range userData.Emails { + if e.Primary { + emailData = e + break + } + } + + params := &SignupParams{ + Provider: providerType, + Email: emailData.Email, + Aud: aud, + Data: identityData, + } + + user, terr = a.signupNewUser(ctx, tx, params) + if terr != nil { + return terr + } + + if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil { + return terr + } } + } else { + return terr } + } - user, terr = a.signupNewUser(ctx, tx, params) + if identity != nil && user == nil { + // get user associated with identity + user, terr = models.FindUserByID(tx, identity.UserID) if terr != nil { return terr } + if terr = tx.UpdateOnly(identity, "identity_data", "last_sign_in_at"); terr != nil { + return terr + } + if terr = user.UpdateAppMetaDataProvider(tx); terr != nil { + return terr + } } if !user.IsConfirmed() { @@ -281,19 +303,20 @@ func (a *API) processInvite(ctx context.Context, tx *storage.Connection, userDat return nil, badRequestError("Invited email does not match emails from external provider").WithInternalMessage("invited=%s external=%s", user.Email, strings.Join(emails, ", ")) } - if err := user.UpdateAppMetaData(tx, map[string]interface{}{ - "provider": providerType, - }); err != nil { - return nil, internalServerError("Database error updating user").WithInternalError(err) - } - - updates := make(map[string]interface{}) - for k, v := range userData.Metadata { - if v != "" { - updates[k] = v + var identityData map[string]interface{} + if userData.Metadata != nil { + identityData, err = userData.Metadata.ToMap() + if err != nil { + return nil, internalServerError("Error serialising user metadata").WithInternalError(err) } } - if err := user.UpdateUserMetaData(tx, updates); err != nil { + if _, err := a.createNewIdentity(tx, user, providerType, identityData); err != nil { + return nil, err + } + if err = user.UpdateAppMetaDataProvider(tx); err != nil { + return nil, err + } + if err := user.UpdateUserMetaData(tx, identityData); err != nil { return nil, internalServerError("Database error updating user").WithInternalError(err) } @@ -417,3 +440,23 @@ func (a *API) getExternalRedirectURL(r *http.Request) string { } return config.SiteURL } + +func (a *API) createNewIdentity(conn *storage.Connection, user *models.User, providerType string, identityData map[string]interface{}) (*models.Identity, error) { + identity, err := models.NewIdentity(user, providerType, identityData) + if err != nil { + return nil, err + } + + err = conn.Transaction(func(tx *storage.Connection) error { + if terr := tx.Create(identity); terr != nil { + return internalServerError("Error creating identity").WithInternalError(terr) + } + return nil + }) + + if err != nil { + return nil, err + } + + return identity, nil +} diff --git a/api/external_azure_test.go b/api/external_azure_test.go index 40874db211..0626e9a716 100644 --- a/api/external_azure_test.go +++ b/api/external_azure_test.go @@ -9,6 +9,11 @@ import ( jwt "github.com/golang-jwt/jwt" ) +const ( + azureUser string = `{"name":"Azure Test","email":"azure@example.com","sub":"azuretestid"}` + azureUserNoEmail string = `{"name":"Azure Test","sub":"azuretestid"}` +) + func (ts *ExternalTestSuite) TestSignupExternalAzure() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=azure", nil) w := httptest.NewRecorder() @@ -61,23 +66,21 @@ func AzureTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int func (ts *ExternalTestSuite) TestSignupExternalAzure_AuthorizationCode() { ts.Config.DisableSignup = false - ts.createUser("azure@example.com", "Azure Test", "", "") + ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "") tokenCount, userCount := 0, 0 code := "authcode" - azureUser := `{"name":"Azure Test","email":"azure@example.com"}` server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "") } func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoUser() { ts.Config.DisableSignup = true tokenCount, userCount := 0, 0 code := "authcode" - azureUser := `{"name":"Azure Test","email":"azure@example.com"}` server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser) defer server.Close() @@ -90,8 +93,7 @@ func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoEmai ts.Config.DisableSignup = true tokenCount, userCount := 0, 0 code := "authcode" - azureUser := `{"name":"Azure Test"}` - server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser) + server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUserNoEmail) defer server.Close() u := performAuthorization(ts, "azure", code, "") @@ -103,32 +105,30 @@ func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoEmai func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("azure@example.com", "Azure Test", "", "") + ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "") tokenCount, userCount := 0, 0 code := "authcode" - azureUser := `{"name":"Azure Test","email":"azure@example.com"}` server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "") } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureSuccessWhenMatchingToken() { // name and avatar should be populated from Azure API - ts.createUser("azure@example.com", "", "", "invite_token") + ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - azureUser := `{"name":"Azure Test","email":"azure@example.com"}}` server := AzureTestSignupSetup(ts, &tokenCount, &userCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "azure@example.com", "Azure Test", "azuretestid", "") } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenNoMatchingToken() { @@ -143,7 +143,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenNoMatchingToke } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenWrongToken() { - ts.createUser("azure@example.com", "", "", "invite_token") + ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" @@ -156,7 +156,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenWrongToken() { } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenEmailDoesntMatch() { - ts.createUser("azure@example.com", "", "", "invite_token") + ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" diff --git a/api/external_bitbucket_test.go b/api/external_bitbucket_test.go index 29d9d27875..ab48a1e1b8 100644 --- a/api/external_bitbucket_test.go +++ b/api/external_bitbucket_test.go @@ -9,6 +9,10 @@ import ( jwt "github.com/golang-jwt/jwt" ) +const ( + bitbucketUser string = `{"uuid":"bitbucketTestId","display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}` +) + func (ts *ExternalTestSuite) TestSignupExternalBitbucket() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=bitbucket", nil) w := httptest.NewRecorder() @@ -66,14 +70,13 @@ func (ts *ExternalTestSuite) TestSignupExternalBitbucket_AuthorizationCode() { ts.Config.DisableSignup = false tokenCount, userCount := 0, 0 code := "authcode" - bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}` emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}` server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails) defer server.Close() u := performAuthorization(ts, "bitbucket", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNoUser() { @@ -94,7 +97,6 @@ func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNo ts.Config.DisableSignup = true tokenCount, userCount := 0, 0 code := "authcode" - bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}` emails := `{"values":[{}]}` server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails) defer server.Close() @@ -108,51 +110,48 @@ func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupErrorWhenNo func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar", "") + ts.createUser("bitbucketTestId", "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" - bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}` emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}` server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails) defer server.Close() u := performAuthorization(ts, "bitbucket", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalBitbucketDisableSignupSuccessWithSecondaryEmail() { ts.Config.DisableSignup = true - ts.createUser("secondary@example.com", "Bitbucket Test", "http://example.com/avatar", "") + ts.createUser("bitbucketTestId", "secondary@example.com", "Bitbucket Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" - bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}` emails := `{"values":[{"email":"primary@example.com","is_primary":true,"is_confirmed":true},{"email":"secondary@example.com","is_primary":false,"is_confirmed":true}]}` server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails) defer server.Close() u := performAuthorization(ts, "bitbucket", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "Bitbucket Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketSuccessWhenMatchingToken() { // name and avatar should be populated from Bitbucket API - ts.createUser("bitbucket@example.com", "", "", "invite_token") + ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}` emails := `{"values":[{"email":"bitbucket@example.com","is_primary":true,"is_confirmed":true}]}` server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails) defer server.Close() u := performAuthorization(ts, "bitbucket", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "bitbucket@example.com", "Bitbucket Test", "bitbucketTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenNoMatchingToken() { @@ -168,7 +167,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenNoMatching } func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenWrongToken() { - ts.createUser("bitbucket@example.com", "", "", "invite_token") + ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" @@ -182,11 +181,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenWrongToken } func (ts *ExternalTestSuite) TestInviteTokenExternalBitbucketErrorWhenEmailDoesntMatch() { - ts.createUser("bitbucket@example.com", "", "", "invite_token") + ts.createUser("bitbucketTestId", "bitbucket@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - bitbucketUser := `{"display_name":"Bitbucket Test","avatar":{"href":"http://example.com/avatar"}}` emails := `{"values":[{"email":"other@example.com","is_primary":true,"is_confirmed":true}]}` server := BitbucketTestSignupSetup(ts, &tokenCount, &userCount, code, bitbucketUser, emails) defer server.Close() diff --git a/api/external_discord_test.go b/api/external_discord_test.go index e615621279..057cd9c59e 100644 --- a/api/external_discord_test.go +++ b/api/external_discord_test.go @@ -9,6 +9,12 @@ import ( jwt "github.com/golang-jwt/jwt" ) +const ( + discordUser string = `{"id":"discordTestId","avatar":"abc","email":"discord@example.com","username":"Discord Test","verified":true}}` + discordUserWrongEmail string = `{"id":"discordTestId","avatar":"abc","email":"other@example.com","username":"Discord Test","verified":true}}` + discordUserNoEmail string = `{"id":"discordTestId","avatar":"abc","username":"Discord Test","verified":true}}` +) + func (ts *ExternalTestSuite) TestSignupExternalDiscord() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=discord", nil) w := httptest.NewRecorder() @@ -63,13 +69,12 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscord_AuthorizationCode() { ts.Config.DisableSignup = false tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}` server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) defer server.Close() u := performAuthorization(ts, "discord", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png") } func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenNoUser() { @@ -77,7 +82,6 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenNoUs tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}` server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) defer server.Close() @@ -90,8 +94,7 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenEmpt tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","id":"123","username":"Discord Test","verified":true}}` - server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) + server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUserNoEmail) defer server.Close() u := performAuthorization(ts, "discord", code, "") @@ -102,38 +105,35 @@ func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupErrorWhenEmpt func (ts *ExternalTestSuite) TestSignupExternalDiscordDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png", "") + ts.createUser("discordTestId", "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/discordTestId/abc.png", "") tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}` server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) defer server.Close() u := performAuthorization(ts, "discord", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png") } func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordSuccessWhenMatchingToken() { // name and avatar should be populated from Discord API - ts.createUser("discord@example.com", "", "", "invite_token") + ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}` server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) defer server.Close() u := performAuthorization(ts, "discord", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "https://cdn.discordapp.com/avatars/123/abc.png") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "discord@example.com", "Discord Test", "discordTestId", "https://cdn.discordapp.com/avatars/discordTestId/abc.png") } func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenNoMatchingToken() { tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}` server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) defer server.Close() @@ -142,11 +142,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenNoMatchingTo } func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenWrongToken() { - ts.createUser("discord@example.com", "", "", "invite_token") + ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","email":"discord@example.com","id":"123","username":"Discord Test","verified":true}}` server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) defer server.Close() @@ -155,12 +154,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenWrongToken() } func (ts *ExternalTestSuite) TestInviteTokenExternalDiscordErrorWhenEmailDoesntMatch() { - ts.createUser("discord@example.com", "", "", "invite_token") + ts.createUser("discordTestId", "discord@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - discordUser := `{"avatar":"abc","email":"other@example.com","id":"123","username":"Discord Test","verified":true}}` - server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUser) + server := DiscordTestSignupSetup(ts, &tokenCount, &userCount, code, discordUserWrongEmail) defer server.Close() u := performAuthorization(ts, "discord", code, "invite_token") diff --git a/api/external_facebook_test.go b/api/external_facebook_test.go index adddfc2263..253715438f 100644 --- a/api/external_facebook_test.go +++ b/api/external_facebook_test.go @@ -9,6 +9,12 @@ import ( jwt "github.com/golang-jwt/jwt" ) +const ( + facebookUser string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` + facebookUserWrongEmail string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"other@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` + facebookUserNoEmail string = `{"id":"facebookTestId","name":"Facebook Test","first_name":"Facebook","last_name":"Test","picture":{"data":{"url":"http://example.com/avatar"}}}}` +) + func (ts *ExternalTestSuite) TestSignupExternalFacebook() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=facebook", nil) w := httptest.NewRecorder() @@ -63,13 +69,12 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebook_AuthorizationCode() { ts.Config.DisableSignup = false tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) defer server.Close() u := performAuthorization(ts, "facebook", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenNoUser() { @@ -77,7 +82,6 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenNoU tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) defer server.Close() @@ -90,8 +94,7 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenEmp tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","picture":{"data":{"url":"http://example.com/avatar"}}}}` - server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) + server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUserNoEmail) defer server.Close() u := performAuthorization(ts, "facebook", code, "") @@ -102,38 +105,35 @@ func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupErrorWhenEmp func (ts *ExternalTestSuite) TestSignupExternalFacebookDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("facebook@example.com", "Facebook Test", "http://example.com/avatar", "") + ts.createUser("facebookTestId", "facebook@example.com", "Facebook Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) defer server.Close() u := performAuthorization(ts, "facebook", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookSuccessWhenMatchingToken() { // name and avatar should be populated from Facebook API - ts.createUser("facebook@example.com", "", "", "invite_token") + ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) defer server.Close() u := performAuthorization(ts, "facebook", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "facebook@example.com", "Facebook Test", "facebookTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenNoMatchingToken() { tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) defer server.Close() @@ -142,11 +142,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenNoMatchingT } func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenWrongToken() { - ts.createUser("facebook@example.com", "", "", "invite_token") + ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"facebook@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) defer server.Close() @@ -155,12 +154,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenWrongToken( } func (ts *ExternalTestSuite) TestInviteTokenExternalFacebookErrorWhenEmailDoesntMatch() { - ts.createUser("facebook@example.com", "", "", "invite_token") + ts.createUser("facebookTestId", "facebook@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - facebookUser := `{"name":"Facebook Test","first_name":"Facebook","last_name":"Test","email":"other@example.com","picture":{"data":{"url":"http://example.com/avatar"}}}}` - server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUser) + server := FacebookTestSignupSetup(ts, &tokenCount, &userCount, code, facebookUserWrongEmail) defer server.Close() u := performAuthorization(ts, "facebook", code, "invite_token") diff --git a/api/external_github_test.go b/api/external_github_test.go index bd2831a712..20eea356be 100644 --- a/api/external_github_test.go +++ b/api/external_github_test.go @@ -47,7 +47,7 @@ func GitHubTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *in case "/api/v3/user": *userCount++ w.Header().Add("Content-Type", "application/json") - fmt.Fprint(w, `{"name":"GitHub Test","avatar_url":"http://example.com/avatar"}`) + fmt.Fprint(w, `{"id":"githubTestId", "name":"GitHub Test","avatar_url":"http://example.com/avatar"}`) case "/api/v3/user/emails": w.Header().Add("Content-Type", "application/json") fmt.Fprint(w, emails) @@ -71,7 +71,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHub_AuthorizationCode() { u := performAuthorization(ts, "github", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "githubTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupErrorWhenNoUser() { @@ -103,7 +103,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupErrorWhenEmpty func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("github@example.com", "GitHub Test", "http://example.com/avatar", "") + ts.createUser("githubTestId", "github@example.com", "GitHub Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" @@ -113,13 +113,13 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithPri u := performAuthorization(ts, "github", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "githubTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithNonPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("secondary@example.com", "GitHub Test", "http://example.com/avatar", "") + ts.createUser("githubTestId", "secondary@example.com", "GitHub Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" @@ -129,12 +129,12 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHubDisableSignupSuccessWithNon u := performAuthorization(ts, "github", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitHub Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitHub Test", "githubTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubSuccessWhenMatchingToken() { // name and avatar should be populated from GitHub API - ts.createUser("github@example.com", "", "", "invite_token") + ts.createUser("githubTestId", "github@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" @@ -144,7 +144,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubSuccessWhenMatchingTok u := performAuthorization(ts, "github", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "github@example.com", "GitHub Test", "githubTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenNoMatchingToken() { @@ -159,7 +159,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenNoMatchingTok } func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenWrongToken() { - ts.createUser("github@example.com", "", "", "invite_token") + ts.createUser("githubTestId", "github@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" @@ -172,7 +172,7 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenWrongToken() } func (ts *ExternalTestSuite) TestInviteTokenExternalGitHubErrorWhenEmailDoesntMatch() { - ts.createUser("github@example.com", "", "", "invite_token") + ts.createUser("githubTestId", "github@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" diff --git a/api/external_gitlab_test.go b/api/external_gitlab_test.go index ba6c4f868c..8a8b0fbf08 100644 --- a/api/external_gitlab_test.go +++ b/api/external_gitlab_test.go @@ -9,6 +9,12 @@ import ( jwt "github.com/golang-jwt/jwt" ) +const ( + gitlabUser string = `{"id":123,"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}` + gitlabUserWrongEmail string = `{"id":123,"email":"other@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}` + gitlabUserNoEmail string = `{"id":123,"name":"Gitlab Test","avatar_url":"http://example.com/avatar"}` +) + func (ts *ExternalTestSuite) TestSignupExternalGitlab() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=gitlab", nil) w := httptest.NewRecorder() @@ -68,14 +74,13 @@ func (ts *ExternalTestSuite) TestSignupExternalGitlab_AuthorizationCode() { tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"name":"GitLab Test","avatar_url":"http://example.com/avatar"}` emails := `[{"id":1,"email":"gitlab@example.com"}]` server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) defer server.Close() u := performAuthorization(ts, "gitlab", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenNoUser() { @@ -83,7 +88,6 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenNoUse tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"name":"Gitlab Test","avatar_url":"http://example.com/avatar"}` emails := `[{"id":1,"email":"gitlab@example.com"}]` server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) defer server.Close() @@ -98,9 +102,8 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenEmpty tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"name":"Gitlab Test","avatar_url":"http://example.com/avatar"}` emails := `[]` - server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) + server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUserNoEmail, emails) defer server.Close() u := performAuthorization(ts, "gitlab", code, "") @@ -111,18 +114,17 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupErrorWhenEmpty func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("gitlab@example.com", "GitLab Test", "http://example.com/avatar", "") + ts.createUser("123", "gitlab@example.com", "GitLab Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}` emails := "[]" server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) defer server.Close() u := performAuthorization(ts, "gitlab", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithSecondaryEmail() { @@ -130,40 +132,37 @@ func (ts *ExternalTestSuite) TestSignupExternalGitLabDisableSignupSuccessWithSec ts.Config.Mailer.Autoconfirm = true ts.Config.DisableSignup = true - ts.createUser("secondary@example.com", "GitLab Test", "http://example.com/avatar", "") + ts.createUser("123", "secondary@example.com", "GitLab Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"email":"primary@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar"}` emails := `[{"id":1,"email":"secondary@example.com"}]` server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) defer server.Close() u := performAuthorization(ts, "gitlab", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitLab Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "secondary@example.com", "GitLab Test", "123", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabSuccessWhenMatchingToken() { // name and avatar should be populated from GitLab API - ts.createUser("gitlab@example.com", "", "", "invite_token") + ts.createUser("123", "gitlab@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}` emails := "[]" server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) defer server.Close() u := performAuthorization(ts, "gitlab", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "gitlab@example.com", "GitLab Test", "123", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenNoMatchingToken() { tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}` emails := "[]" server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) defer server.Close() @@ -173,11 +172,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenNoMatchingTok } func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenWrongToken() { - ts.createUser("gitlab@example.com", "", "", "invite_token") + ts.createUser("123", "gitlab@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"email":"gitlab@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}` emails := "[]" server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) defer server.Close() @@ -187,13 +185,12 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenWrongToken() } func (ts *ExternalTestSuite) TestInviteTokenExternalGitLabErrorWhenEmailDoesntMatch() { - ts.createUser("gitlab@example.com", "", "", "invite_token") + ts.createUser("123", "gitlab@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - gitlabUser := `{"email":"other@example.com","name":"GitLab Test","avatar_url":"http://example.com/avatar","confirmed_at":"2012-05-23T09:05:22Z"}` emails := "[]" - server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUser, emails) + server := GitlabTestSignupSetup(ts, &tokenCount, &userCount, code, gitlabUserWrongEmail, emails) defer server.Close() u := performAuthorization(ts, "gitlab", code, "invite_token") diff --git a/api/external_google_test.go b/api/external_google_test.go index 2728808760..8992d0a651 100644 --- a/api/external_google_test.go +++ b/api/external_google_test.go @@ -9,6 +9,12 @@ import ( jwt "github.com/golang-jwt/jwt" ) +const ( + googleUser string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}` + googleUserWrongEmail string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","email":"other@example.com","verified_email":true}}` + googleUserNoEmail string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","verified_email":true}}` +) + func (ts *ExternalTestSuite) TestSignupExternalGoogle() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=google", nil) w := httptest.NewRecorder() @@ -63,13 +69,12 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogle_AuthorizationCode() { ts.Config.DisableSignup = false tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}` server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) defer server.Close() u := performAuthorization(ts, "google", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenNoUser() { @@ -77,7 +82,6 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenNoUse tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}` server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) defer server.Close() @@ -90,8 +94,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenEmpty tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","verified_email":true}}` - server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) + server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUserNoEmail) defer server.Close() u := performAuthorization(ts, "google", code, "") @@ -102,38 +105,35 @@ func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupErrorWhenEmpty func (ts *ExternalTestSuite) TestSignupExternalGoogleDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("google@example.com", "Google Test", "http://example.com/avatar", "") + ts.createUser("googleTestId", "google@example.com", "Google Test", "http://example.com/avatar", "") tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}` server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) defer server.Close() u := performAuthorization(ts, "google", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleSuccessWhenMatchingToken() { // name and avatar should be populated from Google API - ts.createUser("google@example.com", "", "", "invite_token") + ts.createUser("googleTestId", "google@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}` server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) defer server.Close() u := performAuthorization(ts, "google", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "http://example.com/avatar") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "google@example.com", "Google Test", "googleTestId", "http://example.com/avatar") } func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenNoMatchingToken() { tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}` server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) defer server.Close() @@ -142,11 +142,10 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenNoMatchingTok } func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenWrongToken() { - ts.createUser("google@example.com", "", "", "invite_token") + ts.createUser("googleTestId", "google@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"google@example.com","verified_email":true}}` server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) defer server.Close() @@ -155,12 +154,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenWrongToken() } func (ts *ExternalTestSuite) TestInviteTokenExternalGoogleErrorWhenEmailDoesntMatch() { - ts.createUser("google@example.com", "", "", "invite_token") + ts.createUser("googleTestId", "google@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - googleUser := `{"name":"Google Test","picture":"http://example.com/avatar","email":"other@example.com","verified_email":true}}` - server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUser) + server := GoogleTestSignupSetup(ts, &tokenCount, &userCount, code, googleUserWrongEmail) defer server.Close() u := performAuthorization(ts, "google", code, "invite_token") diff --git a/api/external_oauth.go b/api/external_oauth.go index c15ea743eb..3a4899efce 100644 --- a/api/external_oauth.go +++ b/api/external_oauth.go @@ -87,7 +87,10 @@ func (a *API) oAuthCallback(ctx context.Context, r *http.Request, providerType s // apple only returns user info the first time oauthUser := rq.Get("user") if oauthUser != "" { - userData.Metadata = externalProvider.ParseUser(oauthUser) + err := externalProvider.ParseUser(oauthUser, userData) + if err != nil { + return nil, err + } } } diff --git a/api/external_saml.go b/api/external_saml.go index cb447a1d2b..fa74d1cd60 100644 --- a/api/external_saml.go +++ b/api/external_saml.go @@ -52,6 +52,9 @@ func (a *API) samlCallback(ctx context.Context, r *http.Request) (*provider.User Email: assertionInfo.NameID, Verified: true, }}, + Metadata: &provider.Claims{ + Subject: assertionInfo.NameID, + }, } return userData, nil } diff --git a/api/external_test.go b/api/external_test.go index 0cb7e326b4..9aee2c30c4 100644 --- a/api/external_test.go +++ b/api/external_test.go @@ -41,13 +41,13 @@ func (ts *ExternalTestSuite) SetupTest() { models.TruncateAll(ts.API.db) } -func (ts *ExternalTestSuite) createUser(email string, name string, avatar string, confirmationToken string) (*models.User, error) { +func (ts *ExternalTestSuite) createUser(providerId string, email string, name string, avatar string, confirmationToken string) (*models.User, error) { // Cleanup existing user, if they already exist if u, _ := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, email, ts.Config.JWT.Aud); u != nil { require.NoError(ts.T(), ts.API.db.Destroy(u), "Error deleting user") } - u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"full_name": name, "avatar_url": avatar}) + u, err := models.NewUser(ts.instanceID, email, "test", ts.Config.JWT.Aud, map[string]interface{}{"provider_id": providerId, "full_name": name, "avatar_url": avatar}) if confirmationToken != "" { u.ConfirmationToken = confirmationToken @@ -98,7 +98,7 @@ func performAuthorization(ts *ExternalTestSuite, provider string, code string, i return u } -func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount int, userCount int, email string, name string, avatar string) { +func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount int, userCount int, email string, name string, providerId string, avatar string) { // ensure redirect has #access_token=... v, err := url.ParseQuery(u.RawQuery) ts.Require().NoError(err) @@ -118,6 +118,7 @@ func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount in // ensure user has been created with metadata user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, email, ts.Config.JWT.Aud) ts.Require().NoError(err) + ts.Equal(providerId, user.UserMetaData["provider_id"]) ts.Equal(name, user.UserMetaData["full_name"]) ts.Equal(avatar, user.UserMetaData["avatar_url"]) } diff --git a/api/external_twitch_test.go b/api/external_twitch_test.go index f8f8b340ee..a4e473cae9 100644 --- a/api/external_twitch_test.go +++ b/api/external_twitch_test.go @@ -9,6 +9,11 @@ import ( jwt "github.com/golang-jwt/jwt" ) +const ( + twitchUser string = `{"data":[{"id":"twitchTestId","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}` + twitchUserWrongEmail string = `{"data":[{"id":"twitchTestId","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"other@example.com"}]}` +) + func (ts *ExternalTestSuite) TestSignupExternalTwitch() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=twitch", nil) w := httptest.NewRecorder() @@ -63,13 +68,12 @@ func (ts *ExternalTestSuite) TestSignupExternalTwitch_AuthorizationCode() { ts.Config.DisableSignup = false tokenCount, userCount := 0, 0 code := "authcode" - TwitchUser := `{"data":[{"id":"1","login":"Twitch Test","display_name":"Twitch Test","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}` - server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser) + server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser) defer server.Close() u := performAuthorization(ts, "twitch", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch user", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8") } func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupErrorWhenNoUser() { @@ -103,32 +107,31 @@ func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupErrorWhenEmpty func (ts *ExternalTestSuite) TestSignupExternalTwitchDisableSignupSuccessWithPrimaryEmail() { ts.Config.DisableSignup = true - ts.createUser("twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8", "") + ts.createUser("twitchTestId", "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8", "") tokenCount, userCount := 0, 0 code := "authcode" - TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}` - server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser) + server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser) defer server.Close() u := performAuthorization(ts, "twitch", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8") } func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchSuccessWhenMatchingToken() { // name and avatar should be populated from Twitch API - ts.createUser("twitch@example.com", "", "", "invite_token") + ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - TwitchUser := `{"data":[{"id":"1","login":"Twitch Test","display_name":"Twitch Test","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}` + TwitchUser := `{"data":[{"id":"twitchTestId","login":"Twitch Test","display_name":"Twitch Test","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}` server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser) defer server.Close() u := performAuthorization(ts, "twitch", code, "invite_token") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "twitch@example.com", "Twitch Test", "twitchTestId", "https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8") } func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenNoMatchingToken() { @@ -143,12 +146,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenNoMatchingTok } func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenWrongToken() { - ts.createUser("twitch@example.com", "", "", "invite_token") + ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"twitch@example.com"}]}` - server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser) + server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUser) defer server.Close() w := performAuthorizationRequest(ts, "twitch", "wrong_token") @@ -156,12 +158,11 @@ func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenWrongToken() } func (ts *ExternalTestSuite) TestInviteTokenExternalTwitchErrorWhenEmailDoesntMatch() { - ts.createUser("twitch@example.com", "", "", "invite_token") + ts.createUser("twitchTestId", "twitch@example.com", "", "", "invite_token") tokenCount, userCount := 0, 0 code := "authcode" - TwitchUser := `{"data":[{"id":"1","login":"Twitch user","display_name":"Twitch user","type":"","broadcaster_type":"","description":"","profile_image_url":"https://s.gravatar.com/avatar/23463b99b62a72f26ed677cc556c44e8","offline_image_url":"","email":"other@example.com"}]}` - server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, TwitchUser) + server := TwitchTestSignupSetup(ts, &tokenCount, &userCount, code, twitchUserWrongEmail) defer server.Close() u := performAuthorization(ts, "twitch", code, "invite_token") diff --git a/api/invite_test.go b/api/invite_test.go index 14d58f2168..712b487aad 100644 --- a/api/invite_test.go +++ b/api/invite_test.go @@ -60,7 +60,9 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + identities, err := models.FindIdentitiesByUser(ts.API.db, u) + require.NoError(ts.T(), err, "Error retrieving identities") + token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} @@ -264,7 +266,7 @@ func (ts *InviteTestSuite) TestInviteExternalGitlab() { ts.Require().NoError(err) ts.Equal("Gitlab Test", user.UserMetaData["full_name"]) ts.Equal("http://example.com/avatar", user.UserMetaData["avatar_url"]) - ts.Equal("gitlab", user.AppMetaData["provider"]) + ts.Equal([]interface{}{"gitlab"}, user.AppMetaData["provider"]) } func (ts *InviteTestSuite) TestInviteExternalGitlab_MismatchedEmails() { diff --git a/api/provider/apple.go b/api/provider/apple.go index aaec711be7..148841af9f 100644 --- a/api/provider/apple.go +++ b/api/provider/apple.go @@ -8,6 +8,8 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" + "strings" "github.com/golang-jwt/jwt" "github.com/lestrrat-go/jwx/jwk" @@ -50,6 +52,7 @@ type idTokenClaims struct { AuthTime int `json:"auth_time"` Email string `json:"email"` IsPrivateEmail bool `json:"is_private_email,string"` + Sub string `json:"sub"` } // NewAppleProvider creates a Apple account provider. @@ -87,6 +90,19 @@ func (p AppleProvider) GetOAuthToken(code string) (*oauth2.Token, error) { return p.Exchange(oauth2.NoContext, code, opts...) } +func (p AppleProvider) AuthCodeURL(state string, args ...oauth2.AuthCodeOption) string { + opts := make([]oauth2.AuthCodeOption, 0, 1) + opts = append(opts, oauth2.SetAuthURLParam("response_mode", "form_post")) + authURL := p.Config.AuthCodeURL(state, opts...) + if authURL != "" { + if u, err := url.Parse(authURL); err != nil { + u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%20") + authURL = u.String() + } + } + return authURL +} + // GetUserData returns the user data fetched from the apple provider func (p AppleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { var user *UserProvidedData @@ -148,22 +164,29 @@ func (p AppleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use Verified: true, Primary: true, }}, + Metadata: &Claims{ + Issuer: p.UserInfoURL, + Subject: idToken.Claims.(*idTokenClaims).Sub, + Email: idToken.Claims.(*idTokenClaims).Email, + EmailVerified: true, + + // To be deprecated + ProviderId: idToken.Claims.(*idTokenClaims).Sub, + }, } - } return user, nil } // ParseUser parses the apple user's info -func (p AppleProvider) ParseUser(data string) map[string]string { - userData := &appleUser{} - err := json.Unmarshal([]byte(data), userData) +func (p AppleProvider) ParseUser(data string, userData *UserProvidedData) error { + u := &appleUser{} + err := json.Unmarshal([]byte(data), u) if err != nil { - return nil - } - return map[string]string{ - "firstName": userData.Name.FirstName, - "lastName": userData.Name.LastName, - "email": userData.Email, + return err } + + userData.Metadata.Name = strings.TrimSpace(u.Name.FirstName + " " + u.Name.LastName) + userData.Metadata.FullName = strings.TrimSpace(u.Name.FirstName + " " + u.Name.LastName) + return nil } diff --git a/api/provider/azure.go b/api/provider/azure.go index 903adbef0f..fe78cf9267 100644 --- a/api/provider/azure.go +++ b/api/provider/azure.go @@ -22,6 +22,7 @@ type azureProvider struct { type azureUser struct { Name string `json:"name"` Email string `json:"email"` + Sub string `json:"sub"` } type azureEmail struct { @@ -75,8 +76,16 @@ func (g azureProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use } return &UserProvidedData{ - Metadata: map[string]string{ - nameKey: u.Name, + Metadata: &Claims{ + Issuer: g.APIPath, + Subject: u.Sub, + Name: u.Name, + Email: u.Email, + EmailVerified: true, + + // To be deprecated + FullName: u.Name, + ProviderId: u.Sub, }, Emails: []Email{{ Email: u.Email, diff --git a/api/provider/bitbucket.go b/api/provider/bitbucket.go index 475c912ed3..6fb0f7330c 100644 --- a/api/provider/bitbucket.go +++ b/api/provider/bitbucket.go @@ -20,6 +20,7 @@ type bitbucketProvider struct { type bitbucketUser struct { Name string `json:"display_name"` + ID string `json:"uuid"` Avatar struct { Href string `json:"href"` } `json:"avatar"` @@ -69,12 +70,7 @@ func (g bitbucketProvider) GetUserData(ctx context.Context, tok *oauth2.Token) ( return nil, err } - data := &UserProvidedData{ - Metadata: map[string]string{ - nameKey: u.Name, - avatarURLKey: u.Avatar.Href, - }, - } + data := &UserProvidedData{} var emails bitbucketEmails if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/user/emails", &emails); err != nil { @@ -97,5 +93,17 @@ func (g bitbucketProvider) GetUserData(ctx context.Context, tok *oauth2.Token) ( return nil, errors.New("Unable to find email with Bitbucket provider") } + data.Metadata = &Claims{ + Issuer: g.APIPath, + Subject: u.ID, + Name: u.Name, + Picture: u.Avatar.Href, + + // To be deprecated + AvatarURL: u.Avatar.Href, + FullName: u.Name, + ProviderId: u.ID, + } + return data, nil } diff --git a/api/provider/discord.go b/api/provider/discord.go index 760564def4..b323abfc9f 100644 --- a/api/provider/discord.go +++ b/api/provider/discord.go @@ -92,10 +92,18 @@ func (g discordProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U } return &UserProvidedData{ - Metadata: map[string]string{ - avatarURLKey: avatarURL, - nameKey: u.Name, - providerIDKey: u.ID, + Metadata: &Claims{ + Issuer: g.APIPath, + Subject: u.ID, + Name: u.Name, + Picture: avatarURL, + Email: u.Email, + EmailVerified: u.Verified, + + // To be deprecated + AvatarURL: avatarURL, + FullName: u.Name, + ProviderId: u.ID, }, Emails: []Email{{ Email: u.Email, diff --git a/api/provider/facebook.go b/api/provider/facebook.go index 9c9407b41a..156552b27e 100644 --- a/api/provider/facebook.go +++ b/api/provider/facebook.go @@ -24,6 +24,7 @@ type facebookProvider struct { } type facebookUser struct { + ID string `json:"id"` Email string `json:"email"` FirstName string `json:"first_name"` LastName string `json:"last_name"` @@ -88,10 +89,20 @@ func (p facebookProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (* } return &UserProvidedData{ - Metadata: map[string]string{ - aliasKey: u.Alias, - nameKey: strings.TrimSpace(u.FirstName + " " + u.LastName), - avatarURLKey: u.Avatar.Data.URL, + Metadata: &Claims{ + Issuer: p.ProfileURL, + Subject: u.ID, + Name: strings.TrimSpace(u.FirstName + " " + u.LastName), + NickName: u.Alias, + Email: u.Email, + EmailVerified: true, // if email is returned, the email is verified by facebook already + Picture: u.Avatar.Data.URL, + + // To be deprecated + Slug: u.Alias, + AvatarURL: u.Avatar.Data.URL, + FullName: strings.TrimSpace(u.FirstName + " " + u.LastName), + ProviderId: u.ID, }, Emails: []Email{{ Email: u.Email, diff --git a/api/provider/github.go b/api/provider/github.go index 76e8bb45d8..67b95bb263 100644 --- a/api/provider/github.go +++ b/api/provider/github.go @@ -22,6 +22,7 @@ type githubProvider struct { } type githubUser struct { + ID string `json:"id"` UserName string `json:"login"` Email string `json:"email"` Name string `json:"name"` @@ -80,10 +81,17 @@ func (g githubProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us } data := &UserProvidedData{ - Metadata: map[string]string{ - userNameKey: u.UserName, - nameKey: u.Name, - avatarURLKey: u.AvatarURL, + Metadata: &Claims{ + Issuer: g.APIHost, + Subject: u.ID, + Name: u.Name, + PreferredUsername: u.UserName, + + // To be deprecated + AvatarURL: u.AvatarURL, + FullName: u.Name, + ProviderId: u.ID, + UserNameKey: u.UserName, }, } @@ -96,6 +104,11 @@ func (g githubProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us if e.Email != "" { data.Emails = append(data.Emails, Email{Email: e.Email, Verified: e.Verified, Primary: e.Primary}) } + + if e.Primary { + data.Metadata.Email = e.Email + data.Metadata.EmailVerified = e.Verified + } } if len(data.Emails) <= 0 { diff --git a/api/provider/gitlab.go b/api/provider/gitlab.go index f63eefd75a..1d5c782405 100644 --- a/api/provider/gitlab.go +++ b/api/provider/gitlab.go @@ -3,6 +3,7 @@ package provider import ( "context" "errors" + "strconv" "strings" "github.com/netlify/gotrue/conf" @@ -23,6 +24,7 @@ type gitlabUser struct { Name string `json:"name"` AvatarURL string `json:"avatar_url"` ConfirmedAt string `json:"confirmed_at"` + ID int `json:"id"` } type gitlabUserEmail struct { @@ -71,12 +73,7 @@ func (g gitlabProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us return nil, err } - data := &UserProvidedData{ - Metadata: map[string]string{ - nameKey: u.Name, - avatarURLKey: u.AvatarURL, - }, - } + data := &UserProvidedData{} var emails []*gitlabUserEmail if err := makeRequest(ctx, tok, g.Config, g.Host+"/api/v4/user/emails", &emails); err != nil { @@ -99,5 +96,19 @@ func (g gitlabProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us return nil, errors.New("Unable to find email with GitLab provider") } + data.Metadata = &Claims{ + Issuer: g.Host, + Subject: strconv.Itoa(u.ID), + Name: u.Name, + Picture: u.AvatarURL, + Email: u.Email, + EmailVerified: true, + + // To be deprecated + AvatarURL: u.AvatarURL, + FullName: u.Name, + ProviderId: strconv.Itoa(u.ID), + } + return data, nil } diff --git a/api/provider/google.go b/api/provider/google.go index bf9de6d72b..e2e060ecbf 100644 --- a/api/provider/google.go +++ b/api/provider/google.go @@ -20,6 +20,7 @@ type googleProvider struct { } type googleUser struct { + ID string `json:"id"` Name string `json:"name"` AvatarURL string `json:"picture"` Email string `json:"email"` @@ -69,12 +70,7 @@ func (g googleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us return nil, err } - data := &UserProvidedData{ - Metadata: map[string]string{ - nameKey: u.Name, - avatarURLKey: u.AvatarURL, - }, - } + data := &UserProvidedData{} if u.Email != "" { data.Emails = append(data.Emails, Email{ @@ -88,5 +84,19 @@ func (g googleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us return nil, errors.New("Unable to find email with Google provider") } + data.Metadata = &Claims{ + Issuer: g.APIPath, + Subject: u.ID, + Name: u.Name, + Picture: u.AvatarURL, + Email: u.Email, + EmailVerified: u.EmailVerified, + + // To be deprecated + AvatarURL: u.AvatarURL, + FullName: u.Name, + ProviderId: u.ID, + } + return data, nil } diff --git a/api/provider/provider.go b/api/provider/provider.go index 410389e6bc..1196457235 100644 --- a/api/provider/provider.go +++ b/api/provider/provider.go @@ -7,13 +7,58 @@ import ( "golang.org/x/oauth2" ) -const ( - userNameKey = "user_name" - avatarURLKey = "avatar_url" - nameKey = "full_name" - aliasKey = "slug" - providerIDKey = "provider_id" -) +type Claims struct { + // Reserved claims + Issuer string `json:"iss,omitempty"` + Subject string `json:"sub,omitempty"` + Aud string `json:"aud,omitempty"` + Iat string `json:"iat,omitempty"` + Exp string `json:"exp,omitempty"` + + // Default profile claims + Name string `json:"name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + GivenName string `json:"given_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + NickName string `json:"nickname,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Profile string `json:"profile,omitempty"` + Picture string `json:"picture,omitempty"` + Website string `json:"website,omitempty"` + Gender string `json:"gender,omitempty"` + Birthdate string `json:"birthdate,omitempty"` + ZoneInfo string `json:"zoneinfo,omitempty"` + Locale string `json:"locale,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + Email string `json:"email,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` + Phone string `json:"phone,omitempty"` + PhoneVerified bool `json:"phone_verified,omitempty"` + + // Custom profile claims that are provider specific + CustomClaims map[string]interface{} `json:"custom_claims,omitempty"` + + // TODO: Deprecate in next major release + FullName string `json:"full_name,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + Slug string `json:"slug,omitempty"` + ProviderId string `json:"provider_id,omitempty"` + UserNameKey string `json:"user_name,omitempty"` +} + +// ToMap converts the Claims struct to a map[string]interface{} +func (c *Claims) ToMap() (map[string]interface{}, error) { + m := make(map[string]interface{}) + cBytes, err := json.Marshal(c) + if err != nil { + return nil, err + } + err = json.Unmarshal(cBytes, &m) + if err != nil { + return nil, err + } + return m, nil +} // Email is a struct that provides information on whether an email is verified or is the primary email address type Email struct { @@ -25,7 +70,7 @@ type Email struct { // UserProvidedData is a struct that contains the user's data returned from the oauth provider type UserProvidedData struct { Emails []Email - Metadata map[string]string + Metadata *Claims } // Provider is an interface for interacting with external account providers diff --git a/api/provider/twitch.go b/api/provider/twitch.go index edcea5100f..01bb806d6f 100644 --- a/api/provider/twitch.go +++ b/api/provider/twitch.go @@ -113,10 +113,27 @@ func (t twitchProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us } data := &UserProvidedData{ - Metadata: map[string]string{ - nameKey: user.Login, - aliasKey: user.DisplayName, - avatarURLKey: user.ProfileImageURL, + Metadata: &Claims{ + Issuer: t.APIHost, + Subject: user.ID, + Picture: user.ProfileImageURL, + Name: user.Login, + NickName: user.DisplayName, + Email: user.Email, + EmailVerified: true, + CustomClaims: map[string]interface{}{ + "broadcaster_type": user.BroadcasterType, + "description": user.Description, + "type": user.Type, + "offline_image_url": user.OfflineImageURL, + "view_count": user.ViewCount, + }, + + // To be deprecated + Slug: user.DisplayName, + AvatarURL: user.ProfileImageURL, + FullName: user.Login, + ProviderId: user.ID, }, Emails: []Email{{ Email: user.Email, diff --git a/api/provider/twitter.go b/api/provider/twitter.go index a046e976c0..a3a752c32d 100644 --- a/api/provider/twitter.go +++ b/api/provider/twitter.go @@ -40,6 +40,7 @@ type twitterUser struct { Name string `json:"name"` AvatarURL string `json:"profile_image_url_https"` Email string `json:"email"` + ID string `json:"id_str"` } // NewTwitterProvider creates a Twitter account provider. @@ -94,10 +95,20 @@ func (t TwitterProvider) FetchUserData(ctx context.Context, tok *oauth.AccessTok } data := &UserProvidedData{ - Metadata: map[string]string{ - userNameKey: u.UserName, - nameKey: u.Name, - avatarURLKey: u.AvatarURL, + Metadata: &Claims{ + Issuer: t.UserInfoURL, + Subject: u.ID, + Name: u.Name, + Picture: u.AvatarURL, + PreferredUsername: u.UserName, + Email: u.Email, + EmailVerified: true, + + // To be deprecated + UserNameKey: u.UserName, + FullName: u.Name, + AvatarURL: u.AvatarURL, + ProviderId: u.ID, }, Emails: []Email{{ Email: u.Email, @@ -105,6 +116,7 @@ func (t TwitterProvider) FetchUserData(ctx context.Context, tok *oauth.AccessTok Primary: true, }}, } + return data, nil } diff --git a/api/signup.go b/api/signup.go index 8e280bb118..77a88b6297 100644 --- a/api/signup.go +++ b/api/signup.go @@ -221,7 +221,7 @@ func (a *API) signupNewUser(ctx context.Context, conn *storage.Connection, param if user.AppMetaData == nil { user.AppMetaData = make(map[string]interface{}) } - user.AppMetaData["provider"] = params.Provider + user.AppMetaData["provider"] = []string{params.Provider} if params.Password == "" { user.EncryptedPassword = "" diff --git a/api/signup_test.go b/api/signup_test.go index 60e33dd32d..c0282c3d0e 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -73,7 +73,7 @@ func (ts *SignupTestSuite) TestSignup() { assert.Equal(ts.T(), "test@example.com", data.GetEmail()) assert.Equal(ts.T(), ts.Config.JWT.Aud, data.Aud) assert.Equal(ts.T(), 1.0, data.UserMetaData["a"]) - assert.Equal(ts.T(), "email", data.AppMetaData["provider"]) + assert.Equal(ts.T(), []interface{}{"email"}, data.AppMetaData["provider"]) } func (ts *SignupTestSuite) TestWebhookTriggered() { @@ -120,7 +120,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { appmeta, ok := u["app_metadata"].(map[string]interface{}) require.True(ok) assert.Len(appmeta, 1) - assert.EqualValues("email", appmeta["provider"]) + assert.EqualValues([]interface{}{"email"}, appmeta["provider"]) usermeta, ok := u["user_metadata"].(map[string]interface{}) require.True(ok) diff --git a/api/token.go b/api/token.go index 404041df7a..3f0d94df79 100644 --- a/api/token.go +++ b/api/token.go @@ -20,6 +20,7 @@ type GoTrueClaims struct { Phone string `json:"phone"` AppMetaData map[string]interface{} `json:"app_metadata"` UserMetaData map[string]interface{} `json:"user_metadata"` + Identities []*models.Identity `json:"identities"` Role string `json:"role"` } @@ -183,7 +184,13 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h return internalServerError(terr.Error()) } - tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + identities := make([]*models.Identity, 0) + identities, terr = models.FindIdentitiesByUser(tx, user) + if terr != nil { + return internalServerError("error retrieving identities").WithInternalError(terr) + } + + tokenString, terr = generateAccessToken(user, identities, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } @@ -208,7 +215,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h }) } -func generateAccessToken(user *models.User, expiresIn time.Duration, secret string) (string, error) { +func generateAccessToken(user *models.User, identities []*models.Identity, expiresIn time.Duration, secret string) (string, error) { claims := &GoTrueClaims{ StandardClaims: jwt.StandardClaims{ Subject: user.ID.String(), @@ -219,6 +226,7 @@ func generateAccessToken(user *models.User, expiresIn time.Duration, secret stri Phone: user.GetPhone(), AppMetaData: user.AppMetaData, UserMetaData: user.UserMetaData, + Identities: identities, Role: user.Role, } @@ -241,8 +249,12 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u if terr != nil { return internalServerError("Database error granting user").WithInternalError(terr) } + identities, terr := models.FindIdentitiesByUser(tx, user) + if terr != nil { + return internalServerError("Database error granting user").WithInternalError(terr) + } - tokenString, terr = generateAccessToken(user, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + tokenString, terr = generateAccessToken(user, identities, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } diff --git a/api/user_test.go b/api/user_test.go index 3510c3e422..bb8d22b408 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" + "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" - "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -62,7 +62,10 @@ func (ts *UserTestSuite) TestUser_UpdatePassword() { req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) req.Header.Set("Content-Type", "application/json") - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + identities, err := models.FindIdentitiesByUser(ts.API.db, u) + require.NoError(ts.T(), err) + + token, err := generateAccessToken(u, identities, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) diff --git a/example.env b/example.env index c013289973..73b973a9e8 100644 --- a/example.env +++ b/example.env @@ -57,6 +57,12 @@ GOTRUE_EXTERNAL_GITLAB_ENABLED="false" GOTRUE_EXTERNAL_GITLAB_CLIENT_ID="" GOTRUE_EXTERNAL_GITLAB_SECRET="" GOTRUE_EXTERNAL_GITLAB_REDIRECT_URI="" +GOTRUE_EXTERNAL_SAML_ENABLED="false" +GOTRUE_EXTERNAL_SAML_METADATA_URL="/" +GOTRUE_EXTERNAL_SAML_API_BASE="/" +GOTRUE_EXTERNAL_SAML_NAME="auth0" +GOTRUE_EXTERNAL_SAML_SIGNING_CERT="/" +GOTRUE_EXTERNAL_SAML_SIGNING_KEY="/" GOTRUE_MAILER_TEMPLATES_INVITE="https://app.supabase.io/api/auth/example-project-ref/templates/invite" GOTRUE_MAILER_TEMPLATES_CONFIRMATION="https://app.supabase.io/api/auth/example-project-ref/templates/confirmation" GOTRUE_MAILER_TEMPLATES_RECOVERY="https://app.supabase.io/api/auth/example-project-ref/templates/recovery" diff --git a/migrations/20210909172000_create_identities_table.up.sql b/migrations/20210909172000_create_identities_table.up.sql new file mode 100644 index 0000000000..10b1f2b90f --- /dev/null +++ b/migrations/20210909172000_create_identities_table.up.sql @@ -0,0 +1,14 @@ +-- adds identities table + +CREATE TABLE IF NOT EXISTS auth.identities ( + id text NOT NULL, + user_id uuid NOT NULL, + identity_data JSONB NOT NULL, + provider text NOT NULL, + last_sign_in_at timestamptz NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT identities_pkey PRIMARY KEY (provider, id), + CONSTRAINT identities_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE +); +COMMENT ON TABLE auth.identities is 'Auth: Stores identities associated to a user.'; diff --git a/models/connection.go b/models/connection.go index 89a8fd5ac3..7c01a1de8c 100644 --- a/models/connection.go +++ b/models/connection.go @@ -32,15 +32,15 @@ type SortField struct { func TruncateAll(conn *storage.Connection) error { return conn.Transaction(func(tx *storage.Connection) error { - if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: User{}}).TableName()).Exec(); err != nil { + if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: User{}}).TableName() + " CASCADE").Exec(); err != nil { return err } - if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: RefreshToken{}}).TableName()).Exec(); err != nil { + if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: RefreshToken{}}).TableName() + " CASCADE").Exec(); err != nil { return err } - if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: AuditLogEntry{}}).TableName()).Exec(); err != nil { + if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: AuditLogEntry{}}).TableName() + " CASCADE").Exec(); err != nil { return err } - return tx.RawQuery("TRUNCATE " + (&pop.Model{Value: Instance{}}).TableName()).Exec() + return tx.RawQuery("TRUNCATE " + (&pop.Model{Value: Instance{}}).TableName() + " CASCADE").Exec() }) } diff --git a/models/errors.go b/models/errors.go index f568364fc8..6c33c70c24 100644 --- a/models/errors.go +++ b/models/errors.go @@ -13,6 +13,8 @@ func IsNotFoundError(err error) bool { return true case TotpSecretNotFoundError: return true + case IdentityNotFoundError: + return true } return false } @@ -24,6 +26,13 @@ func (e UserNotFoundError) Error() string { return "User not found" } +// IdentityNotFoundError represents when an identity is not found. +type IdentityNotFoundError struct{} + +func (e IdentityNotFoundError) Error() string { + return "Identity not found" +} + // ConfirmationTokenNotFoundError represents when a confirmation token is not found. type ConfirmationTokenNotFoundError struct{} diff --git a/models/identity.go b/models/identity.go new file mode 100644 index 0000000000..74b9383b2e --- /dev/null +++ b/models/identity.go @@ -0,0 +1,84 @@ +package models + +import ( + "database/sql" + "time" + + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" +) + +type Identity struct { + ID string `json:"id" db:"id"` + UserID uuid.UUID `json:"user_id" db:"user_id"` + IdentityData JSONMap `json:"identity_data,omitempty" db:"identity_data"` + Provider string `json:"provider" db:"provider"` + LastSignInAt *time.Time `json:"last_sign_in_at,omitempty" db:"last_sign_in_at"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +func (Identity) TableName() string { + tableName := "identities" + return tableName +} + +// NewIdentity returns an identity associated to the user's id. +func NewIdentity(user *User, provider string, identityData map[string]interface{}) (*Identity, error) { + id, ok := identityData["sub"] + if !ok { + return nil, errors.New("Error missing provider id") + } + now := time.Now() + + identity := &Identity{ + ID: id.(string), + UserID: user.ID, + IdentityData: identityData, + Provider: provider, + LastSignInAt: &now, + } + + return identity, nil +} + +// FindIdentityById searches for an identity with the matching provider_id and provider given. +func FindIdentityByIdAndProvider(tx *storage.Connection, providerId, provider string) (*Identity, error) { + identity := &Identity{} + if err := tx.Q().Where("id = ? AND provider = ?", providerId, provider).First(identity); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, IdentityNotFoundError{} + } + return nil, errors.Wrap(err, "error finding identity") + } + return identity, nil +} + +// FindIdentitiesByUser returns all identities associated to a user +func FindIdentitiesByUser(tx *storage.Connection, user *User) ([]*Identity, error) { + identities := []*Identity{} + if err := tx.Q().Where("user_id = ?", user.ID).All(&identities); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return identities, nil + } + return nil, errors.Wrap(err, "error finding identities") + } + return identities, nil +} + +// FindProvidersByUser returns all providers associated to a user +func FindProvidersByUser(tx *storage.Connection, user *User) ([]string, error) { + identities := []Identity{} + providers := make([]string, 0) + if err := tx.Q().Select("provider").Where("user_id = ?", user.ID).All(&identities); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return providers, nil + } + return nil, errors.Wrap(err, "error finding providers") + } + for _, identity := range identities { + providers = append(providers, identity.Provider) + } + return providers, nil +} diff --git a/models/identity_test.go b/models/identity_test.go new file mode 100644 index 0000000000..c94b6a567f --- /dev/null +++ b/models/identity_test.go @@ -0,0 +1,94 @@ +package models + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/storage" + "github.com/netlify/gotrue/storage/test" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type IdentityTestSuite struct { + suite.Suite + db *storage.Connection +} + +func (ts *IdentityTestSuite) SetupTest() { + TruncateAll(ts.db) +} + +func TestIdentity(t *testing.T) { + globalConfig, err := conf.LoadGlobal(modelsTestConfig) + require.NoError(t, err) + + conn, err := test.SetupDBConnection(globalConfig) + require.NoError(t, err) + + ts := &IdentityTestSuite{ + db: conn, + } + defer ts.db.Close() + + suite.Run(t, ts) +} + +func (ts *IdentityTestSuite) TestNewIdentity() { + u := ts.createUserWithEmail("test@supabase.io") + ts.Run("Test create identity with no provider id", func() { + identityData := map[string]interface{}{} + _, err := NewIdentity(u, "email", identityData) + require.Error(ts.T(), err, "Error missing provider id") + }) + + ts.Run("Test create identity successfully", func() { + identityData := map[string]interface{}{"sub": uuid.Nil.String()} + identity, err := NewIdentity(u, "email", identityData) + require.NoError(ts.T(), err) + require.Equal(ts.T(), u.ID, identity.UserID) + }) +} + +func (ts *IdentityTestSuite) TestFindUserIdentities() { + u := ts.createUserWithIdentity("test@supabase.io") + identities, err := FindIdentitiesByUser(ts.db, u) + require.NoError(ts.T(), err) + + require.Len(ts.T(), identities, 1) + +} + +func (ts *IdentityTestSuite) createUserWithEmail(email string) *User { + user, err := NewUser(uuid.Nil, email, "secret", "test", nil) + require.NoError(ts.T(), err) + + err = ts.db.Create(user) + require.NoError(ts.T(), err) + + return user +} + +func (ts *IdentityTestSuite) createUserWithIdentity(email string) *User { + user, err := NewUser(uuid.Nil, email, "secret", "test", nil) + require.NoError(ts.T(), err) + + err = ts.db.Create(user) + require.NoError(ts.T(), err) + + identityData := map[string]interface{}{ + "sub": uuid.Nil.String(), + "name": "test", + "email": email, + } + require.NoError(ts.T(), err) + + identity, err := NewIdentity(user, "email", identityData) + require.NoError(ts.T(), err) + + err = ts.db.Create(identity) + require.NoError(ts.T(), err) + + return user +} diff --git a/models/user.go b/models/user.go index f0e63461f1..48adc2ffc4 100644 --- a/models/user.go +++ b/models/user.go @@ -214,6 +214,17 @@ func (u *User) UpdateAppMetaData(tx *storage.Connection, updates map[string]inte return tx.UpdateOnly(u, "raw_app_meta_data") } +// UpdateAppMetaDataProvider updates the provider field in AppMetaData column +func (u *User) UpdateAppMetaDataProvider(tx *storage.Connection) error { + providers, terr := FindProvidersByUser(tx, u) + if terr != nil { + return terr + } + return u.UpdateAppMetaData(tx, map[string]interface{}{ + "provider": providers, + }) +} + // SetEmail sets the user's email func (u *User) SetEmail(tx *storage.Connection, email string) error { u.Email = storage.NullString(email)