Repository: supabase/gotrue
Pull request #1348 (2023-12-15 08:23:29)

fix: handle oauth email check separately
## What kind of change does this PR introduce?
* Instead of returning an error in every provider if the email doesn't exist, we check if the email exists in `internalExternalProviderCallback` instead
* Setting the `Email` and `EmailVerified` claims in the `Claims` struct is redundant since we are tracking it separately in `Emails`. 
 
```go
type UserProvidedData struct {
	Emails   []Email
	Metadata *Claims
}
```

* Fix buggy oauth tests that appear to pass but they were actually returning the wrong value / wrong expected error
* This PR lays the foundation for removing the dependency on the email returned by the oauth provider



Patch:
diff --git a/internal/api/external.go b/internal/api/external.go
index 4c65fb46..9ccfdb19 100644
--- a/internal/api/external.go
+++ b/internal/api/external.go
@@ -187,7 +187,22 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 	if err != nil {
 		return err
 	}
+
 	userData := data.userData
+	if len(userData.Emails) <= 0 {
+		return internalServerError("Error getting user email from external provider")
+	}
+	userData.Metadata.EmailVerified = false
+	for _, email := range userData.Emails {
+		if email.Primary {
+			userData.Metadata.Email = email.Email
+			userData.Metadata.EmailVerified = email.Verified
+			break
+		} else {
+			userData.Metadata.Email = email.Email
+			userData.Metadata.EmailVerified = email.Verified
+		}
+	}
 	providerAccessToken := data.token
 	providerRefreshToken := data.refreshToken
 
diff --git a/internal/api/external_oauth.go b/internal/api/external_oauth.go
index da5f0e69..2fd93d22 100644
--- a/internal/api/external_oauth.go
+++ b/internal/api/external_oauth.go
@@ -82,7 +82,7 @@ func (a *API) oAuthCallback(ctx context.Context, r *http.Request, providerType s
 
 	userData, err := oAuthProvider.GetUserData(ctx, token)
 	if err != nil {
-		return nil, internalServerError("Error getting user email from external provider").WithInternalError(err)
+		return nil, internalServerError("Error getting user profile from external provider").WithInternalError(err)
 	}
 
 	switch externalProvider := oAuthProvider.(type) {
diff --git a/internal/api/provider/bitbucket.go b/internal/api/provider/bitbucket.go
index 218bd167..e5fae5c9 100644
--- a/internal/api/provider/bitbucket.go
+++ b/internal/api/provider/bitbucket.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 
 	"github.com/supabase/auth/internal/conf"
 	"golang.org/x/oauth2"
@@ -89,10 +88,6 @@ func (g bitbucketProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (
 		}
 	}
 
-	if len(data.Emails) <= 0 {
-		return nil, errors.New("unable to find email with Bitbucket provider")
-	}
-
 	data.Metadata = &Claims{
 		Issuer:  g.APIPath,
 		Subject: u.ID,
diff --git a/internal/api/provider/discord.go b/internal/api/provider/discord.go
index e500aaf6..50d413b7 100644
--- a/internal/api/provider/discord.go
+++ b/internal/api/provider/discord.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"strconv"
 	"strings"
@@ -72,8 +71,13 @@ func (g discordProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Discord provider")
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
+			Email:    u.Email,
+			Verified: u.Verified,
+			Primary:  true,
+		}}
 	}
 
 	var avatarURL string
@@ -97,27 +101,20 @@ func (g discordProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U
 		avatarURL = fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.%s", u.ID, u.Avatar, extension)
 	}
 
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        g.APIPath,
-			Subject:       u.ID,
-			Name:          fmt.Sprintf("%v#%v", u.Name, u.Discriminator),
-			Picture:       avatarURL,
-			Email:         u.Email,
-			EmailVerified: u.Verified,
-			CustomClaims: map[string]interface{}{
-				"global_name": u.GlobalName,
-			},
-
-			// To be deprecated
-			AvatarURL:  avatarURL,
-			FullName:   u.Name,
-			ProviderId: u.ID,
+	data.Metadata = &Claims{
+		Issuer:  g.APIPath,
+		Subject: u.ID,
+		Name:    fmt.Sprintf("%v#%v", u.Name, u.Discriminator),
+		Picture: avatarURL,
+		CustomClaims: map[string]interface{}{
+			"global_name": u.GlobalName,
 		},
-		Emails: []Email{{
-			Email:    u.Email,
-			Verified: u.Verified,
-			Primary:  true,
-		}},
-	}, nil
+
+		// To be deprecated
+		AvatarURL:  avatarURL,
+		FullName:   u.Name,
+		ProviderId: u.ID,
+	}
+
+	return data, nil
 }
