mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-08 12:47:39 +00:00
f20539ef8f
# Which Problems Are Solved During authentication in the login UI, there is a check if the user's MFA is already checked or needs to be setup. In cases where the user was just set up or especially, if the user was just federated without a verified email address, this can lead to the problem, where OTP Email cannot be setup as there's no verified email address. # How the Problems Are Solved - Added a check if there's no verified email address on the user and require a mail verification check before checking for MFA. Note: that if the user had a verified email address, but changed it and has not verified it, they will still be prompted with an MFA check before the email verification. This is make sure, we don't break the existing behavior and the user's authentication is properly checked. # Additional Changes None # Additional Context - closes https://github.com/zitadel/zitadel/issues/9035
311 lines
7.5 KiB
Go
311 lines
7.5 KiB
Go
package model
|
|
|
|
import (
|
|
"time"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
type UserView struct {
|
|
ID string
|
|
UserName string
|
|
CreationDate time.Time
|
|
ChangeDate time.Time
|
|
State UserState
|
|
Sequence uint64
|
|
ResourceOwner string
|
|
LastLogin time.Time
|
|
PreferredLoginName string
|
|
LoginNames []string
|
|
*MachineView
|
|
*HumanView
|
|
}
|
|
|
|
type HumanView struct {
|
|
PasswordSet bool
|
|
PasswordInitRequired bool
|
|
PasswordChangeRequired bool
|
|
UsernameChangeRequired bool
|
|
PasswordChanged time.Time
|
|
FirstName string
|
|
LastName string
|
|
NickName string
|
|
DisplayName string
|
|
AvatarKey string
|
|
PreferredLanguage string
|
|
Gender Gender
|
|
Email string
|
|
IsEmailVerified bool
|
|
VerifiedEmail string
|
|
Phone string
|
|
IsPhoneVerified bool
|
|
Country string
|
|
Locality string
|
|
PostalCode string
|
|
Region string
|
|
StreetAddress string
|
|
OTPState MFAState
|
|
OTPSMSAdded bool
|
|
OTPEmailAdded bool
|
|
U2FTokens []*WebAuthNView
|
|
PasswordlessTokens []*WebAuthNView
|
|
MFAMaxSetUp domain.MFALevel
|
|
MFAInitSkipped time.Time
|
|
InitRequired bool
|
|
PasswordlessInitRequired bool
|
|
}
|
|
|
|
type WebAuthNView struct {
|
|
TokenID string
|
|
Name string
|
|
State MFAState
|
|
}
|
|
|
|
type MachineView struct {
|
|
LastKeyAdded time.Time
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
type UserSearchRequest struct {
|
|
Offset uint64
|
|
Limit uint64
|
|
SortingColumn UserSearchKey
|
|
Asc bool
|
|
Queries []*UserSearchQuery
|
|
}
|
|
|
|
type UserSearchKey int32
|
|
|
|
const (
|
|
UserSearchKeyUnspecified UserSearchKey = iota
|
|
UserSearchKeyUserID
|
|
UserSearchKeyUserName
|
|
UserSearchKeyFirstName
|
|
UserSearchKeyLastName
|
|
UserSearchKeyNickName
|
|
UserSearchKeyDisplayName
|
|
UserSearchKeyEmail
|
|
UserSearchKeyState
|
|
UserSearchKeyResourceOwner
|
|
UserSearchKeyLoginNames
|
|
UserSearchKeyType
|
|
UserSearchKeyPreferredLoginName
|
|
UserSearchKeyInstanceID
|
|
UserSearchOwnerRemoved
|
|
)
|
|
|
|
type UserSearchQuery struct {
|
|
Key UserSearchKey
|
|
Method domain.SearchMethod
|
|
Value interface{}
|
|
}
|
|
|
|
type UserSearchResponse struct {
|
|
Offset uint64
|
|
Limit uint64
|
|
TotalResult uint64
|
|
Result []*UserView
|
|
Sequence uint64
|
|
Timestamp time.Time
|
|
}
|
|
|
|
type UserState int32
|
|
|
|
const (
|
|
UserStateUnspecified UserState = iota
|
|
UserStateActive
|
|
UserStateInactive
|
|
UserStateDeleted
|
|
UserStateLocked
|
|
UserStateSuspend
|
|
UserStateInitial
|
|
)
|
|
|
|
type Gender int32
|
|
|
|
const (
|
|
GenderUnspecified Gender = iota
|
|
GenderFemale
|
|
GenderMale
|
|
GenderDiverse
|
|
)
|
|
|
|
func (r *UserSearchRequest) EnsureLimit(limit uint64) error {
|
|
if r.Limit > limit {
|
|
return zerrors.ThrowInvalidArgument(nil, "SEARCH-zz62F", "Errors.Limit.ExceedsDefault")
|
|
}
|
|
if r.Limit == 0 {
|
|
r.Limit = limit
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *UserSearchRequest) AppendMyOrgQuery(orgID string) {
|
|
r.Queries = append(r.Queries, &UserSearchQuery{Key: UserSearchKeyResourceOwner, Method: domain.SearchMethodEquals, Value: orgID})
|
|
}
|
|
|
|
func (u *UserView) MFATypesSetupPossible(level domain.MFALevel, policy *domain.LoginPolicy) []domain.MFAType {
|
|
types := make([]domain.MFAType, 0)
|
|
switch level {
|
|
default:
|
|
fallthrough
|
|
case domain.MFALevelSecondFactor:
|
|
if policy.HasSecondFactors() {
|
|
for _, mfaType := range policy.SecondFactors {
|
|
switch mfaType {
|
|
case domain.SecondFactorTypeTOTP:
|
|
if u.OTPState != MFAStateReady {
|
|
types = append(types, domain.MFATypeTOTP)
|
|
}
|
|
case domain.SecondFactorTypeU2F:
|
|
types = append(types, domain.MFATypeU2F)
|
|
case domain.SecondFactorTypeOTPSMS:
|
|
if !u.OTPSMSAdded {
|
|
types = append(types, domain.MFATypeOTPSMS)
|
|
}
|
|
case domain.SecondFactorTypeOTPEmail:
|
|
if !u.OTPEmailAdded {
|
|
types = append(types, domain.MFATypeOTPEmail)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return types
|
|
}
|
|
|
|
func (u *UserView) MFATypesAllowed(level domain.MFALevel, policy *domain.LoginPolicy, isInternalAuthentication bool) ([]domain.MFAType, bool) {
|
|
types := make([]domain.MFAType, 0)
|
|
required := true
|
|
switch level {
|
|
default:
|
|
required = domain.RequiresMFA(policy.ForceMFA, policy.ForceMFALocalOnly, isInternalAuthentication)
|
|
fallthrough
|
|
case domain.MFALevelSecondFactor:
|
|
if policy.HasSecondFactors() {
|
|
for _, mfaType := range policy.SecondFactors {
|
|
switch mfaType {
|
|
case domain.SecondFactorTypeTOTP:
|
|
if u.OTPState == MFAStateReady {
|
|
types = append(types, domain.MFATypeTOTP)
|
|
}
|
|
case domain.SecondFactorTypeU2F:
|
|
if u.IsU2FReady() {
|
|
types = append(types, domain.MFATypeU2F)
|
|
}
|
|
case domain.SecondFactorTypeOTPSMS:
|
|
if u.OTPSMSAdded {
|
|
types = append(types, domain.MFATypeOTPSMS)
|
|
}
|
|
case domain.SecondFactorTypeOTPEmail:
|
|
if u.OTPEmailAdded {
|
|
types = append(types, domain.MFATypeOTPEmail)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return types, required
|
|
}
|
|
|
|
func (u *UserView) IsU2FReady() bool {
|
|
for _, token := range u.U2FTokens {
|
|
if token.State == MFAStateReady {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (u *UserView) IsPasswordlessReady() bool {
|
|
for _, token := range u.PasswordlessTokens {
|
|
if token.State == MFAStateReady {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (u *UserView) GetProfile() (*Profile, error) {
|
|
if u.HumanView == nil {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "MODEL-WLTce", "Errors.User.NotHuman")
|
|
}
|
|
return &Profile{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: u.ID,
|
|
Sequence: u.Sequence,
|
|
ResourceOwner: u.ResourceOwner,
|
|
CreationDate: u.CreationDate,
|
|
ChangeDate: u.ChangeDate,
|
|
},
|
|
FirstName: u.FirstName,
|
|
LastName: u.LastName,
|
|
NickName: u.NickName,
|
|
DisplayName: u.DisplayName,
|
|
PreferredLanguage: language.Make(u.PreferredLanguage),
|
|
Gender: u.Gender,
|
|
PreferredLoginName: u.PreferredLoginName,
|
|
LoginNames: u.LoginNames,
|
|
AvatarKey: u.AvatarKey,
|
|
}, nil
|
|
}
|
|
|
|
func (u *UserView) GetPhone() (*Phone, error) {
|
|
if u.HumanView == nil {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "MODEL-him4a", "Errors.User.NotHuman")
|
|
}
|
|
return &Phone{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: u.ID,
|
|
Sequence: u.Sequence,
|
|
ResourceOwner: u.ResourceOwner,
|
|
CreationDate: u.CreationDate,
|
|
ChangeDate: u.ChangeDate,
|
|
},
|
|
PhoneNumber: u.Phone,
|
|
IsPhoneVerified: u.IsPhoneVerified,
|
|
}, nil
|
|
}
|
|
|
|
func (u *UserView) GetEmail() (*Email, error) {
|
|
if u.HumanView == nil {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "MODEL-PWd6K", "Errors.User.NotHuman")
|
|
}
|
|
return &Email{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: u.ID,
|
|
Sequence: u.Sequence,
|
|
ResourceOwner: u.ResourceOwner,
|
|
CreationDate: u.CreationDate,
|
|
ChangeDate: u.ChangeDate,
|
|
},
|
|
EmailAddress: u.Email,
|
|
IsEmailVerified: u.IsEmailVerified,
|
|
}, nil
|
|
}
|
|
|
|
func (u *UserView) GetAddress() (*Address, error) {
|
|
if u.HumanView == nil {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "MODEL-DN61m", "Errors.User.NotHuman")
|
|
}
|
|
return &Address{
|
|
ObjectRoot: models.ObjectRoot{
|
|
AggregateID: u.ID,
|
|
Sequence: u.Sequence,
|
|
ResourceOwner: u.ResourceOwner,
|
|
CreationDate: u.CreationDate,
|
|
ChangeDate: u.ChangeDate,
|
|
},
|
|
Country: u.Country,
|
|
Locality: u.Locality,
|
|
PostalCode: u.PostalCode,
|
|
Region: u.Region,
|
|
StreetAddress: u.StreetAddress,
|
|
}, nil
|
|
}
|