mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-15 04:18:01 +00:00
ec222a13d7
# Which Problems Are Solved As already mentioned and (partially) fixed in #7992 we discovered, issues with v2 tokens that where obtained through an IDP, with passwordless authentication or with password authentication (wihtout any 2FA set up) using the v1 login for zitadel API calls - (Previous) authentication through an IdP is now correctly treated as auth method in case of a reauth even when the user is not redirected to the IdP - There were some cases where passwordless authentication was successfully checked but not correctly set as auth method, which denied access to ZITADEL API - Users with password and passwordless, but no 2FA set up which authenticate just wich password can access the ZITADEL API again Additionally while testing we found out that because of #7969 the login UI could completely break / block with the following error: `sql: Scan error on column index 3, name "state": converting NULL to int32 is unsupported (Internal)` # How the Problems Are Solved - IdP checks are treated the same way as other factors and it's ensured that a succeeded check within the configured timeframe will always provide the idp auth method - `MFATypesAllowed` checks for possible passwordless authentication - As with the v1 login, the token check now only requires MFA if the policy is set or the user has 2FA set up - UserAuthMethodsRequirements now always uses the correctly policy to check for MFA enforcement - `State` column is handled as nullable and additional events set the state to active (as before #7969) # Additional Changes - Console now also checks for 403 (mfa required) errors (e.g. after setting up the first 2FA in console) and redirects the user to the login UI (with the current id_token as id_token_hint) - Possible duplicates in auth methods / AMRs are removed now as well. # Additional Context - Bugs were introduced in #7822 and # and 7969 and only part of a pre-release. - partially already fixed with #7992 - Reported internally.
259 lines
11 KiB
Go
259 lines
11 KiB
Go
package model
|
|
|
|
import (
|
|
"database/sql"
|
|
"time"
|
|
|
|
"github.com/zitadel/logging"
|
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/repository/user"
|
|
"github.com/zitadel/zitadel/internal/user/model"
|
|
es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
const (
|
|
UserSessionKeyUserAgentID = "user_agent_id"
|
|
UserSessionKeyUserID = "user_id"
|
|
UserSessionKeyState = "state"
|
|
UserSessionKeyResourceOwner = "resource_owner"
|
|
UserSessionKeyInstanceID = "instance_id"
|
|
UserSessionKeyOwnerRemoved = "owner_removed"
|
|
UserSessionKeyCreationDate = "creation_date"
|
|
UserSessionKeyChangeDate = "change_date"
|
|
UserSessionKeySequence = "sequence"
|
|
UserSessionKeyPasswordVerification = "password_verification"
|
|
UserSessionKeySecondFactorVerification = "second_factor_verification"
|
|
UserSessionKeySecondFactorVerificationType = "second_factor_verification_type"
|
|
UserSessionKeyMultiFactorVerification = "multi_factor_verification"
|
|
UserSessionKeyMultiFactorVerificationType = "multi_factor_verification_type"
|
|
UserSessionKeyPasswordlessVerification = "passwordless_verification"
|
|
UserSessionKeyExternalLoginVerification = "external_login_verification"
|
|
UserSessionKeySelectedIDPConfigID = "selected_idp_config_id"
|
|
)
|
|
|
|
type UserSessionView struct {
|
|
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
|
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
|
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
|
State sql.Null[domain.UserSessionState] `json:"-" gorm:"column:state"`
|
|
UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id;primary_key"`
|
|
UserID string `json:"userID" gorm:"column:user_id;primary_key"`
|
|
// As of https://github.com/zitadel/zitadel/pull/7199 the following 4 attributes
|
|
// are not projected in the user session handler anymore
|
|
// and are therefore annotated with a `gorm:"-"`.
|
|
// They will be read from the corresponding projection directly.
|
|
UserName sql.NullString `json:"-" gorm:"-"`
|
|
LoginName sql.NullString `json:"-" gorm:"-"`
|
|
DisplayName sql.NullString `json:"-" gorm:"-"`
|
|
AvatarKey sql.NullString `json:"-" gorm:"-"`
|
|
SelectedIDPConfigID sql.NullString `json:"selectedIDPConfigID" gorm:"column:selected_idp_config_id"`
|
|
PasswordVerification sql.NullTime `json:"-" gorm:"column:password_verification"`
|
|
PasswordlessVerification sql.NullTime `json:"-" gorm:"column:passwordless_verification"`
|
|
ExternalLoginVerification sql.NullTime `json:"-" gorm:"column:external_login_verification"`
|
|
SecondFactorVerification sql.NullTime `json:"-" gorm:"column:second_factor_verification"`
|
|
SecondFactorVerificationType sql.NullInt32 `json:"-" gorm:"column:second_factor_verification_type"`
|
|
MultiFactorVerification sql.NullTime `json:"-" gorm:"column:multi_factor_verification"`
|
|
MultiFactorVerificationType sql.NullInt32 `json:"-" gorm:"column:multi_factor_verification_type"`
|
|
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
|
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
|
|
}
|
|
|
|
type userAgentIDPayload struct {
|
|
ID string `json:"userAgentID"`
|
|
}
|
|
|
|
func UserAgentIDFromEvent(event eventstore.Event) (string, error) {
|
|
payload := new(userAgentIDPayload)
|
|
if err := event.Unmarshal(payload); err != nil {
|
|
logging.WithError(err).Error("could not unmarshal event data")
|
|
return "", zerrors.ThrowInternal(nil, "MODEL-HJwk9", "could not unmarshal data")
|
|
}
|
|
return payload.ID, nil
|
|
}
|
|
|
|
func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView {
|
|
return &model.UserSessionView{
|
|
ChangeDate: userSession.ChangeDate,
|
|
CreationDate: userSession.CreationDate,
|
|
ResourceOwner: userSession.ResourceOwner,
|
|
State: userSession.State.V,
|
|
UserAgentID: userSession.UserAgentID,
|
|
UserID: userSession.UserID,
|
|
UserName: userSession.UserName.String,
|
|
LoginName: userSession.LoginName.String,
|
|
DisplayName: userSession.DisplayName.String,
|
|
AvatarKey: userSession.AvatarKey.String,
|
|
SelectedIDPConfigID: userSession.SelectedIDPConfigID.String,
|
|
PasswordVerification: userSession.PasswordVerification.Time,
|
|
PasswordlessVerification: userSession.PasswordlessVerification.Time,
|
|
ExternalLoginVerification: userSession.ExternalLoginVerification.Time,
|
|
SecondFactorVerification: userSession.SecondFactorVerification.Time,
|
|
SecondFactorVerificationType: domain.MFAType(userSession.SecondFactorVerificationType.Int32),
|
|
MultiFactorVerification: userSession.MultiFactorVerification.Time,
|
|
MultiFactorVerificationType: domain.MFAType(userSession.MultiFactorVerificationType.Int32),
|
|
Sequence: userSession.Sequence,
|
|
}
|
|
}
|
|
|
|
func UserSessionsToModel(userSessions []*UserSessionView) []*model.UserSessionView {
|
|
result := make([]*model.UserSessionView, len(userSessions))
|
|
for i, s := range userSessions {
|
|
result[i] = UserSessionToModel(s)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (v *UserSessionView) AppendEvent(event eventstore.Event) error {
|
|
// in case anything needs to be change here check if the Reduce function needs the change as well
|
|
v.Sequence = event.Sequence()
|
|
v.ChangeDate = event.CreatedAt()
|
|
switch event.Type() {
|
|
case user.UserV1PasswordCheckSucceededType,
|
|
user.HumanPasswordCheckSucceededType:
|
|
v.PasswordVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
|
v.State.V = domain.UserSessionStateActive
|
|
case user.UserIDPLoginCheckSucceededType:
|
|
data := new(es_model.AuthRequest)
|
|
err := data.SetData(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.ExternalLoginVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
|
v.SelectedIDPConfigID = sql.NullString{String: data.SelectedIDPConfigID, Valid: true}
|
|
v.State.V = domain.UserSessionStateActive
|
|
case user.HumanPasswordlessTokenCheckSucceededType:
|
|
v.PasswordlessVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
|
v.MultiFactorVerification = sql.NullTime{Time: event.CreatedAt(), Valid: true}
|
|
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFATypeU2FUserVerification)}
|
|
v.State.V = domain.UserSessionStateActive
|
|
case user.HumanPasswordlessTokenCheckFailedType,
|
|
user.HumanPasswordlessTokenRemovedType:
|
|
v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
v.MultiFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
case user.UserV1PasswordCheckFailedType,
|
|
user.HumanPasswordCheckFailedType:
|
|
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
case user.UserV1PasswordChangedType,
|
|
user.HumanPasswordChangedType:
|
|
data := new(es_model.PasswordChange)
|
|
err := data.SetData(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v.UserAgentID != data.UserAgentID {
|
|
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
}
|
|
case user.HumanMFAOTPVerifiedType:
|
|
data := new(es_model.OTPVerified)
|
|
err := data.SetData(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v.UserAgentID == data.UserAgentID {
|
|
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeTOTP)
|
|
}
|
|
case user.UserV1MFAOTPCheckSucceededType,
|
|
user.HumanMFAOTPCheckSucceededType:
|
|
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeTOTP)
|
|
case user.HumanOTPSMSCheckSucceededType:
|
|
data := new(es_model.OTPVerified)
|
|
err := data.SetData(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v.UserAgentID == data.UserAgentID {
|
|
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeOTPSMS)
|
|
}
|
|
case user.HumanOTPEmailCheckSucceededType:
|
|
data := new(es_model.OTPVerified)
|
|
err := data.SetData(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v.UserAgentID == data.UserAgentID {
|
|
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeOTPEmail)
|
|
}
|
|
case user.UserV1MFAOTPCheckFailedType,
|
|
user.UserV1MFAOTPRemovedType,
|
|
user.HumanMFAOTPCheckFailedType,
|
|
user.HumanMFAOTPRemovedType,
|
|
user.HumanU2FTokenCheckFailedType,
|
|
user.HumanU2FTokenRemovedType,
|
|
user.HumanOTPSMSCheckFailedType,
|
|
user.HumanOTPEmailCheckFailedType:
|
|
v.SecondFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
case user.HumanU2FTokenVerifiedType:
|
|
data := new(es_model.WebAuthNVerify)
|
|
err := data.SetData(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if v.UserAgentID == data.UserAgentID {
|
|
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeU2F)
|
|
}
|
|
case user.HumanU2FTokenCheckSucceededType:
|
|
v.setSecondFactorVerification(event.CreatedAt(), domain.MFATypeU2F)
|
|
case user.UserV1SignedOutType,
|
|
user.HumanSignedOutType,
|
|
user.UserLockedType,
|
|
user.UserDeactivatedType:
|
|
v.PasswordlessVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
v.PasswordVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
v.SecondFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
v.SecondFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFALevelNotSetUp)}
|
|
v.MultiFactorVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
v.MultiFactorVerificationType = sql.NullInt32{Int32: int32(domain.MFALevelNotSetUp)}
|
|
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
v.State.V = domain.UserSessionStateTerminated
|
|
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
|
|
v.ExternalLoginVerification = sql.NullTime{Time: time.Time{}, Valid: true}
|
|
v.SelectedIDPConfigID = sql.NullString{String: "", Valid: true}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *UserSessionView) setSecondFactorVerification(verificationTime time.Time, mfaType domain.MFAType) {
|
|
v.SecondFactorVerification = sql.NullTime{Time: verificationTime, Valid: true}
|
|
v.SecondFactorVerificationType = sql.NullInt32{Int32: int32(mfaType)}
|
|
v.State.V = domain.UserSessionStateActive
|
|
}
|
|
|
|
func (v *UserSessionView) EventTypes() []eventstore.EventType {
|
|
return []eventstore.EventType{
|
|
user.UserV1PasswordCheckSucceededType,
|
|
user.HumanPasswordCheckSucceededType,
|
|
user.UserIDPLoginCheckSucceededType,
|
|
user.HumanPasswordlessTokenCheckSucceededType,
|
|
user.HumanPasswordlessTokenCheckFailedType,
|
|
user.HumanPasswordlessTokenRemovedType,
|
|
user.UserV1PasswordCheckFailedType,
|
|
user.HumanPasswordCheckFailedType,
|
|
user.UserV1PasswordChangedType,
|
|
user.HumanPasswordChangedType,
|
|
user.HumanMFAOTPVerifiedType,
|
|
user.UserV1MFAOTPCheckSucceededType,
|
|
user.HumanMFAOTPCheckSucceededType,
|
|
user.UserV1MFAOTPCheckFailedType,
|
|
user.UserV1MFAOTPRemovedType,
|
|
user.HumanMFAOTPCheckFailedType,
|
|
user.HumanMFAOTPRemovedType,
|
|
user.HumanOTPSMSCheckSucceededType,
|
|
user.HumanOTPSMSCheckFailedType,
|
|
user.HumanOTPEmailCheckSucceededType,
|
|
user.HumanOTPEmailCheckFailedType,
|
|
user.HumanU2FTokenCheckFailedType,
|
|
user.HumanU2FTokenRemovedType,
|
|
user.HumanU2FTokenVerifiedType,
|
|
user.HumanU2FTokenCheckSucceededType,
|
|
user.UserV1SignedOutType,
|
|
user.HumanSignedOutType,
|
|
user.UserLockedType,
|
|
user.UserDeactivatedType,
|
|
user.UserIDPLinkRemovedType,
|
|
user.UserIDPLinkCascadeRemovedType,
|
|
}
|
|
}
|