Repository: supabase/gotrue
Pull request #208 (2021-09-07 11:11:36)

Refactor user schema to store provider data better
## What kind of change does this PR introduce?
- [x] All OAuth providers should return the provider id which is unique to a user (regardless of whether the user changes the email associated with the provider login, the provider id will remain the same)
- [x] Use provider id as primary identifier instead of email for external oauth flows
- [x] Create identities table to each identity associated with a user 
- [x] `raw_app_meta_data.provider` field will be marked as deprecated
- [x] Return `identities` in access token payload in the following format:
```
{
  "aud": "authenticated",
  "exp": 1632719084,
  "sub": "12345",
  "email": "a@a.com",
  "phone": "",
  "app_metadata": {
    "provider": "google"
  },
  "user_metadata": {
    ...
  },
  "identities": [
    {
      "id": "1",
      "user_id": "12345",
      "identity_data": {
        "avatar_url": "",
        "email": "a@a.com",
        "email_verified": true,
        "full_name": "a",
        "iss": "https://www.googleapis.com/userinfo/v2/me",
        "name": "abc",
        "picture": "",
        "provider_id": "1",
        "sub": "1"
      },
      "provider": "google",
      "last_sign_in_at": "2021-09-27T10:13:48.877604+08:00",
      "created_at": "2021-09-27T10:13:48.877648+08:00",
      "updated_at": "2021-09-27T10:13:48.877652+08:00"
    },
    {
      "id": "123",
      "user_id": "12345",
      "identity_data": {
        "email": "a@a.com",
        "email_verified": true,
        "iss": "https://appleid.apple.com/auth/keys",
        "provider_id": "123",
        "sub": "123"
      },
      "provider": "apple",
      "last_sign_in_at": "2021-09-27T12:04:43.376864+08:00",
      "created_at": "2021-09-27T12:04:43.37691+08:00",
      "updated_at": "2021-09-27T12:04:43.376916+08:00"
    }
  ],
  "role": "authenticated"
}
```


Patch:
diff --git a/api/admin.go b/api/admin.go
index 5d7a6812..fb85bcd1 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/external.go b/api/external.go
index 1ea3c7fe..28cfbd3f 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_oauth.go b/api/external_oauth.go
index c15ea743..3a4899ef 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 cb447a1d..fa74d1cd 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/provider/apple.go b/api/provider/apple.go
index aaec711b..148841af 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 903adbef..fe78cf92 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 475c912e..6fb0f733 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 760564de..b323abfc 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 9c9407b4..156552b2 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 76e8bb45..67b95bb2 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 f63eefd7..1d5c7824 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 bf9de6d7..e2e060ec 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 410389e6..11964572 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 edcea510..01bb806d 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 a046e976..a3a752c3 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 8e280bb1..77a88b62 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/token.go b/api/token.go
index 404041df..3f0d94df 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/example.env b/example.env
index c0132899..73b973a9 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 00000000..10b1f2b9
--- /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 89a8fd5a..7c01a1de 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 f568364f..6c33c70c 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 00000000..74b9383b
--- /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/user.go b/models/user.go
index f0e63461..48adc2ff 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)


Test patch:
diff --git a/api/admin_test.go b/api/admin_test.go
index 36bd35aa..06e11b02 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 b6a3fdd0..a7972330 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_azure_test.go b/api/external_azure_test.go
index 40874db2..0626e9a7 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 29d9d278..ab48a1e1 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 e6156212..057cd9c5 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 adddfc22..25371543 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 bd2831a7..20eea356 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 ba6c4f86..8a8b0fbf 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 27288087..8992d0a6 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_test.go b/api/external_test.go
index 0cb7e326..9aee2c30 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 f8f8b340..a4e473ca 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 14d58f21..712b487a 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/signup_test.go b/api/signup_test.go
index 60e33dd3..c0282c3d 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/user_test.go b/api/user_test.go
index 3510c3e4..bb8d22b4 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/models/identity_test.go b/models/identity_test.go
new file mode 100644
index 00000000..c94b6a56
--- /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
+}