diff --git a/internal/api/provider/facebook.go b/internal/api/provider/facebook.go
index 2c5d08b7..e73c419d 100644
--- a/internal/api/provider/facebook.go
+++ b/internal/api/provider/facebook.go
@@ -5,7 +5,6 @@ import (
 	"crypto/hmac"
 	"crypto/sha256"
 	"encoding/hex"
-	"errors"
 	"strings"
 
 	"github.com/supabase/auth/internal/conf"
@@ -86,30 +85,28 @@ func (p facebookProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Facebook provider")
-	}
-
-	return &UserProvidedData{
-		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{{
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Email,
 			Verified: true,
 			Primary:  true,
-		}},
-	}, nil
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:   p.ProfileURL,
+		Subject:  u.ID,
+		Name:     strings.TrimSpace(u.FirstName + " " + u.LastName),
+		NickName: u.Alias,
+		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,
+	}
+
+	return data, nil
 }
diff --git a/internal/api/provider/figma.go b/internal/api/provider/figma.go
index 0877d792..2ebdd013 100644
--- a/internal/api/provider/figma.go
+++ b/internal/api/provider/figma.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/supabase/auth/internal/conf"
@@ -72,27 +71,26 @@ func (p figmaProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Figma provider")
-	}
-
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        p.APIHost,
-			Subject:       u.ID,
-			Name:          u.Name,
-			Email:         u.Email,
-			EmailVerified: true,
-
-			// To be deprecated
-			AvatarURL:  u.AvatarURL,
-			FullName:   u.Name,
-			ProviderId: u.ID,
-		},
-		Emails: []Email{{
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Email,
 			Verified: true,
 			Primary:  true,
-		}},
-	}, nil
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:        p.APIHost,
+		Subject:       u.ID,
+		Name:          u.Name,
+		Email:         u.Email,
+		EmailVerified: true,
+
+		// To be deprecated
+		AvatarURL:  u.AvatarURL,
+		FullName:   u.Name,
+		ProviderId: u.ID,
+	}
+	return data, nil
 }
diff --git a/internal/api/provider/fly.go b/internal/api/provider/fly.go
index ffde096c..d9337524 100644
--- a/internal/api/provider/fly.go
+++ b/internal/api/provider/fly.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/supabase/auth/internal/conf"
@@ -76,30 +75,29 @@ func (p flyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserP
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Fly provider")
-	}
-
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        p.APIPath,
-			Subject:       u.UserID,
-			FullName:      u.UserName,
-			Email:         u.Email,
-			EmailVerified: true,
-			ProviderId:    u.UserID,
-			CustomClaims: map[string]interface{}{
-				"resource_owner_id": u.ResourceOwnerID,
-				"organizations":     u.Organizations,
-				"application":       u.Application,
-				"scope":             u.Scope,
-				"created_at":        u.CreatedAt,
-			},
-		},
-		Emails: []Email{{
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Email,
 			Verified: true,
 			Primary:  true,
-		}},
-	}, nil
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:        p.APIPath,
+		Subject:       u.UserID,
+		FullName:      u.UserName,
+		Email:         u.Email,
+		EmailVerified: true,
+		ProviderId:    u.UserID,
+		CustomClaims: map[string]interface{}{
+			"resource_owner_id": u.ResourceOwnerID,
+			"organizations":     u.Organizations,
+			"application":       u.Application,
+			"scope":             u.Scope,
+			"created_at":        u.CreatedAt,
+		},
+	}
+	return data, nil
 }
diff --git a/internal/api/provider/github.go b/internal/api/provider/github.go
index bbf036fd..0da3e884 100644
--- a/internal/api/provider/github.go
+++ b/internal/api/provider/github.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strconv"
 	"strings"
 
@@ -105,15 +104,6 @@ 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 {
-		return nil, errors.New("unable to find email with GitHub provider")
 	}
 
 	return data, nil
