feat: passwordless registration (#2103)

* begin pw less registration

* create pwless one time codes

* send pwless link

* separate send and add passwordless link

* separate send and add passwordless link events

* custom message text for passwordless registration

* begin custom login texts for passwordless

* i18n

* i18n message

* i18n message

* custom message text

* custom login text

* org design and texts

* create link in human import process

* fix import human tests

* begin passwordless init required step

* passwordless init

* passwordless init

* do not return link in mgmt api

* prompt

* passwordless init only (no additional prompt)

* cleanup

* cleanup

* add passwordless prompt to custom login text

* increase init code complexity

* fix grpc

* cleanup

* fix and add some cases for nextStep tests

* fix tests

* Update internal/notification/static/i18n/en.yaml

* Update internal/notification/static/i18n/de.yaml

* Update proto/zitadel/management.proto

* Update internal/ui/login/static/i18n/de.yaml

* Update internal/ui/login/static/i18n/de.yaml

* Update internal/ui/login/static/i18n/de.yaml

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Livio Amstutz
2021-08-02 15:24:58 +02:00
committed by GitHub
parent 9b5cb38d62
commit 00220e9532
60 changed files with 2916 additions and 350 deletions

View File

@@ -39,6 +39,7 @@ type Commands struct {
emailVerificationCode crypto.Generator
phoneVerificationCode crypto.Generator
passwordVerificationCode crypto.Generator
passwordlessInitCode crypto.Generator
machineKeyAlg crypto.EncryptionAlgorithm
machineKeySize int
applicationKeySize int
@@ -90,6 +91,7 @@ func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults
repo.emailVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.EmailVerificationCode, userEncryptionAlgorithm)
repo.phoneVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PhoneVerificationCode, userEncryptionAlgorithm)
repo.passwordVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PasswordVerificationCode, userEncryptionAlgorithm)
repo.passwordlessInitCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PasswordlessInitCode, userEncryptionAlgorithm)
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
repo.machineKeyAlg = userEncryptionAlgorithm
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)

View File

@@ -32,6 +32,9 @@ func (c *Commands) createAllLoginTextEvents(ctx context.Context, agg *eventstore
events = append(events, c.createVerifyMFAOTPEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createVerifyMFAU2FEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createPasswordlessEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createPasswordlessPromptEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createPasswordlessRegistrationEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createPasswordlessRegistrationDoneEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createPasswordChangeEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createPasswordChangeDoneEvents(ctx, agg, existingText, text, defaultText)...)
events = append(events, c.createPasswordResetDoneEvents(ctx, agg, existingText, text, defaultText)...)
@@ -589,6 +592,81 @@ func (c *Commands) createPasswordlessEvents(ctx context.Context, agg *eventstore
return events
}
func (c *Commands) createPasswordlessRegistrationEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
events := make([]eventstore.EventPusher, 0)
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationTitle, existingText.PasswordlessRegistrationTitle, text.PasswordlessRegistration.Title, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDescription, existingText.PasswordlessRegistrationDescription, text.PasswordlessRegistration.Description, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationRegisterTokenButtonText, existingText.PasswordlessRegistrationRegisterTokenButtonText, text.PasswordlessRegistration.RegisterTokenButtonText, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationTokenNameLabel, existingText.PasswordlessRegistrationTokenNameLabel, text.PasswordlessRegistration.TokenNameLabel, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationNotSupported, existingText.PasswordlessRegistrationNotSupported, text.PasswordlessRegistration.NotSupported, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationErrorRetry, existingText.PasswordlessRegistrationErrorRetry, text.PasswordlessRegistration.ErrorRetry, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
return events
}
func (c *Commands) createPasswordlessPromptEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
events := make([]eventstore.EventPusher, 0)
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptTitle, existingText.PasswordlessPromptTitle, text.PasswordlessPrompt.Title, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptDescription, existingText.PasswordlessPromptDescription, text.PasswordlessPrompt.Description, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptDescriptionInit, existingText.PasswordlessPromptDescriptionInit, text.PasswordlessPrompt.DescriptionInit, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptPasswordlessButtonText, existingText.PasswordlessPromptPasswordlessButtonText, text.PasswordlessPrompt.PasswordlessButtonText, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptNextButtonText, existingText.PasswordlessPromptNextButtonText, text.PasswordlessPrompt.NextButtonText, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptSkipButtonText, existingText.PasswordlessPromptSkipButtonText, text.PasswordlessPrompt.SkipButtonText, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
return events
}
func (c *Commands) createPasswordlessRegistrationDoneEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
events := make([]eventstore.EventPusher, 0)
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDoneTitle, existingText.PasswordlessRegistrationDoneTitle, text.PasswordlessRegistrationDone.Title, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDoneDescription, existingText.PasswordlessRegistrationDoneDescription, text.PasswordlessRegistrationDone.Description, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDoneNextButtonText, existingText.PasswordlessRegistrationDoneNextButtonText, text.PasswordlessRegistrationDone.NextButtonText, text.Language, defaultText)
if event != nil {
events = append(events, event)
}
return events
}
func (c *Commands) createPasswordChangeEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
events := make([]eventstore.EventPusher, 0)
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordChangeTitle, existingText.PasswordChangeTitle, text.PasswordChange.Title, text.Language, defaultText)

