fix: improvements for login flow (incl. webauthn) (#1026)

* fix: typo ZITADEL uppercase for OTP Issuer

* fix: password validation after change in current user agent

* fix: otp validation after setup in current user agent

* add waiting

* add waiting

* show u2f state

* regenerate css

* add useragentID to webauthn verify

* return mfa attribute in mgmt

* switch between providers

* use preferredLoginName for webauthn display

* some fixes

* correct translations for login

* add some missing event translations

* fix usersession test

* remove unnecessary cancel button on password change done
This commit is contained in:
Livio Amstutz
2020-12-07 12:09:10 +01:00
committed by GitHub
parent 8b88a0ab86
commit 077a9a628e
48 changed files with 451 additions and 123 deletions

View File

@@ -578,10 +578,10 @@ func (es *UserEventstore) SetOneTimePassword(ctx context.Context, policy *iam_mo
if err != nil {
return nil, err
}
return es.changedPassword(ctx, user, policy, password.SecretString, true)
return es.changedPassword(ctx, user, policy, password.SecretString, true, "")
}
func (es *UserEventstore) SetPassword(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, code, password string) error {
func (es *UserEventstore) SetPassword(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, code, password, userAgentID string) error {
user, err := es.HumanByID(ctx, userID)
if err != nil {
return err
@@ -592,7 +592,7 @@ func (es *UserEventstore) SetPassword(ctx context.Context, policy *iam_model.Pas
if err := crypto.VerifyCode(user.PasswordCode.CreationDate, user.PasswordCode.Expiry, user.PasswordCode.Code, code, es.PasswordVerificationCode); err != nil {
return err
}
_, err = es.changedPassword(ctx, user, policy, password, false)
_, err = es.changedPassword(ctx, user, policy, password, false, userAgentID)
return err
}
@@ -655,7 +655,7 @@ func (es *UserEventstore) ChangeMachine(ctx context.Context, machine *usr_model.
return model.MachineToModel(repoUser.Machine), nil
}
func (es *UserEventstore) ChangePassword(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, old, new string) (_ *usr_model.Password, err error) {
func (es *UserEventstore) ChangePassword(ctx context.Context, policy *iam_model.PasswordComplexityPolicyView, userID, old, new, userAgentID string) (_ *usr_model.Password, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -672,10 +672,10 @@ func (es *UserEventstore) ChangePassword(ctx context.Context, policy *iam_model.
if err != nil {
return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-s56a3", "Errors.User.Password.Invalid")
}
return es.changedPassword(ctx, user, policy, new, false)
return es.changedPassword(ctx, user, policy, new, false, userAgentID)
}
func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.User, policy *iam_model.PasswordComplexityPolicyView, password string, onetime bool) (_ *usr_model.Password, err error) {
func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.User, policy *iam_model.PasswordComplexityPolicyView, password string, onetime bool, userAgentID string) (_ *usr_model.Password, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
pw := &usr_model.Password{SecretString: password}
@@ -683,7 +683,7 @@ func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.U
if err != nil {
return nil, err
}
repoPassword := model.PasswordFromModel(pw)
repoPassword := model.PasswordChangeFromModel(pw, userAgentID)
repoUser := model.UserFromModel(user)
agg := PasswordChangeAggregate(es.AggregateCreator(), repoUser, repoPassword)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
@@ -1235,7 +1235,7 @@ func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error {
return nil
}
func (es *UserEventstore) CheckMFAOTPSetup(ctx context.Context, userID, code string) error {
func (es *UserEventstore) CheckMFAOTPSetup(ctx context.Context, userID, code, userAgentID string) error {
user, err := es.HumanByID(ctx, userID)
if err != nil {
return err
@@ -1250,7 +1250,7 @@ func (es *UserEventstore) CheckMFAOTPSetup(ctx context.Context, userID, code str
return err
}
repoUser := model.UserFromModel(user)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAOTPVerifyAggregate(es.AggregateCreator(), repoUser))
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAOTPVerifyAggregate(es.AggregateCreator(), repoUser, userAgentID))
if err != nil {
return err
}
@@ -1326,7 +1326,7 @@ func (es *UserEventstore) AddU2F(ctx context.Context, userID string) (*usr_model
return webAuthN, nil
}
func (es *UserEventstore) VerifyU2FSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error {
func (es *UserEventstore) VerifyU2FSetup(ctx context.Context, userID, tokenName, userAgentID string, credentialData []byte) error {
user, err := es.HumanByID(ctx, userID)
if err != nil {
return err
@@ -1337,7 +1337,7 @@ func (es *UserEventstore) VerifyU2FSetup(ctx context.Context, userID, tokenName
return err
}
repoUser := model.UserFromModel(user)
repoWebAuthN := model.WebAuthNVerifyFromModel(webAuthN)
repoWebAuthN := model.WebAuthNVerifyFromModel(webAuthN, userAgentID)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAU2FVerifyAggregate(es.AggregateCreator(), repoUser, repoWebAuthN))
if err != nil {
return err
@@ -1432,7 +1432,7 @@ func (es *UserEventstore) AddPasswordless(ctx context.Context, userID string) (*
return webAuthN, nil
}
func (es *UserEventstore) VerifyPasswordlessSetup(ctx context.Context, userID, tokenName string, credentialData []byte) error {
func (es *UserEventstore) VerifyPasswordlessSetup(ctx context.Context, userID, tokenName, userAgentID string, credentialData []byte) error {
user, err := es.HumanByID(ctx, userID)
if err != nil {
return err
@@ -1443,7 +1443,7 @@ func (es *UserEventstore) VerifyPasswordlessSetup(ctx context.Context, userID, t
return err
}
repoUser := model.UserFromModel(user)
repoWebAuthN := model.WebAuthNVerifyFromModel(webAuthN)
repoWebAuthN := model.WebAuthNVerifyFromModel(webAuthN, userAgentID)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MFAPasswordlessVerifyAggregate(es.AggregateCreator(), repoUser, repoWebAuthN))
if err != nil {
return err

View File

@@ -1678,7 +1678,7 @@ func TestSetPassword(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.es.SetPassword(tt.args.ctx, tt.args.policy, tt.args.userID, tt.args.code, tt.args.password)
err := tt.args.es.SetPassword(tt.args.ctx, tt.args.policy, tt.args.userID, tt.args.code, tt.args.password, "")
if tt.res.errFunc == nil && err != nil {
t.Errorf("result has error: %v", err)
@@ -1838,7 +1838,7 @@ func TestChangePassword(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.args.es.ChangePassword(tt.args.ctx, tt.args.policy, tt.args.userID, tt.args.old, tt.args.new)
result, err := tt.args.es.ChangePassword(tt.args.ctx, tt.args.policy, tt.args.userID, tt.args.old, tt.args.new, "")
if tt.res.errFunc == nil && result.AggregateID == "" {
t.Errorf("result has no id")
@@ -3578,7 +3578,7 @@ func TestCheckMFAOTPSetup(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.es.CheckMFAOTPSetup(tt.args.ctx, tt.args.userID, tt.args.code)
err := tt.args.es.CheckMFAOTPSetup(tt.args.ctx, tt.args.userID, tt.args.code, "")
if tt.res.errFunc == nil && err != nil {
t.Errorf("result should not get err")

View File

@@ -3,6 +3,7 @@ package model
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
@@ -16,6 +17,10 @@ type OTP struct {
State int32 `json:"-"`
}
type OTPVerified struct {
UserAgentID string `json:"userAgentID,omitempty"`
}
func OTPFromModel(otp *model.OTP) *OTP {
return &OTP{
ObjectRoot: otp.ObjectRoot,
@@ -55,3 +60,11 @@ func (o *OTP) setData(event *es_models.Event) error {
}
return nil
}
func (o *OTPVerified) SetData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, o); err != nil {
logging.Log("EVEN-BF421").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-GB6hj", "could not unmarshal event")
}
return nil
}

View File

@@ -26,6 +26,11 @@ type PasswordCode struct {
NotificationType int32 `json:"notificationType,omitempty"`
}
type PasswordChange struct {
Password
UserAgentID string `json:"userAgentID,omitempty"`
}
func PasswordFromModel(password *model.Password) *Password {
return &Password{
ObjectRoot: password.ObjectRoot,
@@ -51,6 +56,17 @@ func PasswordCodeToModel(code *PasswordCode) *model.PasswordCode {
}
}
func PasswordChangeFromModel(password *model.Password, userAgentID string) *PasswordChange {
return &PasswordChange{
Password: Password{
ObjectRoot: password.ObjectRoot,
Secret: password.SecretCrypto,
ChangeRequired: password.ChangeRequired,
},
UserAgentID: userAgentID,
}
}
func (u *Human) appendUserPasswordChangedEvent(event *es_models.Event) error {
u.Password = new(Password)
err := u.Password.setData(event)
@@ -84,3 +100,12 @@ func (c *PasswordCode) SetData(event *es_models.Event) error {
}
return nil
}
func (pw *PasswordChange) SetData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, pw); err != nil {
logging.Log("EVEN-ADs31").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-BDd32", "could not unmarshal event")
}
pw.ObjectRoot.AppendEvent(event)
return nil
}

View File

@@ -32,6 +32,7 @@ type WebAuthNVerify struct {
AAGUID []byte `json:"aaguid"`
SignCount uint32 `json:"signCount"`
WebAuthNTokenName string `json:"webAuthNTokenName"`
UserAgentID string `json:"userAgentID,omitempty"`
}
type WebAuthNSignCount struct {
@@ -104,7 +105,7 @@ func WebAuthNToModel(webAuthN *WebAuthNToken) *model.WebAuthNToken {
}
}
func WebAuthNVerifyFromModel(webAuthN *model.WebAuthNToken) *WebAuthNVerify {
func WebAuthNVerifyFromModel(webAuthN *model.WebAuthNToken, userAgentID string) *WebAuthNVerify {
return &WebAuthNVerify{
WebAuthNTokenID: webAuthN.WebAuthNTokenID,
KeyID: webAuthN.KeyID,
@@ -113,6 +114,7 @@ func WebAuthNVerifyFromModel(webAuthN *model.WebAuthNToken) *WebAuthNVerify {
SignCount: webAuthN.SignCount,
AttestationType: webAuthN.AttestationType,
WebAuthNTokenName: webAuthN.WebAuthNTokenName,
UserAgentID: userAgentID,
}
}
@@ -148,6 +150,14 @@ func WebAuthNLoginToModel(webAuthN *WebAuthNLogin) *model.WebAuthNLogin {
}
}
func (w *WebAuthNVerify) SetData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, w); err != nil {
logging.Log("EVEN-G342rf").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-B6641", "could not unmarshal event")
}
return nil
}
func (u *Human) appendU2FAddedEvent(event *es_models.Event) error {
webauthn := new(WebAuthNToken)
err := webauthn.setData(event)

View File

@@ -410,16 +410,16 @@ func SkipMFAAggregate(aggCreator *es_models.AggregateCreator, user *model.User)
}
}
func PasswordChangeAggregate(aggCreator *es_models.AggregateCreator, user *model.User, password *model.Password) func(ctx context.Context) (*es_models.Aggregate, error) {
func PasswordChangeAggregate(aggCreator *es_models.AggregateCreator, user *model.User, passwordChange *model.PasswordChange) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if password == nil {
if passwordChange == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d9832", "Errors.Internal")
}
agg, err := UserAggregate(ctx, aggCreator, user)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.HumanPasswordChanged, password)
return agg.AppendEvent(model.HumanPasswordChanged, passwordChange)
}
}
@@ -737,13 +737,13 @@ func MFAOTPAddAggregate(aggCreator *es_models.AggregateCreator, user *model.User
}
}
func MFAOTPVerifyAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
func MFAOTPVerifyAggregate(aggCreator *es_models.AggregateCreator, user *model.User, userAgentID string) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, user)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.HumanMFAOTPVerified, nil)
return agg.AppendEvent(model.HumanMFAOTPVerified, &model.OTPVerified{UserAgentID: userAgentID})
}
}

View File

@@ -1060,10 +1060,10 @@ func TestSkipMFAAggregate(t *testing.T) {
func TestChangePasswordAggregate(t *testing.T) {
type args struct {
ctx context.Context
user *model.User
password *model.Password
aggCreator *models.AggregateCreator
ctx context.Context
user *model.User
passwordChange *model.PasswordChange
aggCreator *models.AggregateCreator
}
type res struct {
eventLen int
@@ -1086,8 +1086,8 @@ func TestChangePasswordAggregate(t *testing.T) {
Profile: &model.Profile{DisplayName: "DisplayName"},
},
},
password: &model.Password{ChangeRequired: true},
aggCreator: models.NewAggregateCreator("Test"),
passwordChange: &model.PasswordChange{Password: model.Password{ChangeRequired: true}},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
eventLen: 1,
@@ -1113,7 +1113,7 @@ func TestChangePasswordAggregate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := PasswordChangeAggregate(tt.args.aggCreator, tt.args.user, tt.args.password)(tt.args.ctx)
agg, err := PasswordChangeAggregate(tt.args.aggCreator, tt.args.user, tt.args.passwordChange)(tt.args.ctx)
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
@@ -2329,7 +2329,7 @@ func TestOTPVerifyAggregate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := MFAOTPVerifyAggregate(tt.args.aggCreator, tt.args.user)(tt.args.ctx)
agg, err := MFAOTPVerifyAggregate(tt.args.aggCreator, tt.args.user, "")(tt.args.ctx)
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))

View File

@@ -81,7 +81,7 @@ func UserSessionsToModel(userSessions []*UserSessionView) []*model.UserSessionVi
return result
}
func (v *UserSessionView) AppendEvent(event *models.Event) {
func (v *UserSessionView) AppendEvent(event *models.Event) error {
v.Sequence = event.Sequence
v.ChangeDate = event.CreationDate
switch event.Type {
@@ -91,7 +91,10 @@ func (v *UserSessionView) AppendEvent(event *models.Event) {
v.State = int32(req_model.UserSessionStateActive)
case es_model.HumanExternalLoginCheckSucceeded:
data := new(es_model.AuthRequest)
data.SetData(event)
err := data.SetData(event)
if err != nil {
return err
}
v.ExternalLoginVerification = event.CreationDate
v.SelectedIDPConfigID = data.SelectedIDPConfigID
v.State = int32(req_model.UserSessionStateActive)
@@ -105,15 +108,31 @@ func (v *UserSessionView) AppendEvent(event *models.Event) {
v.PasswordlessVerification = time.Time{}
v.MultiFactorVerification = time.Time{}
case es_model.UserPasswordCheckFailed,
es_model.UserPasswordChanged,
es_model.HumanPasswordCheckFailed,
es_model.HumanPasswordChanged:
es_model.HumanPasswordCheckFailed:
v.PasswordVerification = time.Time{}
case es_model.UserPasswordChanged,
es_model.HumanPasswordChanged:
data := new(es_model.PasswordChange)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID != data.UserAgentID {
v.PasswordVerification = time.Time{}
}
case es_model.MFAOTPVerified,
es_model.HumanMFAOTPVerified:
data := new(es_model.OTPVerified)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreationDate, req_model.MFATypeOTP)
}
case es_model.MFAOTPCheckSucceeded,
es_model.HumanMFAOTPCheckSucceeded:
v.SecondFactorVerification = event.CreationDate
v.SecondFactorVerificationType = int32(req_model.MFATypeOTP)
v.State = int32(req_model.UserSessionStateActive)
v.setSecondFactorVerification(event.CreationDate, req_model.MFATypeOTP)
case es_model.MFAOTPCheckFailed,
es_model.MFAOTPRemoved,
es_model.HumanMFAOTPCheckFailed,
@@ -121,10 +140,17 @@ func (v *UserSessionView) AppendEvent(event *models.Event) {
es_model.HumanMFAU2FTokenCheckFailed,
es_model.HumanMFAU2FTokenRemoved:
v.SecondFactorVerification = time.Time{}
case es_model.HumanMFAU2FTokenVerified:
data := new(es_model.WebAuthNVerify)
err := data.SetData(event)
if err != nil {
return err
}
if v.UserAgentID == data.UserAgentID {
v.setSecondFactorVerification(event.CreationDate, req_model.MFATypeU2F)
}
case es_model.HumanMFAU2FTokenCheckSucceeded:
v.SecondFactorVerification = event.CreationDate
v.SecondFactorVerificationType = int32(req_model.MFATypeU2F)
v.State = int32(req_model.UserSessionStateActive)
v.setSecondFactorVerification(event.CreationDate, req_model.MFATypeU2F)
case es_model.SignedOut,
es_model.HumanSignedOut,
es_model.UserLocked,
@@ -137,4 +163,11 @@ func (v *UserSessionView) AppendEvent(event *models.Event) {
v.ExternalLoginVerification = time.Time{}
v.SelectedIDPConfigID = ""
}
return nil
}
func (v *UserSessionView) setSecondFactorVerification(verificationTime time.Time, mfaType req_model.MFAType) {
v.SecondFactorVerification = verificationTime
v.SecondFactorVerificationType = int32(mfaType)
v.State = int32(req_model.UserSessionStateActive)
}

View File

@@ -1,11 +1,13 @@
package model
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
)
@@ -59,18 +61,87 @@ func TestAppendEvent(t *testing.T) {
{
name: "append user password changed event",
args: args{
event: &es_models.Event{CreationDate: now(), Type: es_model.UserPasswordChanged},
userView: &UserSessionView{PasswordVerification: now()},
event: &es_models.Event{
CreationDate: now(),
Type: es_model.UserPasswordChanged,
Data: func() []byte {
d, _ := json.Marshal(&es_model.Password{
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: now()},
},
result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: time.Time{}},
},
{
name: "append human password changed event",
args: args{
event: &es_models.Event{CreationDate: now(), Type: es_model.HumanPasswordChanged},
userView: &UserSessionView{PasswordVerification: now()},
event: &es_models.Event{
CreationDate: now(),
Type: es_model.HumanPasswordChanged,
Data: func() []byte {
d, _ := json.Marshal(&es_model.PasswordChange{
Password: es_model.Password{
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
},
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: now()},
},
result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: time.Time{}},
},
{
name: "append human password changed event same user agent",
args: args{
event: &es_models.Event{
CreationDate: now(),
Type: es_model.HumanPasswordChanged,
Data: func() []byte {
d, _ := json.Marshal(&es_model.PasswordChange{
Password: es_model.Password{
Secret: &crypto.CryptoValue{Crypted: []byte("test")},
},
UserAgentID: "id",
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id", PasswordVerification: now()},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), PasswordVerification: now()},
},
{
name: "append user otp verified event",
args: args{
event: &es_models.Event{
CreationDate: now(),
Type: es_model.MFAOTPVerified,
Data: nil,
},
userView: &UserSessionView{UserAgentID: "id"},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now()},
},
{
name: "append user otp verified event same user agent",
args: args{
event: &es_models.Event{
CreationDate: now(),
Type: es_model.MFAOTPVerified,
Data: func() []byte {
d, _ := json.Marshal(&es_model.OTPVerified{
UserAgentID: "id",
})
return d
}(),
},
userView: &UserSessionView{UserAgentID: "id"},
},
result: &UserSessionView{UserAgentID: "id", ChangeDate: now(), SecondFactorVerification: now()},
},
{
name: "append user otp check succeeded event",