diff --git a/internal/api/provider/gitlab.go b/internal/api/provider/gitlab.go
index 0b9c80f8..4b5d70cb 100644
--- a/internal/api/provider/gitlab.go
+++ b/internal/api/provider/gitlab.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strconv"
 	"strings"
 
@@ -92,17 +91,11 @@ func (g gitlabProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		data.Emails = append(data.Emails, Email{Email: u.Email, Verified: verified, Primary: true})
 	}
 
-	if len(data.Emails) <= 0 {
-		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,
+		Issuer:  g.Host,
+		Subject: strconv.Itoa(u.ID),
+		Name:    u.Name,
+		Picture: u.AvatarURL,
 
 		// To be deprecated
 		AvatarURL:  u.AvatarURL,
diff --git a/internal/api/provider/google.go b/internal/api/provider/google.go
index 60849759..03b76aeb 100644
--- a/internal/api/provider/google.go
+++ b/internal/api/provider/google.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/coreos/go-oidc/v3/oidc"
@@ -115,10 +114,6 @@ func (g googleProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		})
 	}
 
-	if len(data.Emails) <= 0 {
-		return nil, errors.New("provider: Google OAuth2 user info endpoint did not return an email address")
-	}
-
 	data.Metadata = &Claims{
 		Issuer:        internalUserInfoEndpointGoogle,
 		Subject:       u.ID,
diff --git a/internal/api/provider/kakao.go b/internal/api/provider/kakao.go
index 4340e808..0963a918 100644
--- a/internal/api/provider/kakao.go
+++ b/internal/api/provider/kakao.go
@@ -43,29 +43,30 @@ func (p kakaoProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
 		return nil, err
 	}
 
-	data := &UserProvidedData{
-		Emails: []Email{
+	data := &UserProvidedData{}
+
+	if u.Account.Email != "" {
+		data.Emails = []Email{
 			{
 				Email:    u.Account.Email,
 				Verified: u.Account.EmailVerified && u.Account.EmailValid,
 				Primary:  true,
 			},
-		},
-		Metadata: &Claims{
-			Issuer:        p.APIHost,
-			Subject:       strconv.Itoa(u.ID),
-			Email:         u.Account.Email,
-			EmailVerified: u.Account.EmailVerified && u.Account.EmailValid,
-
-			Name:              u.Account.Profile.Name,
-			PreferredUsername: u.Account.Profile.Name,
-
-			// To be deprecated
-			AvatarURL:   u.Account.Profile.ProfileImageURL,
-			FullName:    u.Account.Profile.Name,
-			ProviderId:  strconv.Itoa(u.ID),
-			UserNameKey: u.Account.Profile.Name,
-		},
+		}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:  p.APIHost,
+		Subject: strconv.Itoa(u.ID),
+
+		Name:              u.Account.Profile.Name,
+		PreferredUsername: u.Account.Profile.Name,
+
+		// To be deprecated
+		AvatarURL:   u.Account.Profile.ProfileImageURL,
+		FullName:    u.Account.Profile.Name,
+		ProviderId:  strconv.Itoa(u.ID),
+		UserNameKey: u.Account.Profile.Name,
 	}
 	return data, nil
 }
diff --git a/internal/api/provider/keycloak.go b/internal/api/provider/keycloak.go
index 8ed0b5ea..39ccec5b 100644
--- a/internal/api/provider/keycloak.go
+++ b/internal/api/provider/keycloak.go
@@ -72,27 +72,27 @@ func (g keycloakProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Keycloak provider")
-	}
-
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        g.Host,
-			Subject:       u.Sub,
-			Name:          u.Name,
-			Email:         u.Email,
-			EmailVerified: u.EmailVerified,
-
-			// To be deprecated
-			FullName:   u.Name,
-			ProviderId: u.Sub,
-		},
-		Emails: []Email{{
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Email,
 			Verified: u.EmailVerified,
 			Primary:  true,
-		}},
-	}, nil
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:        g.Host,
+		Subject:       u.Sub,
+		Name:          u.Name,
+		Email:         u.Email,
+		EmailVerified: u.EmailVerified,
+
+		// To be deprecated
+		FullName:   u.Name,
+		ProviderId: u.Sub,
+	}
+
+	return data, nil
 
 }
