mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-27 22:27:23 +08:00
329 lines
11 KiB
Go
329 lines
11 KiB
Go
package thirdparty
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gobuffalo/pop/v6"
|
|
"github.com/gofrs/uuid"
|
|
"github.com/teamhanko/hanko/backend/v2/config"
|
|
"github.com/teamhanko/hanko/backend/v2/persistence"
|
|
"github.com/teamhanko/hanko/backend/v2/persistence/models"
|
|
"github.com/teamhanko/hanko/backend/v2/webhooks/events"
|
|
)
|
|
|
|
type AccountLinkingResult struct {
|
|
Type models.AuditLogType
|
|
User *models.User
|
|
WebhookEvent *events.Event
|
|
UserCreated bool
|
|
}
|
|
|
|
func LinkAccount(tx *pop.Connection, cfg *config.Config, p persistence.Persister, userData *UserData, providerID string, isSaml bool, samlDomain *string, isFlow bool) (*AccountLinkingResult, error) {
|
|
if !isFlow {
|
|
if cfg.Email.RequireVerification && !userData.Metadata.EmailVerified {
|
|
return nil, ErrorUnverifiedProviderEmail("third party provider email must be verified")
|
|
}
|
|
}
|
|
|
|
// Validate userData
|
|
if userData == nil {
|
|
return nil, ErrorInvalidRequest("user data must be set")
|
|
}
|
|
|
|
// Ensure the email is lowercase to avoid case sensitivity issues
|
|
userData.Metadata.Email = strings.ToLower(userData.Metadata.Email)
|
|
|
|
identity, err := p.GetIdentityPersister().Get(userData.Metadata.Subject, providerID)
|
|
if err != nil {
|
|
return nil, ErrorServer("could not get identity").WithCause(err)
|
|
}
|
|
|
|
if identity == nil {
|
|
user, err := p.GetUserPersisterWithConnection(tx).GetByEmailAddress(userData.Metadata.Email)
|
|
if err != nil {
|
|
return nil, ErrorServer("could not get email").WithCause(err)
|
|
}
|
|
|
|
if user == nil {
|
|
return signUp(tx, cfg, p, userData, providerID, isSaml, samlDomain)
|
|
} else {
|
|
return link(tx, cfg, p, userData, providerID, user, isSaml, samlDomain)
|
|
}
|
|
} else {
|
|
return signIn(tx, cfg, p, userData, identity)
|
|
}
|
|
}
|
|
|
|
func link(tx *pop.Connection, cfg *config.Config, p persistence.Persister, userData *UserData, providerID string, user *models.User, isSaml bool, samlDomain *string) (*AccountLinkingResult, error) {
|
|
if !isSaml {
|
|
if strings.HasPrefix(providerID, "custom_") {
|
|
provider, ok := cfg.ThirdParty.CustomProviders[strings.TrimPrefix(providerID, "custom_")]
|
|
if !ok {
|
|
return nil, ErrorServer(fmt.Sprintf("unknown provider: %s", providerID))
|
|
}
|
|
if !provider.AllowLinking {
|
|
return nil, ErrorUserConflict("third party account linking for existing user with same email disallowed")
|
|
}
|
|
} else {
|
|
provider := cfg.ThirdParty.Providers.Get(providerID)
|
|
if provider == nil {
|
|
return nil, fmt.Errorf("unknown provider: %s", providerID)
|
|
}
|
|
|
|
if !provider.AllowLinking {
|
|
return nil, ErrorUserConflict("third party account linking for existing user with same email disallowed")
|
|
}
|
|
}
|
|
}
|
|
|
|
email := user.GetEmailByAddress(userData.Metadata.Email)
|
|
|
|
userDataMap, err := userData.ToMap()
|
|
if err != nil {
|
|
return nil, ErrorServer("could not link account").WithCause(err)
|
|
}
|
|
|
|
identity, err := models.NewIdentity(providerID, userDataMap, email.ID)
|
|
if err != nil {
|
|
return nil, ErrorServer("could not create identity").WithCause(err)
|
|
}
|
|
|
|
err = p.GetIdentityPersisterWithConnection(tx).Create(*identity)
|
|
if err != nil {
|
|
return nil, ErrorServer("could not create identity").WithCause(err)
|
|
}
|
|
|
|
if isSaml && samlDomain != nil && *samlDomain != "" {
|
|
if existingSamlIdentity := email.GetSamlIdentityForDomain(*samlDomain); existingSamlIdentity != nil {
|
|
identityToDeleteID := existingSamlIdentity.IdentityID
|
|
existingSamlIdentity.IdentityID = identity.ID
|
|
|
|
err = p.GetSamlIdentityPersisterWithConnection(tx).Update(*existingSamlIdentity)
|
|
if err != nil {
|
|
return nil, ErrorServer("could update saml identity").WithCause(err)
|
|
}
|
|
|
|
err = p.GetIdentityPersisterWithConnection(tx).Delete(models.Identity{ID: identityToDeleteID})
|
|
if err != nil {
|
|
return nil, ErrorServer("could not delete identity").WithCause(err)
|
|
}
|
|
} else {
|
|
samlIdentityID, _ := uuid.NewV4()
|
|
now := time.Now().UTC()
|
|
samlIdentity := &models.SamlIdentity{
|
|
ID: samlIdentityID,
|
|
IdentityID: identity.ID,
|
|
Domain: *samlDomain,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
err = p.GetSamlIdentityPersisterWithConnection(tx).Create(*samlIdentity)
|
|
if err != nil {
|
|
return nil, ErrorServer("could not create saml identity").WithCause(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
u, terr := p.GetUserPersisterWithConnection(tx).Get(*email.UserID)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not get user").WithCause(terr)
|
|
}
|
|
|
|
return &AccountLinkingResult{
|
|
Type: models.AuditLogThirdPartyLinkingSucceeded,
|
|
User: u,
|
|
WebhookEvent: nil,
|
|
UserCreated: false,
|
|
}, nil
|
|
}
|
|
|
|
func signIn(tx *pop.Connection, cfg *config.Config, p persistence.Persister, userData *UserData, identity *models.Identity) (*AccountLinkingResult, error) {
|
|
var linkingResult *AccountLinkingResult
|
|
var webhookEvent events.Event
|
|
|
|
userPersister := p.GetUserPersisterWithConnection(tx)
|
|
emailPersister := p.GetEmailPersisterWithConnection(tx)
|
|
identityPersister := p.GetIdentityPersisterWithConnection(tx)
|
|
|
|
var terr error
|
|
email := identity.Email
|
|
if userData.Metadata.Email != identity.Email.Address {
|
|
// The primary email address at the provider has changed, check if the new provider email already exists
|
|
email, terr = emailPersister.FindByAddress(userData.Metadata.Email)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not get email").WithCause(terr)
|
|
}
|
|
|
|
if email != nil {
|
|
if email.UserID == nil {
|
|
// The email already exists but is unassigned, claim it and associate the identity with it
|
|
email.UserID = identity.Email.UserID
|
|
email.Verified = userData.Metadata.EmailVerified
|
|
|
|
terr = emailPersister.Update(*email)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not update email").WithCause(terr)
|
|
}
|
|
|
|
identity.EmailID = email.ID
|
|
webhookEvent = events.UserUpdate
|
|
} else if email.UserID.String() != identity.Email.UserID.String() {
|
|
// The email is assigned to a different user, and so the identity is linked to multiple users. There
|
|
// is not much we can do here but return an error.
|
|
return nil, ErrorMultipleAccounts(fmt.Sprintf("cannot identify associated user: '%s' is used by multiple accounts", email.Address))
|
|
} else {
|
|
// The email is assigned to the same user. This can happen if the user creates an email with an
|
|
// address equal to the new primary provider email prior to changing the primary mail at the
|
|
// provider and then doing a sign in with the provider. We need to update the associated email in
|
|
// the identity.
|
|
identity.EmailID = email.ID
|
|
}
|
|
} else {
|
|
// The email does not exist. Create a new one and associate the identity with it.
|
|
emailCount, err := emailPersister.CountByUserId(*identity.Email.UserID)
|
|
if err != nil {
|
|
return nil, ErrorServer("failed to count user emails").WithCause(err)
|
|
}
|
|
|
|
if emailCount >= cfg.Email.Limit {
|
|
return nil, ErrorMaxNumberOfAddresses("max number of email addresses reached")
|
|
}
|
|
|
|
email = models.NewEmail(identity.Email.UserID, userData.Metadata.Email)
|
|
email.Verified = userData.Metadata.EmailVerified
|
|
terr = emailPersister.Create(*email)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not create email").WithCause(terr)
|
|
}
|
|
|
|
identity.EmailID = email.ID
|
|
webhookEvent = events.UserEmailCreate
|
|
}
|
|
}
|
|
|
|
userDataMap, err := userData.ToMap()
|
|
if err != nil {
|
|
return nil, ErrorServer("could not link account").WithCause(err)
|
|
}
|
|
|
|
identity.Data = userDataMap
|
|
terr = identityPersister.Update(*identity)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not update identity").WithCause(terr)
|
|
}
|
|
|
|
user, terr := userPersister.Get(*identity.Email.UserID)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not get user").WithCause(terr)
|
|
}
|
|
|
|
linkingResult = &AccountLinkingResult{
|
|
Type: models.AuditLogThirdPartySignInSucceeded,
|
|
User: user,
|
|
WebhookEvent: &webhookEvent,
|
|
UserCreated: false,
|
|
}
|
|
|
|
return linkingResult, nil
|
|
}
|
|
|
|
func signUp(tx *pop.Connection, cfg *config.Config, p persistence.Persister, userData *UserData, providerID string, isSaml bool, samlDomain *string) (*AccountLinkingResult, error) {
|
|
if !cfg.Account.AllowSignup {
|
|
return nil, ErrorSignUpDisabled("account signup is disabled")
|
|
}
|
|
|
|
var linkingResult *AccountLinkingResult
|
|
|
|
userPersister := p.GetUserPersisterWithConnection(tx)
|
|
emailPersister := p.GetEmailPersisterWithConnection(tx)
|
|
primaryEmailPersister := p.GetPrimaryEmailPersisterWithConnection(tx)
|
|
identityPersister := p.GetIdentityPersisterWithConnection(tx)
|
|
|
|
email, terr := emailPersister.FindByAddress(userData.Metadata.Email)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not get email").WithCause(terr)
|
|
}
|
|
|
|
user := models.NewUser()
|
|
terr = userPersister.Create(user)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not create user").WithCause(terr)
|
|
}
|
|
|
|
if email != nil && email.UserID == nil {
|
|
// There exists an email with the same address as the primary provider address, but it is not assigned
|
|
// to any user yet, hence we assign the new user ID to this email.
|
|
email.UserID = &user.ID
|
|
email.Verified = userData.Metadata.EmailVerified
|
|
terr = emailPersister.Update(*email)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not update email").WithCause(terr)
|
|
}
|
|
|
|
} else {
|
|
// No email exists, create a new one using the provider user data email
|
|
email = models.NewEmail(&user.ID, userData.Metadata.Email)
|
|
email.Verified = userData.Metadata.EmailVerified
|
|
terr = emailPersister.Create(*email)
|
|
if terr != nil {
|
|
return nil, ErrorServer("failed to store email").WithCause(terr)
|
|
}
|
|
}
|
|
|
|
primaryEmail := models.NewPrimaryEmail(email.ID, *email.UserID)
|
|
terr = primaryEmailPersister.Create(*primaryEmail)
|
|
if terr != nil {
|
|
return nil, ErrorServer("failed to store primary email").WithCause(terr)
|
|
}
|
|
|
|
userDataMap, err := userData.ToMap()
|
|
if err != nil {
|
|
return nil, ErrorServer("could not link account").WithCause(err)
|
|
}
|
|
|
|
identity, terr := models.NewIdentity(providerID, userDataMap, email.ID)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not create identity").WithCause(terr)
|
|
}
|
|
|
|
terr = identityPersister.Create(*identity)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not store identity").WithCause(terr)
|
|
}
|
|
|
|
if isSaml && samlDomain != nil && *samlDomain != "" {
|
|
samlIdentityID, _ := uuid.NewV4()
|
|
now := time.Now().UTC()
|
|
samlIdentity := &models.SamlIdentity{
|
|
ID: samlIdentityID,
|
|
IdentityID: identity.ID,
|
|
Domain: *samlDomain,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
err = p.GetSamlIdentityPersisterWithConnection(tx).Create(*samlIdentity)
|
|
if err != nil {
|
|
return nil, ErrorServer("could not store saml identity").WithCause(err)
|
|
}
|
|
}
|
|
|
|
u, terr := userPersister.Get(*email.UserID)
|
|
if terr != nil {
|
|
return nil, ErrorServer("could not get user").WithCause(terr)
|
|
}
|
|
|
|
evt := events.UserCreate
|
|
linkingResult = &AccountLinkingResult{
|
|
Type: models.AuditLogThirdPartySignUpSucceeded,
|
|
User: u,
|
|
WebhookEvent: &evt,
|
|
UserCreated: true,
|
|
}
|
|
|
|
return linkingResult, nil
|
|
}
|