204 lines
6.7 KiB
Go
204 lines
6.7 KiB
Go
package models
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/supabase/auth/internal/api/provider"
|
|
"github.com/supabase/auth/internal/conf"
|
|
"github.com/supabase/auth/internal/storage"
|
|
)
|
|
|
|
// GetAccountLinkingDomain returns a string that describes the account linking
|
|
// domain. An account linking domain describes a set of Identity entities that
|
|
// _should_ generally fall under the same User entity. It's just a runtime
|
|
// string, and is not typically persisted in the database. This value can vary
|
|
// across time.
|
|
func GetAccountLinkingDomain(provider string) string {
|
|
if strings.HasPrefix(provider, "sso:") {
|
|
// when the provider ID is a SSO provider, then the linking
|
|
// domain is the provider itself i.e. there can only be one
|
|
// user + identity per identity provider
|
|
return provider
|
|
}
|
|
|
|
// otherwise, the linking domain is the default linking domain that
|
|
// links all accounts
|
|
return "default"
|
|
}
|
|
|
|
type AccountLinkingDecision = int
|
|
|
|
const (
|
|
AccountExists AccountLinkingDecision = iota
|
|
CreateAccount
|
|
LinkAccount
|
|
MultipleAccounts
|
|
)
|
|
|
|
type AccountLinkingResult struct {
|
|
Decision AccountLinkingDecision
|
|
User *User
|
|
Identities []*Identity
|
|
LinkingDomain string
|
|
CandidateEmail provider.Email
|
|
}
|
|
|
|
// DetermineAccountLinking uses the provided data and database state to compute a decision on whether:
|
|
// - A new User should be created (CreateAccount)
|
|
// - A new Identity should be created (LinkAccount) with a UserID pointing to an existing user account
|
|
// - Nothing should be done (AccountExists)
|
|
// - It's not possible to decide due to data inconsistency (MultipleAccounts) and the caller should decide
|
|
//
|
|
// Errors signal failure in processing only, like database access errors.
|
|
func DetermineAccountLinking(tx *storage.Connection, config *conf.GlobalConfiguration, emails []provider.Email, aud, providerName, sub string) (AccountLinkingResult, error) {
|
|
var verifiedEmails []string
|
|
var candidateEmail provider.Email
|
|
for _, email := range emails {
|
|
if email.Verified || config.Mailer.Autoconfirm {
|
|
verifiedEmails = append(verifiedEmails, strings.ToLower(email.Email))
|
|
}
|
|
if email.Primary {
|
|
candidateEmail = email
|
|
candidateEmail.Email = strings.ToLower(email.Email)
|
|
}
|
|
}
|
|
|
|
if identity, terr := FindIdentityByIdAndProvider(tx, sub, providerName); terr == nil {
|
|
// account exists
|
|
|
|
var user *User
|
|
if user, terr = FindUserByID(tx, identity.UserID); terr != nil {
|
|
return AccountLinkingResult{}, terr
|
|
}
|
|
|
|
// we overwrite the email with the existing user's email since the user
|
|
// could have an empty email
|
|
candidateEmail.Email = user.GetEmail()
|
|
return AccountLinkingResult{
|
|
Decision: AccountExists,
|
|
User: user,
|
|
Identities: []*Identity{identity},
|
|
LinkingDomain: GetAccountLinkingDomain(providerName),
|
|
CandidateEmail: candidateEmail,
|
|
}, nil
|
|
} else if !IsNotFoundError(terr) {
|
|
return AccountLinkingResult{}, terr
|
|
}
|
|
|
|
// the identity does not exist, so we need to check if we should create a new account
|
|
// or link to an existing one
|
|
|
|
// this is the linking domain for the new identity
|
|
candidateLinkingDomain := GetAccountLinkingDomain(providerName)
|
|
if len(verifiedEmails) == 0 {
|
|
// if there are no verified emails, we always decide to create a new account
|
|
user, terr := IsDuplicatedEmail(tx, candidateEmail.Email, aud, nil)
|
|
if terr != nil {
|
|
return AccountLinkingResult{}, terr
|
|
}
|
|
if user != nil {
|
|
candidateEmail.Email = ""
|
|
}
|
|
return AccountLinkingResult{
|
|
Decision: CreateAccount,
|
|
LinkingDomain: candidateLinkingDomain,
|
|
CandidateEmail: candidateEmail,
|
|
}, nil
|
|
}
|
|
|
|
var similarIdentities []*Identity
|
|
var similarUsers []*User
|
|
// look for similar identities and users based on email
|
|
if terr := tx.Q().Eager().Where("email = any (?)", verifiedEmails).All(&similarIdentities); terr != nil {
|
|
return AccountLinkingResult{}, terr
|
|
}
|
|
|
|
if !strings.HasPrefix(providerName, "sso:") {
|
|
// there can be multiple user accounts with the same email when is_sso_user is true
|
|
// so we just do not consider those similar user accounts
|
|
if terr := tx.Q().Eager().Where("email = any (?) and is_sso_user = false", verifiedEmails).All(&similarUsers); terr != nil {
|
|
return AccountLinkingResult{}, terr
|
|
}
|
|
}
|
|
|
|
// Need to check if the new identity should be assigned to an
|
|
// existing user or to create a new user, according to the automatic
|
|
// linking rules
|
|
var linkingIdentities []*Identity
|
|
|
|
// now let's see if there are any existing and similar identities in
|
|
// the same linking domain
|
|
for _, identity := range similarIdentities {
|
|
if GetAccountLinkingDomain(identity.Provider) == candidateLinkingDomain {
|
|
linkingIdentities = append(linkingIdentities, identity)
|
|
}
|
|
}
|
|
|
|
if len(linkingIdentities) == 0 {
|
|
if len(similarUsers) == 1 {
|
|
// no similarIdentities but a user with the same email exists
|
|
// so we link this new identity to the user
|
|
// TODO: Backfill the missing identity for the user
|
|
return AccountLinkingResult{
|
|
Decision: LinkAccount,
|
|
User: similarUsers[0],
|
|
Identities: linkingIdentities,
|
|
LinkingDomain: candidateLinkingDomain,
|
|
CandidateEmail: candidateEmail,
|
|
}, nil
|
|
} else if len(similarUsers) > 1 {
|
|
// this shouldn't happen since there is a partial unique index on (email and is_sso_user = false)
|
|
return AccountLinkingResult{
|
|
Decision: MultipleAccounts,
|
|
Identities: linkingIdentities,
|
|
LinkingDomain: candidateLinkingDomain,
|
|
CandidateEmail: candidateEmail,
|
|
}, nil
|
|
} else {
|
|
// there are no identities in the linking domain, we have to
|
|
// create a new identity and new user
|
|
return AccountLinkingResult{
|
|
Decision: CreateAccount,
|
|
LinkingDomain: candidateLinkingDomain,
|
|
CandidateEmail: candidateEmail,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// there is at least one identity in the linking domain let's do a
|
|
// sanity check to see if all of the identities in the domain share the
|
|
// same user ID
|
|
linkingUserId := linkingIdentities[0].UserID
|
|
for _, identity := range linkingIdentities {
|
|
if identity.UserID != linkingUserId {
|
|
// ok this linking domain has more than one user account
|
|
// caller should decide what to do
|
|
|
|
return AccountLinkingResult{
|
|
Decision: MultipleAccounts,
|
|
Identities: linkingIdentities,
|
|
LinkingDomain: candidateLinkingDomain,
|
|
CandidateEmail: candidateEmail,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// there's only one user ID in this linking domain, we can go on and
|
|
// create a new identity and link it to the existing account
|
|
|
|
var user *User
|
|
var terr error
|
|
|
|
if user, terr = FindUserByID(tx, linkingUserId); terr != nil {
|
|
return AccountLinkingResult{}, terr
|
|
}
|
|
|
|
return AccountLinkingResult{
|
|
Decision: LinkAccount,
|
|
User: user,
|
|
Identities: linkingIdentities,
|
|
LinkingDomain: candidateLinkingDomain,
|
|
CandidateEmail: candidateEmail,
|
|
}, nil
|
|
}
|