diff --git a/internal/api/provider/linkedin.go b/internal/api/provider/linkedin.go
index e9134bec..bc33515e 100644
--- a/internal/api/provider/linkedin.go
+++ b/internal/api/provider/linkedin.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/supabase/auth/internal/conf"
@@ -120,37 +119,31 @@ func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*
 		return nil, err
 	}
 
-	if len(e.Elements) <= 0 {
-		return nil, errors.New("unable to find email with Linkedin provider")
-	}
-
-	emails := []Email{}
+	data := &UserProvidedData{}
 
 	if e.Elements[0].HandleTilde.EmailAddress != "" {
 		// linkedin only returns the primary email which is verified for the r_emailaddress scope.
-		emails = append(emails, Email{
+		data.Emails = []Email{{
 			Email:    e.Elements[0].HandleTilde.EmailAddress,
 			Primary:  true,
 			Verified: true,
-		})
+		}}
 	}
 
 	avatarURL := u.getAvatarUrl()
 
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        g.APIPath,
-			Subject:       u.ID,
-			Name:          strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)),
-			Picture:       avatarURL,
-			Email:         e.Elements[0].HandleTilde.EmailAddress,
-			EmailVerified: true,
-
-			// To be deprecated
-			AvatarURL:  avatarURL,
-			FullName:   strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)),
-			ProviderId: u.ID,
-		},
-		Emails: emails,
-	}, nil
+	data.Metadata = &Claims{
+		Issuer:        g.APIPath,
+		Subject:       u.ID,
+		Name:          strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)),
+		Picture:       avatarURL,
+		Email:         e.Elements[0].HandleTilde.EmailAddress,
+		EmailVerified: true,
+
+		// To be deprecated
+		AvatarURL:  avatarURL,
+		FullName:   strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)),
+		ProviderId: u.ID,
+	}
+	return data, nil
 }
diff --git a/internal/api/provider/notion.go b/internal/api/provider/notion.go
index 3750b3e6..f8d0ee70 100644
--- a/internal/api/provider/notion.go
+++ b/internal/api/provider/notion.go
@@ -3,7 +3,6 @@ package provider
 import (
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -98,28 +97,25 @@ func (g notionProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		return nil, err
 	}
 
-	if u.Bot.Owner.User.Person.Email == "" {
-		return nil, errors.New("unable to find email with notion provider")
-	}
-
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        g.APIPath,
-			Subject:       u.Bot.Owner.User.ID,
-			Name:          u.Bot.Owner.User.Name,
-			Picture:       u.Bot.Owner.User.AvatarURL,
-			Email:         u.Bot.Owner.User.Person.Email,
-			EmailVerified: true, // Notion dosen't provide data on if email is verified.
-
-			// To be deprecated
-			AvatarURL:  u.Bot.Owner.User.AvatarURL,
-			FullName:   u.Bot.Owner.User.Name,
-			ProviderId: u.Bot.Owner.User.ID,
-		},
-		Emails: []Email{{
+	data := &UserProvidedData{}
+	if u.Bot.Owner.User.Person.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Bot.Owner.User.Person.Email,
 			Verified: true, // Notion dosen't provide data on if email is verified.
 			Primary:  true,
-		}},
-	}, nil
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:  g.APIPath,
+		Subject: u.Bot.Owner.User.ID,
+		Name:    u.Bot.Owner.User.Name,
+		Picture: u.Bot.Owner.User.AvatarURL,
+
+		// To be deprecated
+		AvatarURL:  u.Bot.Owner.User.AvatarURL,
+		FullName:   u.Bot.Owner.User.Name,
+		ProviderId: u.Bot.Owner.User.ID,
+	}
+	return data, nil
 }
diff --git a/internal/api/provider/oidc.go b/internal/api/provider/oidc.go
index 53baf984..be487dfc 100644
--- a/internal/api/provider/oidc.go
+++ b/internal/api/provider/oidc.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"strconv"
 	"strings"
@@ -97,17 +96,11 @@ func parseGoogleIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData,
 		})
 	}
 
