zitadel/internal/user/repository/view/model/user_session.go
Livio Spring fb162a7d75
fix(login): improve auth handlers (#7969)
# Which Problems Are Solved

During the implementation of #7486 it was noticed, that projections in
the `auth` database schema could be blocked.
Investigations suggested, that this is due to the use of
[GORM](https://gorm.io/index.html) and it's inability to use an existing
(sql) transaction.
With the improved / simplified handling (see below) there should also be
a minimal improvement in performance, resp. reduced database update
statements.

# How the Problems Are Solved

The handlers in `auth` are exchanged to proper (sql) statements and gorm
usage is removed for any writing part.
To further improve / simplify the handling of the users, a new
`auth.users3` table is created, where only attributes are handled, which
are not yet available from the `projections.users`,
`projections.login_name` and `projections.user_auth_methods` do not
provide. This reduces the events handled in that specific handler by a
lot.

# Additional Changes

None

# Additional Context

relates to #7486
2024-05-22 15:26:02 +00:00

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 int32 `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: domain.UserSessionState(userSession.State),
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 = int32(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 = int32(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 = int32(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 = int32(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 = int32(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,
}
}