chatdesk-ui/auth_v2.169.0/internal/models/user.go

990 lines
28 KiB
Go

package models
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/supabase/auth/internal/crypto"
"github.com/supabase/auth/internal/storage"
"golang.org/x/crypto/bcrypt"
)
// User respresents a registered user with email/password authentication
type User struct {
ID uuid.UUID `json:"id" db:"id"`
Aud string `json:"aud" db:"aud"`
Role string `json:"role" db:"role"`
Email storage.NullString `json:"email" db:"email"`
IsSSOUser bool `json:"-" db:"is_sso_user"`
EncryptedPassword *string `json:"-" db:"encrypted_password"`
EmailConfirmedAt *time.Time `json:"email_confirmed_at,omitempty" db:"email_confirmed_at"`
InvitedAt *time.Time `json:"invited_at,omitempty" db:"invited_at"`
Phone storage.NullString `json:"phone" db:"phone"`
PhoneConfirmedAt *time.Time `json:"phone_confirmed_at,omitempty" db:"phone_confirmed_at"`
ConfirmationToken string `json:"-" db:"confirmation_token"`
ConfirmationSentAt *time.Time `json:"confirmation_sent_at,omitempty" db:"confirmation_sent_at"`
// For backward compatibility only. Use EmailConfirmedAt or PhoneConfirmedAt instead.
ConfirmedAt *time.Time `json:"confirmed_at,omitempty" db:"confirmed_at" rw:"r"`
RecoveryToken string `json:"-" db:"recovery_token"`
RecoverySentAt *time.Time `json:"recovery_sent_at,omitempty" db:"recovery_sent_at"`
EmailChangeTokenCurrent string `json:"-" db:"email_change_token_current"`
EmailChangeTokenNew string `json:"-" db:"email_change_token_new"`
EmailChange string `json:"new_email,omitempty" db:"email_change"`
EmailChangeSentAt *time.Time `json:"email_change_sent_at,omitempty" db:"email_change_sent_at"`
EmailChangeConfirmStatus int `json:"-" db:"email_change_confirm_status"`
PhoneChangeToken string `json:"-" db:"phone_change_token"`
PhoneChange string `json:"new_phone,omitempty" db:"phone_change"`
PhoneChangeSentAt *time.Time `json:"phone_change_sent_at,omitempty" db:"phone_change_sent_at"`
ReauthenticationToken string `json:"-" db:"reauthentication_token"`
ReauthenticationSentAt *time.Time `json:"reauthentication_sent_at,omitempty" db:"reauthentication_sent_at"`
LastSignInAt *time.Time `json:"last_sign_in_at,omitempty" db:"last_sign_in_at"`
AppMetaData JSONMap `json:"app_metadata" db:"raw_app_meta_data"`
UserMetaData JSONMap `json:"user_metadata" db:"raw_user_meta_data"`
Factors []Factor `json:"factors,omitempty" has_many:"factors"`
Identities []Identity `json:"identities" has_many:"identities"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
BannedUntil *time.Time `json:"banned_until,omitempty" db:"banned_until"`
DeletedAt *time.Time `json:"deleted_at,omitempty" db:"deleted_at"`
IsAnonymous bool `json:"is_anonymous" db:"is_anonymous"`
DONTUSEINSTANCEID uuid.UUID `json:"-" db:"instance_id"`
}
func NewUserWithPasswordHash(phone, email, passwordHash, aud string, userData map[string]interface{}) (*User, error) {
if strings.HasPrefix(passwordHash, crypto.Argon2Prefix) {
_, err := crypto.ParseArgon2Hash(passwordHash)
if err != nil {
return nil, err
}
} else if strings.HasPrefix(passwordHash, crypto.FirebaseScryptPrefix) {
_, err := crypto.ParseFirebaseScryptHash(passwordHash)
if err != nil {
return nil, err
}
} else {
// verify that the hash is a bcrypt hash
_, err := bcrypt.Cost([]byte(passwordHash))
if err != nil {
return nil, err
}
}
id := uuid.Must(uuid.NewV4())
user := &User{
ID: id,
Aud: aud,
Email: storage.NullString(strings.ToLower(email)),
Phone: storage.NullString(phone),
UserMetaData: userData,
EncryptedPassword: &passwordHash,
}
return user, nil
}
// NewUser initializes a new user from an email, password and user data.
func NewUser(phone, email, password, aud string, userData map[string]interface{}) (*User, error) {
passwordHash := ""
if password != "" {
pw, err := crypto.GenerateFromPassword(context.Background(), password)
if err != nil {
return nil, err
}
passwordHash = pw
}
if userData == nil {
userData = make(map[string]interface{})
}
id := uuid.Must(uuid.NewV4())
user := &User{
ID: id,
Aud: aud,
Email: storage.NullString(strings.ToLower(email)),
Phone: storage.NullString(phone),
UserMetaData: userData,
EncryptedPassword: &passwordHash,
}
return user, nil
}
// TableName overrides the table name used by pop
func (User) TableName() string {
tableName := "users"
return tableName
}
func (u *User) HasPassword() bool {
var pwd string
if u.EncryptedPassword != nil {
pwd = *u.EncryptedPassword
}
return pwd != ""
}
// BeforeSave is invoked before the user is saved to the database
func (u *User) BeforeSave(tx *pop.Connection) error {
if u.EmailConfirmedAt != nil && u.EmailConfirmedAt.IsZero() {
u.EmailConfirmedAt = nil
}
if u.PhoneConfirmedAt != nil && u.PhoneConfirmedAt.IsZero() {
u.PhoneConfirmedAt = nil
}
if u.InvitedAt != nil && u.InvitedAt.IsZero() {
u.InvitedAt = nil
}
if u.ConfirmationSentAt != nil && u.ConfirmationSentAt.IsZero() {
u.ConfirmationSentAt = nil
}
if u.RecoverySentAt != nil && u.RecoverySentAt.IsZero() {
u.RecoverySentAt = nil
}
if u.EmailChangeSentAt != nil && u.EmailChangeSentAt.IsZero() {
u.EmailChangeSentAt = nil
}
if u.PhoneChangeSentAt != nil && u.PhoneChangeSentAt.IsZero() {
u.PhoneChangeSentAt = nil
}
if u.ReauthenticationSentAt != nil && u.ReauthenticationSentAt.IsZero() {
u.ReauthenticationSentAt = nil
}
if u.LastSignInAt != nil && u.LastSignInAt.IsZero() {
u.LastSignInAt = nil
}
if u.BannedUntil != nil && u.BannedUntil.IsZero() {
u.BannedUntil = nil
}
return nil
}
// IsConfirmed checks if a user has already been
// registered and confirmed.
func (u *User) IsConfirmed() bool {
return u.EmailConfirmedAt != nil
}
// HasBeenInvited checks if user has been invited
func (u *User) HasBeenInvited() bool {
return u.InvitedAt != nil
}
// IsPhoneConfirmed checks if a user's phone has already been
// registered and confirmed.
func (u *User) IsPhoneConfirmed() bool {
return u.PhoneConfirmedAt != nil
}
// SetRole sets the users Role to roleName
func (u *User) SetRole(tx *storage.Connection, roleName string) error {
u.Role = strings.TrimSpace(roleName)
return tx.UpdateOnly(u, "role")
}
// HasRole returns true when the users role is set to roleName
func (u *User) HasRole(roleName string) bool {
return u.Role == roleName
}
// GetEmail returns the user's email as a string
func (u *User) GetEmail() string {
return string(u.Email)
}
// GetPhone returns the user's phone number as a string
func (u *User) GetPhone() string {
return string(u.Phone)
}
// UpdateUserMetaData sets all user data from a map of updates,
// ensuring that it doesn't override attributes that are not
// in the provided map.
func (u *User) UpdateUserMetaData(tx *storage.Connection, updates map[string]interface{}) error {
if u.UserMetaData == nil {
u.UserMetaData = updates
} else {
for key, value := range updates {
if value != nil {
u.UserMetaData[key] = value
} else {
delete(u.UserMetaData, key)
}
}
}
return tx.UpdateOnly(u, "raw_user_meta_data")
}
// UpdateAppMetaData updates all app data from a map of updates
func (u *User) UpdateAppMetaData(tx *storage.Connection, updates map[string]interface{}) error {
if u.AppMetaData == nil {
u.AppMetaData = updates
} else {
for key, value := range updates {
if value != nil {
u.AppMetaData[key] = value
} else {
delete(u.AppMetaData, key)
}
}
}
return tx.UpdateOnly(u, "raw_app_meta_data")
}
// UpdateAppMetaDataProviders updates the provider field in AppMetaData column
func (u *User) UpdateAppMetaDataProviders(tx *storage.Connection) error {
providers, terr := FindProvidersByUser(tx, u)
if terr != nil {
return terr
}
payload := map[string]interface{}{
"providers": providers,
}
if len(providers) > 0 {
payload["provider"] = providers[0]
}
return u.UpdateAppMetaData(tx, payload)
}
// UpdateUserEmail updates the user's email to one of the identity's email
// if the current email used doesn't match any of the identities email
func (u *User) UpdateUserEmailFromIdentities(tx *storage.Connection) error {
identities, terr := FindIdentitiesByUserID(tx, u.ID)
if terr != nil {
return terr
}
for _, i := range identities {
if u.GetEmail() == i.GetEmail() {
// there's an existing identity that uses the same email
// so the user's email can be kept
return nil
}
}
var primaryIdentity *Identity
for _, i := range identities {
if _, terr := FindUserByEmailAndAudience(tx, i.GetEmail(), u.Aud); terr != nil {
if IsNotFoundError(terr) {
// the identity's email is not used by another user
// so we can set it as the primary identity
primaryIdentity = i
break
}
return terr
}
}
if primaryIdentity == nil {
return UserEmailUniqueConflictError{}
}
// default to the first identity's email
if terr := u.SetEmail(tx, primaryIdentity.GetEmail()); terr != nil {
return terr
}
if primaryIdentity.GetEmail() == "" {
u.EmailConfirmedAt = nil
if terr := tx.UpdateOnly(u, "email_confirmed_at"); terr != nil {
return terr
}
}
return nil
}
// SetEmail sets the user's email
func (u *User) SetEmail(tx *storage.Connection, email string) error {
u.Email = storage.NullString(email)
return tx.UpdateOnly(u, "email")
}
// SetPhone sets the user's phone
func (u *User) SetPhone(tx *storage.Connection, phone string) error {
u.Phone = storage.NullString(phone)
return tx.UpdateOnly(u, "phone")
}
func (u *User) SetPassword(ctx context.Context, password string, encrypt bool, encryptionKeyID, encryptionKey string) error {
if password == "" {
u.EncryptedPassword = nil
return nil
}
pw, err := crypto.GenerateFromPassword(ctx, password)
if err != nil {
return err
}
u.EncryptedPassword = &pw
if encrypt {
es, err := crypto.NewEncryptedString(u.ID.String(), []byte(pw), encryptionKeyID, encryptionKey)
if err != nil {
return err
}
encryptedPassword := es.String()
u.EncryptedPassword = &encryptedPassword
}
return nil
}
// UpdatePassword updates the user's password. Use SetPassword outside of a transaction first!
func (u *User) UpdatePassword(tx *storage.Connection, sessionID *uuid.UUID) error {
// These need to be reset because password change may mean the user no longer trusts the actions performed by the previous password.
u.ConfirmationToken = ""
u.ConfirmationSentAt = nil
u.RecoveryToken = ""
u.RecoverySentAt = nil
u.EmailChangeTokenCurrent = ""
u.EmailChangeTokenNew = ""
u.EmailChangeSentAt = nil
u.PhoneChangeToken = ""
u.PhoneChangeSentAt = nil
u.ReauthenticationToken = ""
u.ReauthenticationSentAt = nil
if err := tx.UpdateOnly(u, "encrypted_password", "confirmation_token", "confirmation_sent_at", "recovery_token", "recovery_sent_at", "email_change_token_current", "email_change_token_new", "email_change_sent_at", "phone_change_token", "phone_change_sent_at", "reauthentication_token", "reauthentication_sent_at"); err != nil {
return err
}
if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
return err
}
if sessionID == nil {
// log out user from all sessions to ensure reauthentication after password change
return Logout(tx, u.ID)
} else {
// log out user from all other sessions to ensure reauthentication after password change
return LogoutAllExceptMe(tx, *sessionID, u.ID)
}
}
// Authenticate a user from a password
func (u *User) Authenticate(ctx context.Context, tx *storage.Connection, password string, decryptionKeys map[string]string, encrypt bool, encryptionKeyID string) (bool, bool, error) {
if u.EncryptedPassword == nil {
return false, false, nil
}
hash := *u.EncryptedPassword
if hash == "" {
return false, false, nil
}
es := crypto.ParseEncryptedString(hash)
if es != nil {
h, err := es.Decrypt(u.ID.String(), decryptionKeys)
if err != nil {
return false, false, err
}
hash = string(h)
}
compareErr := crypto.CompareHashAndPassword(ctx, hash, password)
if !strings.HasPrefix(hash, crypto.Argon2Prefix) && !strings.HasPrefix(hash, crypto.FirebaseScryptPrefix) {
// check if cost exceeds default cost or is too low
cost, err := bcrypt.Cost([]byte(hash))
if err != nil {
return compareErr == nil, false, err
}
if cost > bcrypt.DefaultCost || cost == bcrypt.MinCost {
// don't bother with encrypting the password in Authenticate
// since it's handled separately
if err := u.SetPassword(ctx, password, false, "", ""); err != nil {
return compareErr == nil, false, err
}
}
}
return compareErr == nil, encrypt && (es == nil || es.ShouldReEncrypt(encryptionKeyID)), nil
}
// ConfirmReauthentication resets the reauthentication token
func (u *User) ConfirmReauthentication(tx *storage.Connection) error {
u.ReauthenticationToken = ""
if err := tx.UpdateOnly(u, "reauthentication_token"); err != nil {
return err
}
if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
return err
}
return nil
}
// Confirm resets the confimation token and sets the confirm timestamp
func (u *User) Confirm(tx *storage.Connection) error {
u.ConfirmationToken = ""
now := time.Now()
u.EmailConfirmedAt = &now
if err := tx.UpdateOnly(u, "confirmation_token", "email_confirmed_at"); err != nil {
return err
}
if err := u.UpdateUserMetaData(tx, map[string]interface{}{
"email_verified": true,
}); err != nil {
return err
}
if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
return err
}
return nil
}
// ConfirmPhone resets the confimation token and sets the confirm timestamp
func (u *User) ConfirmPhone(tx *storage.Connection) error {
u.ConfirmationToken = ""
now := time.Now()
u.PhoneConfirmedAt = &now
if err := tx.UpdateOnly(u, "confirmation_token", "phone_confirmed_at"); err != nil {
return nil
}
return ClearAllOneTimeTokensForUser(tx, u.ID)
}
// UpdateLastSignInAt update field last_sign_in_at for user according to specified field
func (u *User) UpdateLastSignInAt(tx *storage.Connection) error {
return tx.UpdateOnly(u, "last_sign_in_at")
}
// ConfirmEmailChange confirm the change of email for a user
func (u *User) ConfirmEmailChange(tx *storage.Connection, status int) error {
email := u.EmailChange
u.Email = storage.NullString(email)
u.EmailChange = ""
u.EmailChangeTokenCurrent = ""
u.EmailChangeTokenNew = ""
u.EmailChangeConfirmStatus = status
if err := tx.UpdateOnly(
u,
"email",
"email_change",
"email_change_token_current",
"email_change_token_new",
"email_change_confirm_status",
); err != nil {
return err
}
if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
return err
}
if !u.IsConfirmed() {
if err := u.Confirm(tx); err != nil {
return err
}
}
identity, err := FindIdentityByIdAndProvider(tx, u.ID.String(), "email")
if err != nil {
if IsNotFoundError(err) {
// no email identity, not an error
return nil
}
return err
}
if _, ok := identity.IdentityData["email"]; ok {
identity.IdentityData["email"] = email
if err := tx.UpdateOnly(identity, "identity_data"); err != nil {
return err
}
}
return nil
}
// ConfirmPhoneChange confirms the change of phone for a user
func (u *User) ConfirmPhoneChange(tx *storage.Connection) error {
now := time.Now()
phone := u.PhoneChange
u.Phone = storage.NullString(phone)
u.PhoneChange = ""
u.PhoneChangeToken = ""
u.PhoneConfirmedAt = &now
if err := tx.UpdateOnly(
u,
"phone",
"phone_change",
"phone_change_token",
"phone_confirmed_at",
); err != nil {
return err
}
if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
return err
}
identity, err := FindIdentityByIdAndProvider(tx, u.ID.String(), "phone")
if err != nil {
if IsNotFoundError(err) {
// no phone identity, not an error
return nil
}
return err
}
if _, ok := identity.IdentityData["phone"]; ok {
identity.IdentityData["phone"] = phone
}
if err := tx.UpdateOnly(identity, "identity_data"); err != nil {
return err
}
return nil
}
// Recover resets the recovery token
func (u *User) Recover(tx *storage.Connection) error {
u.RecoveryToken = ""
if err := tx.UpdateOnly(u, "recovery_token"); err != nil {
return err
}
return ClearAllOneTimeTokensForUser(tx, u.ID)
}
// CountOtherUsers counts how many other users exist besides the one provided
func CountOtherUsers(tx *storage.Connection, id uuid.UUID) (int, error) {
userCount, err := tx.Q().Where("instance_id = ? and id != ?", uuid.Nil, id).Count(&User{})
return userCount, errors.Wrap(err, "error finding registered users")
}
func findUser(tx *storage.Connection, query string, args ...interface{}) (*User, error) {
obj := &User{}
if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, UserNotFoundError{}
}
return nil, errors.Wrap(err, "error finding user")
}
return obj, nil
}
// FindUserByEmailAndAudience finds a user with the matching email and audience.
func FindUserByEmailAndAudience(tx *storage.Connection, email, aud string) (*User, error) {
return findUser(tx, "instance_id = ? and LOWER(email) = ? and aud = ? and is_sso_user = false", uuid.Nil, strings.ToLower(email), aud)
}
// FindUserByPhoneAndAudience finds a user with the matching email and audience.
func FindUserByPhoneAndAudience(tx *storage.Connection, phone, aud string) (*User, error) {
return findUser(tx, "instance_id = ? and phone = ? and aud = ? and is_sso_user = false", uuid.Nil, phone, aud)
}
// FindUserByID finds a user matching the provided ID.
func FindUserByID(tx *storage.Connection, id uuid.UUID) (*User, error) {
return findUser(tx, "instance_id = ? and id = ?", uuid.Nil, id)
}
// FindUserWithRefreshToken finds a user from the provided refresh token. If
// forUpdate is set to true, then the SELECT statement used by the query has
// the form SELECT ... FOR UPDATE SKIP LOCKED. This means that a FOR UPDATE
// lock will only be acquired if there's no other lock. In case there is a
// lock, a IsNotFound(err) error will be returned.
func FindUserWithRefreshToken(tx *storage.Connection, token string, forUpdate bool) (*User, *RefreshToken, *Session, error) {
refreshToken := &RefreshToken{}
if forUpdate {
// pop does not provide us with a way to execute FOR UPDATE
// queries which lock the rows affected by the query from
// being accessed by any other transaction that also uses FOR
// UPDATE
if err := tx.RawQuery(fmt.Sprintf("SELECT * FROM %q WHERE token = ? LIMIT 1 FOR UPDATE SKIP LOCKED;", refreshToken.TableName()), token).First(refreshToken); err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil, nil, RefreshTokenNotFoundError{}
}
return nil, nil, nil, errors.Wrap(err, "error finding refresh token for update")
}
}
// once the rows are locked (if forUpdate was true), we can query again using pop
if err := tx.Where("token = ?", token).First(refreshToken); err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil, nil, RefreshTokenNotFoundError{}
}
return nil, nil, nil, errors.Wrap(err, "error finding refresh token")
}
user, err := FindUserByID(tx, refreshToken.UserID)
if err != nil {
return nil, nil, nil, err
}
var session *Session
if refreshToken.SessionId != nil {
sessionId := *refreshToken.SessionId
if sessionId != uuid.Nil {
session, err = FindSessionByID(tx, sessionId, forUpdate)
if err != nil {
if forUpdate {
return nil, nil, nil, err
}
if !IsNotFoundError(err) {
return nil, nil, nil, errors.Wrap(err, "error finding session from refresh token")
}
// otherwise, there's no session for this refresh token
}
}
}
return user, refreshToken, session, nil
}
// FindUsersInAudience finds users with the matching audience.
func FindUsersInAudience(tx *storage.Connection, aud string, pageParams *Pagination, sortParams *SortParams, filter string) ([]*User, error) {
users := []*User{}
q := tx.Q().Where("instance_id = ? and aud = ?", uuid.Nil, aud)
if filter != "" {
lf := "%" + filter + "%"
// we must specify the collation in order to get case insensitive search for the JSON column
q = q.Where("(email LIKE ? OR raw_user_meta_data->>'full_name' ILIKE ?)", lf, lf)
}
if sortParams != nil && len(sortParams.Fields) > 0 {
for _, field := range sortParams.Fields {
q = q.Order(field.Name + " " + string(field.Dir))
}
}
var err error
if pageParams != nil {
err = q.Paginate(int(pageParams.Page), int(pageParams.PerPage)).All(&users) // #nosec G115
pageParams.Count = uint64(q.Paginator.TotalEntriesSize) // #nosec G115
} else {
err = q.All(&users)
}
return users, err
}
// IsDuplicatedEmail returns whether a user exists with a matching email and audience.
// If a currentUser is provided, we will need to filter out any identities that belong to the current user.
func IsDuplicatedEmail(tx *storage.Connection, email, aud string, currentUser *User) (*User, error) {
var identities []Identity
if err := tx.Eager().Q().Where("email = ?", strings.ToLower(email)).All(&identities); err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, nil
}
return nil, errors.Wrap(err, "unable to find identity by email for duplicates")
}
userIDs := make(map[string]uuid.UUID)
for _, identity := range identities {
if _, ok := userIDs[identity.UserID.String()]; !ok {
if !identity.IsForSSOProvider() {
userIDs[identity.UserID.String()] = identity.UserID
}
}
}
var currentUserId uuid.UUID
if currentUser != nil {
currentUserId = currentUser.ID
}
for _, userID := range userIDs {
if userID != currentUserId {
user, err := FindUserByID(tx, userID)
if err != nil {
return nil, errors.Wrap(err, "unable to find user from email identity for duplicates")
}
if user.Aud == aud {
return user, nil
}
}
}
// out of an abundance of caution, if nothing was found via the
// identities table we also do a final check on the users table
user, err := FindUserByEmailAndAudience(tx, email, aud)
if err != nil && !IsNotFoundError(err) {
return nil, errors.Wrap(err, "unable to find user email address for duplicates")
}
return user, nil
}
// IsDuplicatedPhone checks if the phone number already exists in the users table
func IsDuplicatedPhone(tx *storage.Connection, phone, aud string) (bool, error) {
_, err := FindUserByPhoneAndAudience(tx, phone, aud)
if err != nil {
if IsNotFoundError(err) {
return false, nil
}
return false, err
}
return true, nil
}
// Ban a user for a given duration.
func (u *User) Ban(tx *storage.Connection, duration time.Duration) error {
if duration == time.Duration(0) {
u.BannedUntil = nil
} else {
t := time.Now().Add(duration)
u.BannedUntil = &t
}
return tx.UpdateOnly(u, "banned_until")
}
// IsBanned checks if a user is banned or not
func (u *User) IsBanned() bool {
if u.BannedUntil == nil {
return false
}
return time.Now().Before(*u.BannedUntil)
}
func (u *User) HasMFAEnabled() bool {
for _, factor := range u.Factors {
if factor.IsVerified() {
return true
}
}
return false
}
func (u *User) UpdateBannedUntil(tx *storage.Connection) error {
return tx.UpdateOnly(u, "banned_until")
}
// RemoveUnconfirmedIdentities removes potentially malicious unconfirmed identities from a user (if any)
func (u *User) RemoveUnconfirmedIdentities(tx *storage.Connection, identity *Identity) error {
if identity.Provider != "email" && identity.Provider != "phone" {
// user is unconfirmed so the password should be reset
u.EncryptedPassword = nil
if terr := tx.UpdateOnly(u, "encrypted_password"); terr != nil {
return terr
}
}
// user is unconfirmed so existing user_metadata should be overwritten
// to use the current identity metadata
u.UserMetaData = identity.IdentityData
if terr := u.UpdateUserMetaData(tx, u.UserMetaData); terr != nil {
return terr
}
// finally, remove all identities except the current identity being authenticated
for i := range u.Identities {
if u.Identities[i].ID != identity.ID {
if terr := tx.Destroy(&u.Identities[i]); terr != nil {
return terr
}
}
}
// user is unconfirmed so none of the providers associated to it are verified yet
// only the current provider should be kept
if terr := u.UpdateAppMetaDataProviders(tx); terr != nil {
return terr
}
return nil
}
// SoftDeleteUser performs a soft deletion on the user by obfuscating and clearing certain fields
func (u *User) SoftDeleteUser(tx *storage.Connection) error {
u.Email = storage.NullString(obfuscateEmail(u, u.GetEmail()))
u.Phone = storage.NullString(obfuscatePhone(u, u.GetPhone()))
u.EmailChange = obfuscateEmail(u, u.EmailChange)
u.PhoneChange = obfuscatePhone(u, u.PhoneChange)
u.EncryptedPassword = nil
u.ConfirmationToken = ""
u.RecoveryToken = ""
u.EmailChangeTokenCurrent = ""
u.EmailChangeTokenNew = ""
u.PhoneChangeToken = ""
// set deleted_at time
now := time.Now()
u.DeletedAt = &now
if err := tx.UpdateOnly(
u,
"email",
"phone",
"encrypted_password",
"email_change",
"phone_change",
"confirmation_token",
"recovery_token",
"email_change_token_current",
"email_change_token_new",
"phone_change_token",
"deleted_at",
); err != nil {
return err
}
if err := ClearAllOneTimeTokensForUser(tx, u.ID); err != nil {
return err
}
// set raw_user_meta_data to {}
userMetaDataUpdates := map[string]interface{}{}
for k := range u.UserMetaData {
userMetaDataUpdates[k] = nil
}
if err := u.UpdateUserMetaData(tx, userMetaDataUpdates); err != nil {
return err
}
// set raw_app_meta_data to {}
appMetaDataUpdates := map[string]interface{}{}
for k := range u.AppMetaData {
appMetaDataUpdates[k] = nil
}
if err := u.UpdateAppMetaData(tx, appMetaDataUpdates); err != nil {
return err
}
if err := Logout(tx, u.ID); err != nil {
return err
}
return nil
}
// SoftDeleteUserIdentities performs a soft deletion on all identities associated to a user
func (u *User) SoftDeleteUserIdentities(tx *storage.Connection) error {
identities, err := FindIdentitiesByUserID(tx, u.ID)
if err != nil {
return err
}
// set identity_data to {}
for _, identity := range identities {
identityDataUpdates := map[string]interface{}{}
for k := range identity.IdentityData {
identityDataUpdates[k] = nil
}
if err := identity.UpdateIdentityData(tx, identityDataUpdates); err != nil {
return err
}
// updating the identity.ID has to happen last since the primary key is on (provider, id)
// we use RawQuery here instead of UpdateOnly because UpdateOnly relies on the primary key of Identity
if err := tx.RawQuery(
"update "+
(&pop.Model{Value: Identity{}}).TableName()+
" set provider_id = ? where id = ?",
obfuscateIdentityProviderId(identity),
identity.ID,
).Exec(); err != nil {
return err
}
}
return nil
}
func (u *User) FindOwnedFactorByID(tx *storage.Connection, factorID uuid.UUID) (*Factor, error) {
var factor Factor
err := tx.Where("user_id = ? AND id = ?", u.ID, factorID).First(&factor)
if err != nil {
if errors.Cause(err) == sql.ErrNoRows {
return nil, &FactorNotFoundError{}
}
return nil, err
}
return &factor, nil
}
func (user *User) WebAuthnID() []byte {
return []byte(user.ID.String())
}
func (user *User) WebAuthnName() string {
return user.Email.String()
}
func (user *User) WebAuthnDisplayName() string {
return user.Email.String()
}
func (user *User) WebAuthnCredentials() []webauthn.Credential {
var credentials []webauthn.Credential
for _, factor := range user.Factors {
if factor.IsVerified() && factor.FactorType == WebAuthn {
credential := factor.WebAuthnCredential.Credential
credentials = append(credentials, credential)
}
}
return credentials
}
func obfuscateValue(id uuid.UUID, value string) string {
hash := sha256.Sum256([]byte(id.String() + value))
return base64.RawURLEncoding.EncodeToString(hash[:])
}
func obfuscateEmail(u *User, email string) string {
return obfuscateValue(u.ID, email)
}
func obfuscatePhone(u *User, phone string) string {
// Field converted from VARCHAR(15) to text
return obfuscateValue(u.ID, phone)[:15]
}
func obfuscateIdentityProviderId(identity *Identity) string {
return obfuscateValue(identity.UserID, identity.Provider+":"+identity.ProviderID)
}
// FindUserByPhoneChangeAndAudience finds a user with the matching phone change and audience.
func FindUserByPhoneChangeAndAudience(tx *storage.Connection, phone, aud string) (*User, error) {
return findUser(tx, "instance_id = ? and phone_change = ? and aud = ? and is_sso_user = false", uuid.Nil, phone, aud)
}