-	if len(data.Emails) <= 0 {
-		return nil, nil, errors.New("provider: Google ID token must contain an email address")
-	}
-
 	data.Metadata = &Claims{
-		Issuer:        claims.Issuer,
-		Subject:       claims.Subject,
-		Name:          claims.Name,
-		Picture:       claims.AvatarURL,
-		Email:         claims.Email,
-		EmailVerified: claims.IsEmailVerified(),
+		Issuer:  claims.Issuer,
+		Subject: claims.Subject,
+		Name:    claims.Name,
+		Picture: claims.AvatarURL,
 
 		// To be deprecated
 		AvatarURL:  claims.AvatarURL,
@@ -148,12 +141,10 @@ func parseAppleIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, e
 	})
 
 	data.Metadata = &Claims{
-		Issuer:        token.Issuer,
-		Subject:       token.Subject,
-		Email:         claims.Email,
-		EmailVerified: true,
-		ProviderId:    token.Subject,
-		CustomClaims:  make(map[string]any),
+		Issuer:       token.Issuer,
+		Subject:      token.Subject,
+		ProviderId:   token.Subject,
+		CustomClaims: make(map[string]any),
 	}
 
 	if claims.IsPrivateEmail != nil {
@@ -193,23 +184,24 @@ func parseLinkedinIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData
 	if err != nil {
 		return nil, nil, err
 	}
-	data.Emails = append(data.Emails, Email{
-		Email:    claims.Email,
-		Verified: emailVerified,
-		Primary:  true,
-	})
+
+	if claims.Email != "" {
+		data.Emails = append(data.Emails, Email{
+			Email:    claims.Email,
+			Verified: emailVerified,
+			Primary:  true,
+		})
+	}
 
 	data.Metadata = &Claims{
-		Issuer:        token.Issuer,
-		Subject:       token.Subject,
-		Email:         claims.Email,
-		EmailVerified: emailVerified,
-		Name:          strings.TrimSpace(claims.GivenName + " " + claims.FamilyName),
-		GivenName:     claims.GivenName,
-		FamilyName:    claims.FamilyName,
-		Locale:        claims.Locale,
-		Picture:       claims.Picture,
-		ProviderId:    token.Subject,
+		Issuer:     token.Issuer,
+		Subject:    token.Subject,
+		Name:       strings.TrimSpace(claims.GivenName + " " + claims.FamilyName),
+		GivenName:  claims.GivenName,
+		FamilyName: claims.FamilyName,
+		Locale:     claims.Locale,
+		Picture:    claims.Picture,
+		ProviderId: token.Subject,
 	}
 
 	return token, &data, nil
@@ -293,20 +285,18 @@ func parseAzureIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, e
 	data.Metadata = &Claims{
 		Issuer:            token.Issuer,
 		Subject:           token.Subject,
-		Email:             azureClaims.Email,
-		EmailVerified:     azureClaims.IsEmailVerified(),
 		ProviderId:        token.Subject,
 		PreferredUsername: azureClaims.PreferredUsername,
 		FullName:          azureClaims.Name,
 		CustomClaims:      make(map[string]any),
 	}
 
-	if data.Metadata.Email != "" {
-		data.Emails = append(data.Emails, Email{
-			Email:    data.Metadata.Email,
-			Verified: data.Metadata.EmailVerified,
+	if azureClaims.Email != "" {
+		data.Emails = []Email{{
+			Email:    azureClaims.Email,
+			Verified: azureClaims.IsEmailVerified(),
 			Primary:  true,
-		})
+		}}
 	}
 
 	if err := token.Claims(&data.Metadata.CustomClaims); err != nil {
@@ -319,10 +309,6 @@ func parseAzureIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, e
 		}
 	}
 
-	if len(data.Emails) <= 0 {
-		return nil, nil, fmt.Errorf("provider: Azure OIDC ID token from issuer %q must contain a valid email address", token.Issuer)
-	}
-
 	return token, &data, nil
 }
 
diff --git a/internal/api/provider/slack.go b/internal/api/provider/slack.go
index f18ecfbc..efe31881 100644
--- a/internal/api/provider/slack.go
+++ b/internal/api/provider/slack.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/supabase/auth/internal/conf"
@@ -68,31 +67,28 @@ func (g slackProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Slack provider")
-	}
-
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        g.APIPath,
-			Subject:       u.ID,
-			Name:          u.Name,
-			Picture:       u.AvatarURL,
-			Email:         u.Email,
-			EmailVerified: true, // Slack dosen't provide data on if email is verified.
-			CustomClaims: map[string]interface{}{
-				"https://slack.com/team_id": u.TeamID,
-			},
-
-			// To be deprecated
-			AvatarURL:  u.AvatarURL,
-			FullName:   u.Name,
-			ProviderId: u.ID,
-		},
-		Emails: []Email{{
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Email,
 			Verified: true, // Slack dosen't provide data on if email is verified.
 			Primary:  true,
-		}},
-	}, nil
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:  g.APIPath,
+		Subject: u.ID,
+		Name:    u.Name,
+		Picture: u.AvatarURL,
+		CustomClaims: map[string]interface{}{
+			"https://slack.com/team_id": u.TeamID,
+		},
+
+		// To be deprecated
+		AvatarURL:  u.AvatarURL,
+		FullName:   u.Name,
+		ProviderId: u.ID,
+	}
+	return data, nil
 }
