990 lines
28 KiB
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)
|
|
}
|