View File

@@ -147,6 +147,24 @@ type CustomLoginTextReadModel struct {
PasswordlessNotSupported string
PasswordlessErrorRetry string
PasswordlessPromptTitle string
PasswordlessPromptDescription string
PasswordlessPromptDescriptionInit string
PasswordlessPromptPasswordlessButtonText string
PasswordlessPromptNextButtonText string
PasswordlessPromptSkipButtonText string
PasswordlessRegistrationTitle string
PasswordlessRegistrationDescription string
PasswordlessRegistrationRegisterTokenButtonText string
PasswordlessRegistrationTokenNameLabel string
PasswordlessRegistrationNotSupported string
PasswordlessRegistrationErrorRetry string
PasswordlessRegistrationDoneTitle string
PasswordlessRegistrationDoneDescription string
PasswordlessRegistrationDoneNextButtonText string
PasswordChangeTitle string
PasswordChangeDescription string
PasswordChangeOldPasswordLabel string
@@ -314,6 +332,18 @@ func (wm *CustomLoginTextReadModel) Reduce() error {
wm.handlePasswordlessScreenSetEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessPrompt) {
wm.handlePasswordlessPromptScreenSetEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistration) {
wm.handlePasswordlessRegistrationScreenSetEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistrationDone) {
wm.handlePasswordlessRegistrationDoneScreenSetEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordChange) {
wm.handlePasswordChangeScreenSetEvent(e)
continue
@@ -438,6 +468,18 @@ func (wm *CustomLoginTextReadModel) Reduce() error {
wm.handlePasswordlessScreenRemoveEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessPrompt) {
wm.handlePasswordlessPromptScreenRemoveEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistration) {
wm.handlePasswordlessRegistrationScreenRemoveEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistrationDone) {
wm.handlePasswordlessRegistrationDoneScreenRemoveEvent(e)
continue
}
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordChange) {
wm.handlePasswordChangeScreenRemoveEvent(e)
continue
@@ -1489,6 +1531,144 @@ func (wm *CustomLoginTextReadModel) handlePasswordlessScreenRemoveEvent(e *polic
}
}
func (wm *CustomLoginTextReadModel) handlePasswordlessPromptScreenSetEvent(e *policy.CustomTextSetEvent) {
if e.Key == domain.LoginKeyPasswordlessPromptTitle {
wm.PasswordlessPromptTitle = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessPromptDescription {
wm.PasswordlessPromptDescription = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessPromptDescriptionInit {
wm.PasswordlessPromptDescriptionInit = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessPromptPasswordlessButtonText {
wm.PasswordlessPromptPasswordlessButtonText = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessPromptNextButtonText {
wm.PasswordlessPromptNextButtonText = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessPromptSkipButtonText {
wm.PasswordlessPromptSkipButtonText = e.Text
return
}
}
func (wm *CustomLoginTextReadModel) handlePasswordlessPromptScreenRemoveEvent(e *policy.CustomTextRemovedEvent) {
if e.Key == domain.LoginKeyPasswordlessPromptTitle {
wm.PasswordlessPromptTitle = ""
return
}
if e.Key == domain.LoginKeyPasswordlessPromptDescription {
wm.PasswordlessPromptDescription = ""
return
}
if e.Key == domain.LoginKeyPasswordlessPromptDescriptionInit {
wm.PasswordlessPromptDescriptionInit = ""
return
}
if e.Key == domain.LoginKeyPasswordlessPromptPasswordlessButtonText {
wm.PasswordlessPromptPasswordlessButtonText = ""
return
}
if e.Key == domain.LoginKeyPasswordlessPromptNextButtonText {
wm.PasswordlessPromptNextButtonText = ""
return
}
if e.Key == domain.LoginKeyPasswordlessPromptSkipButtonText {
wm.PasswordlessPromptSkipButtonText = ""
return
}
}
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationScreenSetEvent(e *policy.CustomTextSetEvent) {
if e.Key == domain.LoginKeyPasswordlessRegistrationTitle {
wm.PasswordlessRegistrationTitle = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationDescription {
wm.PasswordlessRegistrationDescription = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationRegisterTokenButtonText {
wm.PasswordlessRegistrationRegisterTokenButtonText = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationTokenNameLabel {
wm.PasswordlessRegistrationTokenNameLabel = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationNotSupported {
wm.PasswordlessRegistrationNotSupported = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationErrorRetry {
wm.PasswordlessRegistrationErrorRetry = e.Text
return
}
}
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationScreenRemoveEvent(e *policy.CustomTextRemovedEvent) {
if e.Key == domain.LoginKeyPasswordlessRegistrationTitle {
wm.PasswordlessRegistrationTitle = ""
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationDescription {
wm.PasswordlessRegistrationDescription = ""
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationRegisterTokenButtonText {
wm.PasswordlessRegistrationRegisterTokenButtonText = ""
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationTokenNameLabel {
wm.PasswordlessRegistrationTokenNameLabel = ""
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationNotSupported {
wm.PasswordlessRegistrationNotSupported = ""
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationErrorRetry {
wm.PasswordlessRegistrationErrorRetry = ""
return
}
}
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationDoneScreenSetEvent(e *policy.CustomTextSetEvent) {
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneTitle {
wm.PasswordlessRegistrationDoneTitle = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneDescription {
wm.PasswordlessRegistrationDoneDescription = e.Text
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneNextButtonText {
wm.PasswordlessRegistrationDoneNextButtonText = e.Text
return
}
}
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationDoneScreenRemoveEvent(e *policy.CustomTextRemovedEvent) {
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneTitle {
wm.PasswordlessRegistrationDoneTitle = ""
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneDescription {
wm.PasswordlessRegistrationDoneDescription = ""
return
}
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneNextButtonText {
wm.PasswordlessRegistrationDoneNextButtonText = ""
return
}
}
func (wm *CustomLoginTextReadModel) handlePasswordChangeScreenSetEvent(e *policy.CustomTextSetEvent) {
if e.Key == domain.LoginKeyPasswordChangeTitle {
wm.PasswordChangeTitle = e.Text

View File

@@ -143,3 +143,13 @@ func authRequestDomainToAuthRequestInfo(authRequest *domain.AuthRequest) *user.A
}
return info
}
func writeModelToPasswordlessInitCode(initCodeModel *HumanPasswordlessInitCodeWriteModel, code string) *domain.PasswordlessInitCode {
return &domain.PasswordlessInitCode{
ObjectRoot: writeModelToObjectRoot(initCodeModel.WriteModel),
CodeID: initCodeModel.CodeID,
Code: code,
Expiration: initCodeModel.Expiration,
State: initCodeModel.State,
}
}

View File

@@ -50,33 +50,40 @@ func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Hum
return writeModelToHuman(addedHuman), nil
}
func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human) (*domain.Human, error) {
func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
}
orgIAMPolicy, err := c.getOrgIAMPolicy(ctx, orgID)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.OrgIAMPolicy.NotFound")
return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.OrgIAMPolicy.NotFound")
}
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexity.NotFound")
return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexity.NotFound")
}
events, addedHuman, err := c.importHuman(ctx, orgID, human, orgIAMPolicy, pwPolicy)
events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, orgIAMPolicy, pwPolicy)
if err != nil {
return nil, err
return nil, nil, err
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err
return nil, nil, err
}
err = AppendAndReduce(addedHuman, pushedEvents...)
if err != nil {
return nil, err
return nil, nil, err
}
if addedCode != nil {
err = AppendAndReduce(addedCode, pushedEvents...)
if err != nil {
return nil, nil, err
}
passwordlessCode = writeModelToPasswordlessInitCode(addedCode, code)
}
return writeModelToHuman(addedHuman), nil
return writeModelToHuman(addedHuman), passwordlessCode, nil
}
func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
@@ -86,14 +93,26 @@ func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Hum
if human.Password != nil && human.SecretString != "" {
human.ChangeRequired = true
}
return c.createHuman(ctx, orgID, human, nil, false, orgIAMPolicy, pwPolicy)
return c.createHuman(ctx, orgID, human, nil, false, false, orgIAMPolicy, pwPolicy)
}
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) (events []eventstore.EventPusher, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
if orgID == "" || !human.IsValid() {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid")
return nil, nil, nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid")
}
return c.createHuman(ctx, orgID, human, nil, false, orgIAMPolicy, pwPolicy)
events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, orgIAMPolicy, pwPolicy)
if err != nil {
return nil, nil, nil, "", err
}
if passwordless {
var codeEvent eventstore.EventPusher
codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true)
if err != nil {
return nil, nil, nil, "", err
}
events = append(events, codeEvent)
}
return events, humanWriteModel, passwordlessCodeWriteModel, code, nil
}
func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, orgMemberRoles []string) (*domain.Human, error) {
@@ -149,10 +168,10 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai
if human.Password != nil && human.SecretString != "" {
human.ChangeRequired = false
}
return c.createHuman(ctx, orgID, human, externalIDP, true, orgIAMPolicy, pwPolicy)
return c.createHuman(ctx, orgID, human, externalIDP, true, false, orgIAMPolicy, pwPolicy)
}
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, selfregister bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, selfregister, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
if err := human.CheckOrgIAMPolicy(orgIAMPolicy); err != nil {
return nil, nil, err
}
@@ -187,7 +206,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
events = append(events, event)
}
if human.IsInitialState() {
if human.IsInitialState(passwordless) {
initCode, err := domain.NewInitUserCode(c.initializeUserCode)
if err != nil {
return nil, nil, err

View File

@@ -652,19 +652,22 @@ func TestCommandSide_AddHuman(t *testing.T) {
func TestCommandSide_ImportHuman(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretGenerator crypto.Generator
userPasswordAlg crypto.HashAlgorithm
eventstore *eventstore.Eventstore
idGenerator id.Generator
secretGenerator crypto.Generator
userPasswordAlg crypto.HashAlgorithm
passwordlessInitCode crypto.Generator
}
type args struct {
ctx context.Context
orgID string
human *domain.Human
ctx context.Context
orgID string
human *domain.Human
passwordless bool
}
type res struct {
want *domain.Human
err func(error) bool
wantHuman *domain.Human
wantCode *domain.PasswordlessInitCode
err func(error) bool
}
tests := []struct {
name string
@@ -869,7 +872,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
want: &domain.Human{
wantHuman: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
@@ -950,7 +953,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
want: &domain.Human{
wantHuman: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
@@ -970,6 +973,218 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
},
{
name: "add human email verified passwordless only, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewOrgIAMPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newAddHumanEvent("", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate),
),
eventFromEventPusher(
user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"code1",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour,
),
),
},
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
secretGenerator: GetMockSecretGenerator(t),
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
passwordlessInitCode: GetMockSecretGenerator(t),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &domain.Human{
Username: "username",
Profile: &domain.Profile{
FirstName: "firstname",
LastName: "lastname",
},
Email: &domain.Email{
EmailAddress: "email@test.ch",
IsEmailVerified: true,
},
},
passwordless: true,
},
res: res{
wantHuman: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
Username: "username",
Profile: &domain.Profile{
FirstName: "firstname",
LastName: "lastname",
DisplayName: "firstname lastname",
PreferredLanguage: language.Und,
},
Email: &domain.Email{
EmailAddress: "email@test.ch",
IsEmailVerified: true,
},
State: domain.UserStateActive,
},
wantCode: &domain.PasswordlessInitCode{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
Expiration: time.Hour,
CodeID: "code1",
Code: "a",
State: domain.PasswordlessInitCodeStateActive,
},
},
},
{
name: "add human email verified passwordless and password change not required, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewOrgIAMPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newAddHumanEvent("password", false, ""),
),
eventFromEventPusher(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate),
),
eventFromEventPusher(
user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"code1",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour,
),
),
},
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
secretGenerator: GetMockSecretGenerator(t),
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
passwordlessInitCode: GetMockSecretGenerator(t),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &domain.Human{
Username: "username",
Password: &domain.Password{
SecretString: "password",
ChangeRequired: false,
},
Profile: &domain.Profile{
FirstName: "firstname",
LastName: "lastname",
},
Email: &domain.Email{
EmailAddress: "email@test.ch",
IsEmailVerified: true,
},
},
passwordless: true,
},
res: res{
wantHuman: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
Username: "username",
Profile: &domain.Profile{
FirstName: "firstname",
LastName: "lastname",
DisplayName: "firstname lastname",
PreferredLanguage: language.Und,
},
Email: &domain.Email{
EmailAddress: "email@test.ch",
IsEmailVerified: true,
},
State: domain.UserStateActive,
},
wantCode: &domain.PasswordlessInitCode{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
Expiration: time.Hour,
CodeID: "code1",
Code: "a",
State: domain.PasswordlessInitCodeStateActive,
},
},
},
{
name: "add human (with phone), ok",
fields: fields{
@@ -1052,7 +1267,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
want: &domain.Human{
wantHuman: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
@@ -1151,7 +1366,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
},
},
res: res{
want: &domain.Human{
wantHuman: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
@@ -1182,8 +1397,9 @@ func TestCommandSide_ImportHuman(t *testing.T) {
initializeUserCode: tt.fields.secretGenerator,
phoneVerificationCode: tt.fields.secretGenerator,
userPasswordAlg: tt.fields.userPasswordAlg,
passwordlessInitCode: tt.fields.passwordlessInitCode,
}
got, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human)
gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless)
if tt.res.err == nil {
assert.NoError(t, err)
}
@@ -1191,7 +1407,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
assert.Equal(t, tt.res.wantHuman, gotHuman)
assert.Equal(t, tt.res.wantCode, gotCode)
}
})
}

