mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:57:32 +00:00
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:
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -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")
|
||||
|
@@ -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()
|
||||
}
|
||||
|
Reference in New Issue
Block a user