diff --git a/internal/api/provider/spotify.go b/internal/api/provider/spotify.go
index 55064057..57fe8c0f 100644
--- a/internal/api/provider/spotify.go
+++ b/internal/api/provider/spotify.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/supabase/auth/internal/conf"
@@ -74,8 +73,15 @@ func (g spotifyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Spotify provider")
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
+			Email: u.Email,
+			// Spotify dosen't provide data on whether the user's email is verified.
+			// https://developer.spotify.com/documentation/web-api/reference/get-current-users-profile
+			Verified: false,
+			Primary:  true,
+		}}
 	}
 
 	var avatarURL string
@@ -84,26 +90,16 @@ func (g spotifyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U
 		avatarURL = u.Avatars[0].Url
 	}
 
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:  g.APIPath,
-			Subject: u.ID,
-			Name:    u.DisplayName,
-			Picture: avatarURL,
-			Email:   u.Email,
-			// Spotify dosen't provide data on whether the user's email is verified.
-			// https://developer.spotify.com/documentation/web-api/reference/get-current-users-profile
-			EmailVerified: false,
+	data.Metadata = &Claims{
+		Issuer:  g.APIPath,
+		Subject: u.ID,
+		Name:    u.DisplayName,
+		Picture: avatarURL,
 
-			// To be deprecated
-			AvatarURL:  avatarURL,
-			FullName:   u.DisplayName,
-			ProviderId: u.ID,
-		},
-		Emails: []Email{{
-			Email:    u.Email,
-			Verified: false,
-			Primary:  true,
-		}},
-	}, nil
+		// To be deprecated
+		AvatarURL:  avatarURL,
+		FullName:   u.DisplayName,
+		ProviderId: u.ID,
+	}
+	return data, nil
 }
diff --git a/internal/api/provider/twitch.go b/internal/api/provider/twitch.go
index 14ad1d14..defb1983 100644
--- a/internal/api/provider/twitch.go
+++ b/internal/api/provider/twitch.go
@@ -120,38 +120,34 @@ func (t twitchProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 
 	user := u.Data[0]
 
-	if user.Email == "" {
-		return nil, errors.New("unable to find email with twitch provider")
-	}
-
-	data := &UserProvidedData{
-		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{{
+	data := &UserProvidedData{}
+	if user.Email != "" {
+		data.Emails = []Email{{
 			Email:    user.Email,
 			Verified: true,
 			Primary:  true,
-		}},
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:   t.APIHost,
+		Subject:  user.ID,
+		Picture:  user.ProfileImageURL,
+		Name:     user.Login,
+		NickName: user.DisplayName,
+		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,
 	}
 
 	return data, nil
diff --git a/internal/api/provider/twitter.go b/internal/api/provider/twitter.go
index 7d1aae55..8dc5a4c6 100644
--- a/internal/api/provider/twitter.go
+++ b/internal/api/provider/twitter.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -91,31 +90,27 @@ func (t TwitterProvider) FetchUserData(ctx context.Context, tok *oauth.AccessTok
 	}
 	_ = json.NewDecoder(bytes.NewReader(bits)).Decode(&u)
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Twitter provider")
-	}
-
-	data := &UserProvidedData{
-		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{{
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Email,
 			Verified: true,
 			Primary:  true,
-		}},
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:            t.UserInfoURL,
+		Subject:           u.ID,
+		Name:              u.Name,
+		Picture:           u.AvatarURL,
+		PreferredUsername: u.UserName,
+
+		// To be deprecated
+		UserNameKey: u.UserName,
+		FullName:    u.Name,
+		AvatarURL:   u.AvatarURL,
+		ProviderId:  u.ID,
 	}
 
 	return data, nil
diff --git a/internal/api/provider/workos.go b/internal/api/provider/workos.go
index f33c79b2..75cafa27 100644
--- a/internal/api/provider/workos.go
+++ b/internal/api/provider/workos.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/mitchellh/mapstructure"
@@ -72,30 +71,28 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with WorkOS provider")
-	}
-
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        g.APIPath,
-			Subject:       u.ID,
-			Name:          strings.TrimSpace(u.FirstName + " " + u.LastName),
-			Email:         u.Email,
-			EmailVerified: true,
-			CustomClaims: map[string]interface{}{
-				"connection_id":   u.ConnectionID,
-				"organization_id": u.OrganizationID,
-			},
-
-			// To be deprecated
-			FullName:   strings.TrimSpace(u.FirstName + " " + u.LastName),
-			ProviderId: u.ID,
-		},
-		Emails: []Email{{
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		data.Emails = []Email{{
 			Email:    u.Email,
 			Verified: true,
 			Primary:  true,
-		}},
-	}, nil
+		}}
+	}
+
+	data.Metadata = &Claims{
+		Issuer:  g.APIPath,
+		Subject: u.ID,
+		Name:    strings.TrimSpace(u.FirstName + " " + u.LastName),
+		CustomClaims: map[string]interface{}{
+			"connection_id":   u.ConnectionID,
+			"organization_id": u.OrganizationID,
+		},
+
+		// To be deprecated
+		FullName:   strings.TrimSpace(u.FirstName + " " + u.LastName),
+		ProviderId: u.ID,
+	}
+
+	return data, nil
 }