View File

@@ -2,9 +2,11 @@ package command
import (
"context"
"time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@@ -127,8 +129,16 @@ func (c *Commands) HumanAddPasswordlessSetup(ctx context.Context, userID, resour
return createdWebAuthN, nil
}
func (c *Commands) HumanAddPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, codeID, verificationCode string) (*domain.WebAuthNToken, error) {
err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode)
if err != nil {
return nil, err
}
return c.HumanAddPasswordlessSetup(ctx, userID, resourceowner, true)
}
func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner string, isLoginUI bool, tokens []*domain.WebAuthNToken) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
if userID == "" || resourceowner == "" {
if userID == "" {
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing")
}
user, err := c.getHuman(ctx, userID, resourceowner)
@@ -198,7 +208,24 @@ func (c *Commands) HumanVerifyU2FSetup(ctx context.Context, userID, resourceowne
return writeModelToObjectDetails(&verifyWebAuthN.WriteModel), nil
}
func (c *Commands) HumanPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, tokenName, userAgentID, codeID, verificationCode string, credentialData []byte) (*domain.ObjectDetails, error) {
err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode)
if err != nil {
return nil, err
}
succeededEvent := func(userAgg *eventstore.Aggregate) *usr_repo.HumanPasswordlessInitCodeCheckSucceededEvent {
return usr_repo.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, codeID)
}
return c.humanHumanPasswordlessSetup(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, succeededEvent)
}
func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) (*domain.ObjectDetails, error) {
return c.humanHumanPasswordlessSetup(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, nil)
}
func (c *Commands) humanHumanPasswordlessSetup(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte,
codeCheckEvent func(*eventstore.Aggregate) *usr_repo.HumanPasswordlessInitCodeCheckSucceededEvent) (*domain.ObjectDetails, error) {
u2fTokens, err := c.getHumanPasswordlessTokens(ctx, userID, resourceowner)
if err != nil {
return nil, err
@@ -208,7 +235,7 @@ func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, reso
return nil, err
}
pushedEvents, err := c.eventstore.PushEvents(ctx,
events := []eventstore.EventPusher{
usr_repo.NewHumanPasswordlessVerifiedEvent(
ctx,
userAgg,
@@ -221,7 +248,11 @@ func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, reso
webAuthN.SignCount,
userAgentID,
),
)
}
if codeCheckEvent != nil {
events = append(events, codeCheckEvent(userAgg))
}
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
if err != nil {
return nil, err
}
@@ -233,7 +264,7 @@ func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, reso
}
func (c *Commands) verifyHumanWebAuthN(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte, tokens []*domain.WebAuthNToken) (*eventstore.Aggregate, *domain.WebAuthNToken, *HumanWebAuthNWriteModel, error) {
if userID == "" || resourceowner == "" {
if userID == "" {
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing")
}
user, err := c.getHuman(ctx, userID, resourceowner)
@@ -452,6 +483,102 @@ func (c *Commands) HumanRemovePasswordless(ctx context.Context, userID, webAuthN
return c.removeHumanWebAuthN(ctx, userID, webAuthNID, resourceOwner, event)
}
func (c *Commands) HumanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string) (*domain.PasswordlessInitCode, error) {
codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, true)
pushedEvents, err := c.eventstore.PushEvents(ctx, codeEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(initCode, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToPasswordlessInitCode(initCode, code), nil
}
func (c *Commands) HumanSendPasswordlessInitCode(ctx context.Context, userID, resourceOwner string) (*domain.PasswordlessInitCode, error) {
codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, true)
pushedEvents, err := c.eventstore.PushEvents(ctx, codeEvent)
if err != nil {
return nil, err
}
err = AppendAndReduce(initCode, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToPasswordlessInitCode(initCode, code), nil
}
func (c *Commands) humanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string, direct bool) (eventstore.EventPusher, *HumanPasswordlessInitCodeWriteModel, string, error) {
if userID == "" {
return nil, nil, "", caos_errs.ThrowPreconditionFailed(nil, "COMMAND-GVfg3", "Errors.IDMissing")
}
codeID, err := c.idGenerator.Next()
if err != nil {
return nil, nil, "", err
}
initCode := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
err = c.eventstore.FilterToQueryReducer(ctx, initCode)
if err != nil {
return nil, nil, "", err
}
cryptoCode, code, err := crypto.NewCode(c.passwordlessInitCode)
if err != nil {
return nil, nil, "", err
}
codeEventCreator := func(ctx context.Context, agg *eventstore.Aggregate, id string, cryptoCode *crypto.CryptoValue, exp time.Duration) eventstore.EventPusher {
return usr_repo.NewHumanPasswordlessInitCodeAddedEvent(ctx, agg, id, cryptoCode, exp)
}
if !direct {
codeEventCreator = func(ctx context.Context, agg *eventstore.Aggregate, id string, cryptoCode *crypto.CryptoValue, exp time.Duration) eventstore.EventPusher {
return usr_repo.NewHumanPasswordlessInitCodeRequestedEvent(ctx, agg, id, cryptoCode, exp)
}
}
codeEvent := codeEventCreator(ctx, UserAggregateFromWriteModel(&initCode.WriteModel), codeID, cryptoCode, c.passwordlessInitCode.Expiry())
return codeEvent, initCode, code, nil
}
func (c *Commands) HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error {
if userID == "" || codeID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-ADggh", "Errors.IDMissing")
}
initCode := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, initCode)
if err != nil {
return err
}
if initCode.State != domain.PasswordlessInitCodeStateRequested {
return caos_errs.ThrowNotFound(nil, "COMMAND-Gdfg3", "Errors.User.Code.NotFound")
}
_, err = c.eventstore.PushEvents(ctx,
usr_repo.NewHumanPasswordlessInitCodeSentEvent(ctx, UserAggregateFromWriteModel(&initCode.WriteModel), codeID),
)
return err
}
func (c *Commands) humanVerifyPasswordlessInitCode(ctx context.Context, userID, resourceOwner, codeID, verificationCode string) error {
if userID == "" || codeID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-GVfg3", "Errors.IDMissing")
}
initCode := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, initCode)
if err != nil {
return err
}
err = crypto.VerifyCode(initCode.ChangeDate, initCode.Expiration, initCode.CryptoCode, verificationCode, c.passwordlessInitCode)
if err != nil || initCode.State != domain.PasswordlessInitCodeStateActive {
userAgg := UserAggregateFromWriteModel(&initCode.WriteModel)
_, err = c.eventstore.PushEvents(ctx, usr_repo.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, codeID))
logging.LogWithFields("COMMAND-Gkuud", "userID", userAgg.ID).OnError(err).Error("NewHumanPasswordlessInitCodeCheckFailedEvent push failed")
return caos_errs.ThrowInvalidArgument(err, "COMMAND-Dhz8i", "Errors.User.Code.Invalid")
}
return nil
}
func (c *Commands) removeHumanWebAuthN(ctx context.Context, userID, webAuthNID, resourceOwner string, preparedEvent func(*eventstore.Aggregate) eventstore.EventPusher) (*domain.ObjectDetails, error) {
if userID == "" || webAuthNID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M9de", "Errors.IDMissing")

View File

@@ -1,6 +1,9 @@
package command
import (
"time"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
@@ -430,3 +433,106 @@ func (rm *HumanPasswordlessLoginReadModel) Query() *eventstore.SearchQueryBuilde
Builder()
}
type HumanPasswordlessInitCodeWriteModel struct {
eventstore.WriteModel
CodeID string
Attempts uint8
CryptoCode *crypto.CryptoValue
Expiration time.Duration
State domain.PasswordlessInitCodeState
}
func NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner string) *HumanPasswordlessInitCodeWriteModel {
return &HumanPasswordlessInitCodeWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
CodeID: codeID,
}
}
func (wm *HumanPasswordlessInitCodeWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanPasswordlessInitCodeAddedEvent:
if wm.CodeID == e.ID {
wm.WriteModel.AppendEvents(e)
}
case *user.HumanPasswordlessInitCodeRequestedEvent:
if wm.CodeID == e.ID {
wm.WriteModel.AppendEvents(e)
}
case *user.HumanPasswordlessInitCodeSentEvent:
if wm.CodeID == e.ID {
wm.WriteModel.AppendEvents(e)
}
case *user.HumanPasswordlessInitCodeCheckFailedEvent:
if wm.CodeID == e.ID {
wm.WriteModel.AppendEvents(e)
}
case *user.HumanPasswordlessInitCodeCheckSucceededEvent:
if wm.CodeID == e.ID {
wm.WriteModel.AppendEvents(e)
}
case *user.UserRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
func (wm *HumanPasswordlessInitCodeWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanPasswordlessInitCodeAddedEvent:
wm.appendAddedEvent(e)
case *user.HumanPasswordlessInitCodeRequestedEvent:
wm.appendRequestedEvent(e)
case *user.HumanPasswordlessInitCodeSentEvent:
wm.State = domain.PasswordlessInitCodeStateActive
case *user.HumanPasswordlessInitCodeCheckFailedEvent:
wm.appendCheckFailedEvent(e)
case *user.HumanPasswordlessInitCodeCheckSucceededEvent:
wm.State = domain.PasswordlessInitCodeStateRemoved
case *user.UserRemovedEvent:
wm.State = domain.PasswordlessInitCodeStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *HumanPasswordlessInitCodeWriteModel) appendAddedEvent(e *user.HumanPasswordlessInitCodeAddedEvent) {
wm.CryptoCode = e.Code
wm.Expiration = e.Expiry
wm.State = domain.PasswordlessInitCodeStateActive
}
func (wm *HumanPasswordlessInitCodeWriteModel) appendRequestedEvent(e *user.HumanPasswordlessInitCodeRequestedEvent) {
wm.CryptoCode = e.Code
wm.Expiration = e.Expiry
wm.State = domain.PasswordlessInitCodeStateRequested
}
func (wm *HumanPasswordlessInitCodeWriteModel) appendCheckFailedEvent(e *user.HumanPasswordlessInitCodeCheckFailedEvent) {
wm.Attempts++
if wm.Attempts == 3 { //TODO: config?
wm.State = domain.PasswordlessInitCodeStateRemoved
}
}
func (wm *HumanPasswordlessInitCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(user.HumanPasswordlessInitCodeAddedType,
user.HumanPasswordlessInitCodeRequestedType,
user.HumanPasswordlessInitCodeSentType,
user.HumanPasswordlessInitCodeCheckFailedType,
user.HumanPasswordlessInitCodeCheckSucceededType,
user.UserRemovedType).
Builder()
}