diff --git a/internal/api/provider/zoom.go b/internal/api/provider/zoom.go
index 7fecfbd2..8e2e9fa4 100644
--- a/internal/api/provider/zoom.go
+++ b/internal/api/provider/zoom.go
@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"strings"
 
 	"github.com/supabase/auth/internal/conf"
@@ -62,38 +61,31 @@ func (g zoomProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*User
 		return nil, err
 	}
 
-	if u.Email == "" {
-		return nil, errors.New("unable to find email with Zoom provider")
+	data := &UserProvidedData{}
+	if u.Email != "" {
+		email := Email{}
+		email.Email = u.Email
+		email.Primary = true
+		// A login_type of "100" refers to email-based logins, not oauth.
+		// A user is verified (type 1) only if they received an email when their profile was created and confirmed the link.
+		// A zoom user will only be sent an email confirmation link if they signed up using their zoom work email and not oauth.
+		// See: https://devforum.zoom.us/t/how-to-determine-if-a-zoom-user-actually-owns-their-email-address/44430
+		if u.LoginType != "100" || u.EmailVerified != 0 {
+			email.Verified = true
+		}
+		data.Emails = []Email{email}
 	}
 
-	verified := true
+	data.Metadata = &Claims{
+		Issuer:  g.APIPath,
+		Subject: u.ID,
+		Name:    strings.TrimSpace(u.FirstName + " " + u.LastName),
+		Picture: u.AvatarURL,
 
-	// A login_type of "100" refers to email-based logins, not oauth.
-	// A user is verified (type 1) only if they received an email when their profile was created and confirmed the link.
-	// A zoom user will only be sent an email confirmation link if they signed up using their zoom work email and not oauth.
-	// See: https://devforum.zoom.us/t/how-to-determine-if-a-zoom-user-actually-owns-their-email-address/44430
-	if u.LoginType == "100" && u.EmailVerified == 0 {
-		verified = false
+		// To be deprecated
+		AvatarURL:  u.AvatarURL,
+		FullName:   strings.TrimSpace(u.FirstName + " " + u.LastName),
+		ProviderId: u.ID,
 	}
-
-	return &UserProvidedData{
-		Metadata: &Claims{
-			Issuer:        g.APIPath,
-			Subject:       u.ID,
-			Name:          strings.TrimSpace(u.FirstName + " " + u.LastName),
-			Picture:       u.AvatarURL,
-			Email:         u.Email,
-			EmailVerified: verified,
-
-			// To be deprecated
-			AvatarURL:  u.AvatarURL,
-			FullName:   strings.TrimSpace(u.FirstName + " " + u.LastName),
-			ProviderId: u.ID,
-		},
-		Emails: []Email{{
-			Email:    u.Email,
-			Verified: verified,
-			Primary:  true,
-		}},
-	}, nil
+	return data, nil
 }
diff --git a/internal/api/token_oidc.go b/internal/api/token_oidc.go
index 3116c5ea..e1bcd369 100644
--- a/internal/api/token_oidc.go
+++ b/internal/api/token_oidc.go
@@ -138,6 +138,18 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R
 		return oauthError("invalid request", "Bad ID token").WithInternalError(err)
 	}
 
+	userData.Metadata.EmailVerified = false
+	for _, email := range userData.Emails {
+		if email.Primary {
+			userData.Metadata.Email = email.Email
+			userData.Metadata.EmailVerified = email.Verified
+			break
+		} else {
+			userData.Metadata.Email = email.Email
+			userData.Metadata.EmailVerified = email.Verified
+		}
+	}
+
 	if idToken.Subject == "" {
 		return oauthError("invalid request", "Missing sub claim in id_token")
 	}


Test patch:
diff --git a/internal/api/external_google_test.go b/internal/api/external_google_test.go
index 5eb94e64..8458be5d 100644
--- a/internal/api/external_google_test.go
+++ b/internal/api/external_google_test.go
@@ -15,7 +15,7 @@ import (
 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}}`
+	googleUserNoEmail    string = `{"id":"googleTestId","name":"Google Test","picture":"http://example.com/avatar","verified_email":false}}`
 )
 
 func (ts *ExternalTestSuite) TestSignupExternalGoogle() {
diff --git a/internal/api/external_kakao_test.go b/internal/api/external_kakao_test.go
index c711c610..7882e1dc 100644
--- a/internal/api/external_kakao_test.go
+++ b/internal/api/external_kakao_test.go
@@ -63,25 +63,33 @@ func KakaoTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int
 				}
 			}
 
-			if email == nil {
-				w.WriteHeader(400)
-				return
-			}
-
 			w.Header().Add("Content-Type", "application/json")
-			fmt.Fprintf(w, `
-				{
-					"id":123, 
-					"kakao_account": {
-						"profile": {
-							"nickname":"Kakao Test",
-							"profile_image_url":"http://example.com/avatar"
-						},
-						"email": "%v",
-						"is_email_valid": %v,
-						"is_email_verified": %v
-					}
-				}`, email.Email, email.Verified, email.Verified)
+			if email != nil {
+				fmt.Fprintf(w, `
+					{
+						"id":123, 
+						"kakao_account": {
+							"profile": {
+								"nickname":"Kakao Test",
+								"profile_image_url":"http://example.com/avatar"
+							},
+							"email": "%v",
+							"is_email_valid": %v,
+							"is_email_verified": %v
+						}
+					}`, email.Email, email.Verified, email.Verified)
+			} else {
+				fmt.Fprint(w, `
+					{
+						"id":123, 
+						"kakao_account": {
+							"profile": {
+								"nickname":"Kakao Test",
+								"profile_image_url":"http://example.com/avatar"
+							}
+						}
+					}`)
+			}
 		default:
 			w.WriteHeader(500)
 			ts.Fail("unknown kakao oauth call %s", r.URL.Path)
diff --git a/internal/api/external_notion_test.go b/internal/api/external_notion_test.go
index cb25ad27..1680b14c 100644
--- a/internal/api/external_notion_test.go
+++ b/internal/api/external_notion_test.go
@@ -12,7 +12,7 @@ import (
 const (
 	notionUser           string = `{"bot":{"owner":{"user":{"id":"notionTestId","name":"Notion Test","avatar_url":"http://example.com/avatar","person":{"email":"notion@example.com"},"verified_email":true}}}}`
 	notionUserWrongEmail string = `{"bot":{"owner":{"user":{"id":"notionTestId","name":"Notion Test","avatar_url":"http://example.com/avatar","person":{"email":"other@example.com"},"verified_email":true}}}}`
-	notionUserNoEmail    string = `{"bot":{"owner":{"user":{"id":"notionTestId","name":"Notion Test","avatar_url":"http://example.com/avatar","verified_email":true}}}}}`
+	notionUserNoEmail    string = `{"bot":{"owner":{"user":{"id":"notionTestId","name":"Notion Test","avatar_url":"http://example.com/avatar","verified_email":true}}}}`
 )
 
 func (ts *ExternalTestSuite) TestSignupExternalNotion() {