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

@@ -19,6 +19,7 @@ import (
org_model "github.com/caos/zitadel/internal/org/model"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/org/repository/view"
user_repo "github.com/caos/zitadel/internal/repository/user"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
)
@@ -140,7 +141,9 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
es_model.HumanPasswordlessTokenAdded,
es_model.HumanPasswordlessTokenVerified,
es_model.HumanPasswordlessTokenRemoved,
es_model.MachineChanged:
es_model.MachineChanged,
es_models.EventType(user_repo.HumanPasswordlessInitCodeAddedType),
es_models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType):
user, err = u.view.UserByID(event.AggregateID)
if err != nil {
return err

View File

@@ -181,6 +181,40 @@ func (s *Server) SetDefaultDomainClaimedMessageText(ctx context.Context, req *ad
}, nil
}
func (s *Server) GetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordlessRegistrationMessageTextRequest) (*admin_pb.GetDefaultPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.iam.GetDefaultMessageText(ctx, domain.PasswordlessRegistrationMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultPasswordlessRegistrationMessageTextResponse{
CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
func (s *Server) GetCustomPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordlessRegistrationMessageTextRequest) (*admin_pb.GetCustomPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.iam.GetCustomMessageText(ctx, domain.PasswordlessRegistrationMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomPasswordlessRegistrationMessageTextResponse{
CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordlessRegistrationMessageTextRequest) (*admin_pb.SetDefaultPasswordlessRegistrationMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, SetPasswordlessRegistrationCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultPasswordlessRegistrationMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultLoginTexts(ctx context.Context, req *admin_pb.GetDefaultLoginTextsRequest) (*admin_pb.GetDefaultLoginTextsResponse, error) {
msg, err := s.iam.GetDefaultLoginTexts(ctx, req.Language)
if err != nil {

View File

@@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *admin_pb.SetDefaultDomainClaimedMes
}
}
func SetPasswordlessRegistrationCustomTextToDomain(msg *admin_pb.SetDefaultPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordlessRegistrationMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetLoginTextToDomain(req *admin_pb.SetCustomLoginTextsRequest) *domain.CustomLoginText {
langTag := language.Make(req.Language)
result := &domain.CustomLoginText{
@@ -108,6 +123,9 @@ func SetLoginTextToDomain(req *admin_pb.SetCustomLoginTextsRequest) *domain.Cust
result.VerifyMFAOTP = text.VerifyMFAOTPScreenTextPbToDomain(req.VerifyMfaOtpText)
result.VerifyMFAU2F = text.VerifyMFAU2FScreenTextPbToDomain(req.VerifyMfaU2FText)
result.Passwordless = text.PasswordlessScreenTextPbToDomain(req.PasswordlessText)
result.PasswordlessPrompt = text.PasswordlessPromptScreenTextPbToDomain(req.PasswordlessPromptText)
result.PasswordlessRegistration = text.PasswordlessRegistrationScreenTextPbToDomain(req.PasswordlessRegistrationText)
result.PasswordlessRegistrationDone = text.PasswordlessRegistrationDoneScreenTextPbToDomain(req.PasswordlessRegistrationDoneText)
result.PasswordChange = text.PasswordChangeScreenTextPbToDomain(req.PasswordChangeText)
result.PasswordChangeDone = text.PasswordChangeDoneScreenTextPbToDomain(req.PasswordChangeDoneText)
result.PasswordResetDone = text.PasswordResetDoneScreenTextPbToDomain(req.PasswordResetDoneText)

View File

@@ -3,6 +3,8 @@ package auth
import (
"context"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/object"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
@@ -38,6 +40,30 @@ func (s *Server) AddMyPasswordless(ctx context.Context, _ *auth_pb.AddMyPassword
}, nil
}
func (s *Server) AddMyPasswordlessLink(ctx context.Context, _ *auth_pb.AddMyPasswordlessLinkRequest) (*auth_pb.AddMyPasswordlessLinkResponse, error) {
ctxData := authz.GetCtxData(ctx)
initCode, err := s.command.HumanAddPasswordlessInitCode(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.AddMyPasswordlessLinkResponse{
Details: object.AddToDetailsPb(initCode.Sequence, initCode.ChangeDate, initCode.ResourceOwner),
Link: initCode.Link(s.defaults.Notifications.Endpoints.PasswordlessRegistration),
Expiration: durationpb.New(initCode.Expiration),
}, nil
}
func (s *Server) SendMyPasswordlessLink(ctx context.Context, _ *auth_pb.SendMyPasswordlessLinkRequest) (*auth_pb.SendMyPasswordlessLinkResponse, error) {
ctxData := authz.GetCtxData(ctx)
initCode, err := s.command.HumanSendPasswordlessInitCode(ctx, ctxData.UserID, ctxData.ResourceOwner)
if err != nil {
return nil, err
}
return &auth_pb.SendMyPasswordlessLinkResponse{
Details: object.AddToDetailsPb(initCode.Sequence, initCode.ChangeDate, initCode.ResourceOwner),
}, nil
}
func (s *Server) VerifyMyPasswordless(ctx context.Context, req *auth_pb.VerifyMyPasswordlessRequest) (*auth_pb.VerifyMyPasswordlessResponse, error) {
ctxData := authz.GetCtxData(ctx)
objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, ctxData.UserID, ctxData.ResourceOwner, req.Verification.TokenName, "", req.Verification.PublicKeyCredential)

View File

@@ -8,6 +8,7 @@ import (
"github.com/caos/zitadel/internal/auth/repository"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/pkg/grpc/auth"
)
@@ -20,20 +21,22 @@ const (
type Server struct {
auth.UnimplementedAuthServiceServer
command *command.Commands
query *query.Queries
repo repository.Repository
command *command.Commands
query *query.Queries
repo repository.Repository
defaults systemdefaults.SystemDefaults
}
type Config struct {
Repository eventsourcing.Config
}
func CreateServer(command *command.Commands, query *query.Queries, authRepo repository.Repository) *Server {
func CreateServer(command *command.Commands, query *query.Queries, authRepo repository.Repository, defaults systemdefaults.SystemDefaults) *Server {
return &Server{
command: command,
query: query,
repo: authRepo,
command: command,
query: query,
repo: authRepo,
defaults: defaults,
}
}

View File

@@ -252,6 +252,54 @@ func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Contex
}, nil
}
func (s *Server) GetCustomPasswordlessRegistrationMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordlessRegistrationMessageTextRequest) (*mgmt_pb.GetCustomPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.org.GetMessageText(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordlessRegistrationMessageType, req.Language)
if err != nil {
return nil, err
}
return &mgmt_pb.GetCustomPasswordlessRegistrationMessageTextResponse{
CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
func (s *Server) GetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *mgmt_pb.GetDefaultPasswordlessRegistrationMessageTextRequest) (*mgmt_pb.GetDefaultPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.org.GetDefaultMessageText(ctx, domain.PasswordlessRegistrationMessageType, req.Language)
if err != nil {
return nil, err
}
return &mgmt_pb.GetDefaultPasswordlessRegistrationMessageTextResponse{
CustomText: text_grpc.DomainCustomMsgTextToPb(msg),
}, nil
}
func (s *Server) SetCustomPasswordlessRegistrationMessageCustomText(ctx context.Context, req *mgmt_pb.SetCustomPasswordlessRegistrationMessageTextRequest) (*mgmt_pb.SetCustomPasswordlessRegistrationMessageTextResponse, error) {
result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetPasswordlessRegistrationCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &mgmt_pb.SetCustomPasswordlessRegistrationMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordlessRegistrationMessageTextToDefault(ctx context.Context, req *mgmt_pb.ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest) (*mgmt_pb.ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveOrgMessageTexts(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordlessRegistrationMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &mgmt_pb.ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetCustomLoginTexts(ctx context.Context, req *mgmt_pb.GetCustomLoginTextsRequest) (*mgmt_pb.GetCustomLoginTextsResponse, error) {
msg, err := s.org.GetLoginTexts(ctx, authz.GetCtxData(ctx).OrgID, req.Language)
if err != nil {

View File

@@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *mgmt_pb.SetCustomDomainClaimedMessa
}
}
func SetPasswordlessRegistrationCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordlessRegistrationMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetLoginCustomTextToDomain(req *mgmt_pb.SetCustomLoginTextsRequest) *domain.CustomLoginText {
langTag := language.Make(req.Language)
result := &domain.CustomLoginText{
@@ -107,6 +122,8 @@ func SetLoginCustomTextToDomain(req *mgmt_pb.SetCustomLoginTextsRequest) *domain
result.VerifyMFAOTP = text.VerifyMFAOTPScreenTextPbToDomain(req.VerifyMfaOtpText)
result.VerifyMFAU2F = text.VerifyMFAU2FScreenTextPbToDomain(req.VerifyMfaU2FText)
result.Passwordless = text.PasswordlessScreenTextPbToDomain(req.PasswordlessText)
result.PasswordlessRegistration = text.PasswordlessRegistrationScreenTextPbToDomain(req.PasswordlessRegistrationText)
result.PasswordlessRegistrationDone = text.PasswordlessRegistrationDoneScreenTextPbToDomain(req.PasswordlessRegistrationDoneText)
result.PasswordChange = text.PasswordChangeScreenTextPbToDomain(req.PasswordChangeText)
result.PasswordChangeDone = text.PasswordChangeDoneScreenTextPbToDomain(req.PasswordChangeDoneText)
result.PasswordResetDone = text.PasswordResetDoneScreenTextPbToDomain(req.PasswordResetDoneText)

View File

@@ -3,6 +3,8 @@ package management
import (
"context"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/authn"
change_grpc "github.com/caos/zitadel/internal/api/grpc/change"
@@ -92,18 +94,26 @@ func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequ
}
func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUserRequest) (*mgmt_pb.ImportHumanUserResponse, error) {
human, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, ImportHumanUserRequestToDomain(req))
human, passwordless := ImportHumanUserRequestToDomain(req)
addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless)
if err != nil {
return nil, err
}
return &mgmt_pb.ImportHumanUserResponse{
UserId: human.AggregateID,
resp := &mgmt_pb.ImportHumanUserResponse{
UserId: addedHuman.AggregateID,
Details: obj_grpc.AddToDetailsPb(
human.Sequence,
human.ChangeDate,
human.ResourceOwner,
addedHuman.Sequence,
addedHuman.ChangeDate,
addedHuman.ResourceOwner,
),
}, nil
}
if code != nil {
resp.PasswordlessRegistration = &mgmt_pb.ImportHumanUserResponse_PasswordlessRegistration{
Link: code.Link(s.systemDefaults.Notifications.Endpoints.PasswordlessRegistration),
Lifetime: durationpb.New(code.Expiration),
}
}
return resp, nil
}
func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) {
@@ -408,6 +418,17 @@ func (s *Server) ListHumanPasswordless(ctx context.Context, req *mgmt_pb.ListHum
}, nil
}
func (s *Server) SendPasswordlessRegistration(ctx context.Context, req *mgmt_pb.SendPasswordlessRegistrationRequest) (*mgmt_pb.SendPasswordlessRegistrationResponse, error) {
ctxData := authz.GetCtxData(ctx)
initCode, err := s.command.HumanSendPasswordlessInitCode(ctx, req.UserId, ctxData.OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.SendPasswordlessRegistrationResponse{
Details: object.AddToDetailsPb(initCode.Sequence, initCode.ChangeDate, initCode.ResourceOwner),
}, nil
}
func (s *Server) RemoveHumanPasswordless(ctx context.Context, req *mgmt_pb.RemoveHumanPasswordlessRequest) (*mgmt_pb.RemoveHumanPasswordlessResponse, error) {
objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.UserId, req.TokenId, authz.GetCtxData(ctx).OrgID)
if err != nil {

View File

@@ -69,13 +69,13 @@ func AddHumanUserRequestToDomain(req *mgmt_pb.AddHumanUserRequest) *domain.Human
return h
}
func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) *domain.Human {
h := &domain.Human{
func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human *domain.Human, passwordless bool) {
human = &domain.Human{
Username: req.UserName,
}
preferredLanguage, err := language.Parse(req.Profile.PreferredLanguage)
logging.Log("MANAG-3GUFJ").OnError(err).Debug("language malformed")
h.Profile = &domain.Profile{
human.Profile = &domain.Profile{
FirstName: req.Profile.FirstName,
LastName: req.Profile.LastName,
NickName: req.Profile.NickName,
@@ -83,22 +83,22 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) *domain
PreferredLanguage: preferredLanguage,
Gender: user_grpc.GenderToDomain(req.Profile.Gender),
}
h.Email = &domain.Email{
human.Email = &domain.Email{
EmailAddress: req.Email.Email,
IsEmailVerified: req.Email.IsEmailVerified,
}
if req.Phone != nil {
h.Phone = &domain.Phone{
human.Phone = &domain.Phone{
PhoneNumber: req.Phone.Phone,
IsPhoneVerified: req.Phone.IsPhoneVerified,
}
}
if req.Password != "" {
h.Password = &domain.Password{SecretString: req.Password}
h.Password.ChangeRequired = req.PasswordChangeRequired
human.Password = &domain.Password{SecretString: req.Password}
human.Password.ChangeRequired = req.PasswordChangeRequired
}
return h
return human, req.RequestPasswordlessRegistration
}
func AddMachineUserRequestToDomain(req *mgmt_pb.AddMachineUserRequest) *domain.Machine {

View File

@@ -32,36 +32,39 @@ func CustomLoginTextToPb(text *domain.CustomLoginText) *text_pb.LoginCustomText
text.ChangeDate,
text.AggregateID,
),
SelectAccountText: SelectAccountScreenToPb(text.SelectAccount),
LoginText: LoginScreenTextToPb(text.Login),
PasswordText: PasswordScreenTextToPb(text.Password),
UsernameChangeText: UsernameChangeScreenTextToPb(text.UsernameChange),
UsernameChangeDoneText: UsernameChangeDoneScreenTextToPb(text.UsernameChangeDone),
InitPasswordText: InitPasswordScreenTextToPb(text.InitPassword),
InitPasswordDoneText: InitPasswordDoneScreenTextToPb(text.InitPasswordDone),
EmailVerificationText: EmailVerificationScreenTextToPb(text.EmailVerification),
EmailVerificationDoneText: EmailVerificationDoneScreenTextToPb(text.EmailVerificationDone),
InitializeUserText: InitializeUserScreenTextToPb(text.InitUser),
InitializeDoneText: InitializeUserDoneScreenTextToPb(text.InitUserDone),
InitMfaPromptText: InitMFAPromptScreenTextToPb(text.InitMFAPrompt),
InitMfaOtpText: InitMFAOTPScreenTextToPb(text.InitMFAOTP),
InitMfaU2FText: InitMFAU2FScreenTextToPb(text.InitMFAU2F),
InitMfaDoneText: InitMFADoneScreenTextToPb(text.InitMFADone),
MfaProvidersText: MFAProvidersTextToPb(text.MFAProvider),
VerifyMfaOtpText: VerifyMFAOTPScreenTextToPb(text.VerifyMFAOTP),
VerifyMfaU2FText: VerifyMFAU2FScreenTextToPb(text.VerifyMFAU2F),
PasswordlessText: PasswordlessScreenTextToPb(text.Passwordless),
PasswordChangeText: PasswordChangeScreenTextToPb(text.PasswordChange),
PasswordChangeDoneText: PasswordChangeDoneScreenTextToPb(text.PasswordChangeDone),
PasswordResetDoneText: PasswordResetDoneScreenTextToPb(text.PasswordResetDone),
RegistrationOptionText: RegistrationOptionScreenTextToPb(text.RegisterOption),
RegistrationUserText: RegistrationUserScreenTextToPb(text.RegistrationUser),
RegistrationOrgText: RegistrationOrgScreenTextToPb(text.RegistrationOrg),
LinkingUserDoneText: LinkingUserDoneScreenTextToPb(text.LinkingUsersDone),
ExternalUserNotFoundText: ExternalUserNotFoundScreenTextToPb(text.ExternalNotFoundOption),
SuccessLoginText: SuccessLoginScreenTextToPb(text.LoginSuccess),
LogoutText: LogoutDoneScreenTextToPb(text.LogoutDone),
FooterText: FooterTextToPb(text.Footer),
SelectAccountText: SelectAccountScreenToPb(text.SelectAccount),
LoginText: LoginScreenTextToPb(text.Login),
PasswordText: PasswordScreenTextToPb(text.Password),
UsernameChangeText: UsernameChangeScreenTextToPb(text.UsernameChange),
UsernameChangeDoneText: UsernameChangeDoneScreenTextToPb(text.UsernameChangeDone),
InitPasswordText: InitPasswordScreenTextToPb(text.InitPassword),
InitPasswordDoneText: InitPasswordDoneScreenTextToPb(text.InitPasswordDone),
EmailVerificationText: EmailVerificationScreenTextToPb(text.EmailVerification),
EmailVerificationDoneText: EmailVerificationDoneScreenTextToPb(text.EmailVerificationDone),
InitializeUserText: InitializeUserScreenTextToPb(text.InitUser),
InitializeDoneText: InitializeUserDoneScreenTextToPb(text.InitUserDone),
InitMfaPromptText: InitMFAPromptScreenTextToPb(text.InitMFAPrompt),
InitMfaOtpText: InitMFAOTPScreenTextToPb(text.InitMFAOTP),
InitMfaU2FText: InitMFAU2FScreenTextToPb(text.InitMFAU2F),
InitMfaDoneText: InitMFADoneScreenTextToPb(text.InitMFADone),
MfaProvidersText: MFAProvidersTextToPb(text.MFAProvider),
VerifyMfaOtpText: VerifyMFAOTPScreenTextToPb(text.VerifyMFAOTP),
VerifyMfaU2FText: VerifyMFAU2FScreenTextToPb(text.VerifyMFAU2F),
PasswordlessText: PasswordlessScreenTextToPb(text.Passwordless),
PasswordlessPromptText: PasswordlessPromptScreenTextToPb(text.PasswordlessPrompt),
PasswordlessRegistrationText: PasswordlessRegistrationScreenTextToPb(text.PasswordlessRegistration),
PasswordlessRegistrationDoneText: PasswordlessRegistrationDoneScreenTextToPb(text.PasswordlessRegistrationDone),
PasswordChangeText: PasswordChangeScreenTextToPb(text.PasswordChange),
PasswordChangeDoneText: PasswordChangeDoneScreenTextToPb(text.PasswordChangeDone),
PasswordResetDoneText: PasswordResetDoneScreenTextToPb(text.PasswordResetDone),
RegistrationOptionText: RegistrationOptionScreenTextToPb(text.RegisterOption),
RegistrationUserText: RegistrationUserScreenTextToPb(text.RegistrationUser),
RegistrationOrgText: RegistrationOrgScreenTextToPb(text.RegistrationOrg),
LinkingUserDoneText: LinkingUserDoneScreenTextToPb(text.LinkingUsersDone),
ExternalUserNotFoundText: ExternalUserNotFoundScreenTextToPb(text.ExternalNotFoundOption),
SuccessLoginText: SuccessLoginScreenTextToPb(text.LoginSuccess),
LogoutText: LogoutDoneScreenTextToPb(text.LogoutDone),
FooterText: FooterTextToPb(text.Footer),
}
}
@@ -272,6 +275,36 @@ func PasswordlessScreenTextToPb(text domain.PasswordlessScreenText) *text_pb.Pas
}
}
func PasswordlessPromptScreenTextToPb(text domain.PasswordlessPromptScreenText) *text_pb.PasswordlessPromptScreenText {
return &text_pb.PasswordlessPromptScreenText{
Title: text.Title,
Description: text.Description,
DescriptionInit: text.DescriptionInit,
PasswordlessButtonText: text.PasswordlessButtonText,
NextButtonText: text.NextButtonText,
SkipButtonText: text.SkipButtonText,
}
}
func PasswordlessRegistrationScreenTextToPb(text domain.PasswordlessRegistrationScreenText) *text_pb.PasswordlessRegistrationScreenText {
return &text_pb.PasswordlessRegistrationScreenText{
Title: text.Title,
Description: text.Description,
RegisterTokenButtonText: text.RegisterTokenButtonText,
TokenNameLabel: text.TokenNameLabel,
NotSupported: text.NotSupported,
ErrorRetry: text.ErrorRetry,
}
}
func PasswordlessRegistrationDoneScreenTextToPb(text domain.PasswordlessRegistrationDoneScreenText) *text_pb.PasswordlessRegistrationDoneScreenText {
return &text_pb.PasswordlessRegistrationDoneScreenText{
Title: text.Title,
Description: text.Description,
NextButtonText: text.NextButtonText,
}
}
func PasswordChangeScreenTextToPb(text domain.PasswordChangeScreenText) *text_pb.PasswordChangeScreenText {
return &text_pb.PasswordChangeScreenText{
Title: text.Title,
@@ -660,6 +693,45 @@ func PasswordlessScreenTextPbToDomain(text *text_pb.PasswordlessScreenText) doma
}
}
func PasswordlessPromptScreenTextPbToDomain(text *text_pb.PasswordlessPromptScreenText) domain.PasswordlessPromptScreenText {
if text == nil {
return domain.PasswordlessPromptScreenText{}
}
return domain.PasswordlessPromptScreenText{
Title: text.Title,
Description: text.Description,
DescriptionInit: text.DescriptionInit,
PasswordlessButtonText: text.PasswordlessButtonText,
NextButtonText: text.NextButtonText,
SkipButtonText: text.SkipButtonText,
}
}
func PasswordlessRegistrationScreenTextPbToDomain(text *text_pb.PasswordlessRegistrationScreenText) domain.PasswordlessRegistrationScreenText {
if text == nil {
return domain.PasswordlessRegistrationScreenText{}
}
return domain.PasswordlessRegistrationScreenText{
Title: text.Title,
Description: text.Description,
RegisterTokenButtonText: text.RegisterTokenButtonText,
TokenNameLabel: text.TokenNameLabel,
NotSupported: text.NotSupported,
ErrorRetry: text.ErrorRetry,
}
}
func PasswordlessRegistrationDoneScreenTextPbToDomain(text *text_pb.PasswordlessRegistrationDoneScreenText) domain.PasswordlessRegistrationDoneScreenText {
if text == nil {
return domain.PasswordlessRegistrationDoneScreenText{}
}
return domain.PasswordlessRegistrationDoneScreenText{
Title: text.Title,
Description: text.Description,
NextButtonText: text.NextButtonText,
}
}
func PasswordChangeScreenTextPbToDomain(text *text_pb.PasswordChangeScreenText) domain.PasswordChangeScreenText {
if text == nil {
return domain.PasswordChangeScreenText{}

View File

@@ -22,6 +22,10 @@ type AuthRequestRepository interface {
VerifyMFAOTP(ctx context.Context, authRequestID, userID, resourceOwner, code, userAgentID string, info *domain.BrowserInfo) error
BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (*domain.WebAuthNLogin, error)
VerifyMFAU2F(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string, credentialData []byte, info *domain.BrowserInfo) error
BeginPasswordlessSetup(ctx context.Context, userID, resourceOwner string) (login *domain.WebAuthNToken, err error)
VerifyPasswordlessSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName string, credentialData []byte) (err error)
BeginPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, codeID, verificationCode string) (login *domain.WebAuthNToken, err error)
VerifyPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode string, credentialData []byte) (err error)
BeginPasswordlessLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (*domain.WebAuthNLogin, error)
VerifyPasswordless(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string, credentialData []byte, info *domain.BrowserInfo) error

View File

@@ -296,6 +296,32 @@ func (repo *AuthRequestRepo) VerifyMFAU2F(ctx context.Context, userID, resourceO
return repo.Command.HumanFinishU2FLogin(ctx, userID, resourceOwner, credentialData, request, true)
}
func (repo *AuthRequestRepo) BeginPasswordlessSetup(ctx context.Context, userID, resourceOwner string) (login *domain.WebAuthNToken, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return repo.Command.HumanAddPasswordlessSetup(ctx, userID, resourceOwner, true)
}
func (repo *AuthRequestRepo) VerifyPasswordlessSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName string, credentialData []byte) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
_, err = repo.Command.HumanHumanPasswordlessSetup(ctx, userID, resourceOwner, tokenName, userAgentID, credentialData)
return err
}
func (repo *AuthRequestRepo) BeginPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, codeID, verificationCode string) (login *domain.WebAuthNToken, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return repo.Command.HumanAddPasswordlessSetupInitCode(ctx, userID, resourceOwner, codeID, verificationCode)
}
func (repo *AuthRequestRepo) VerifyPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode string, credentialData []byte) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
_, err = repo.Command.HumanPasswordlessSetupInitCode(ctx, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode, credentialData)
return err
}
func (repo *AuthRequestRepo) BeginPasswordlessLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (login *domain.WebAuthNLogin, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -610,7 +636,6 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
if request.LinkingUsers != nil && len(request.LinkingUsers) != 0 {
return append(steps, &domain.LinkUsersStep{}), nil
}
//PLANNED: consent step
@@ -657,10 +682,16 @@ func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, use
request.AuthTime = userSession.PasswordlessVerification
return nil
}
step = &domain.PasswordlessStep{}
step = &domain.PasswordlessStep{
PasswordSet: user.PasswordSet,
}
}
if !user.PasswordSet {
if user.PasswordlessInitRequired {
return &domain.PasswordlessRegistrationPromptStep{}
}
if user.PasswordInitRequired {
return &domain.InitPasswordStep{}
}

View File

@@ -6,6 +6,7 @@ import (
"testing"
"time"
"github.com/caos/zitadel/internal/crypto"
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
@@ -131,14 +132,16 @@ func (m *mockEventErrUser) BulkAddExternalIDPs(ctx context.Context, userID strin
}
type mockViewUser struct {
InitRequired bool
PasswordSet bool
PasswordChangeRequired bool
IsEmailVerified bool
OTPState int32
MFAMaxSetUp int32
MFAInitSkipped time.Time
PasswordlessTokens user_view_model.WebAuthNTokens
InitRequired bool
PasswordInitRequired bool
PasswordSet bool
PasswordChangeRequired bool
IsEmailVerified bool
OTPState int32
MFAMaxSetUp int32
MFAInitSkipped time.Time
PasswordlessInitRequired bool
PasswordlessTokens user_view_model.WebAuthNTokens
}
type mockLoginPolicy struct {
@@ -154,15 +157,17 @@ func (m *mockViewUser) UserByID(string) (*user_view_model.UserView, error) {
State: int32(user_model.UserStateActive),
UserName: "UserName",
HumanView: &user_view_model.HumanView{
FirstName: "FirstName",
InitRequired: m.InitRequired,
PasswordSet: m.PasswordSet,
PasswordChangeRequired: m.PasswordChangeRequired,
IsEmailVerified: m.IsEmailVerified,
OTPState: m.OTPState,
MFAMaxSetUp: m.MFAMaxSetUp,
MFAInitSkipped: m.MFAInitSkipped,
PasswordlessTokens: m.PasswordlessTokens,
FirstName: "FirstName",
InitRequired: m.InitRequired,
PasswordInitRequired: m.PasswordInitRequired,
PasswordSet: m.PasswordSet,
PasswordChangeRequired: m.PasswordChangeRequired,
IsEmailVerified: m.IsEmailVerified,
OTPState: m.OTPState,
MFAMaxSetUp: m.MFAMaxSetUp,
MFAInitSkipped: m.MFAInitSkipped,
PasswordlessInitRequired: m.PasswordlessInitRequired,
PasswordlessTokens: m.PasswordlessTokens,
},
}, nil
}
@@ -486,7 +491,37 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
nil,
},
{
"passwordless not verified, passwordless check step",
"passwordless not initialised, passwordless prompt step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
PasswordlessInitRequired: true,
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
MultiFactorCheckLifeTime: 10 * time.Hour,
},
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false},
[]domain.NextStep{&domain.PasswordlessRegistrationPromptStep{}},
nil,
},
{
"passwordless not verified, no password set, passwordless check step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
PasswordlessTokens: user_view_model.WebAuthNTokens{&user_view_model.WebAuthNView{ID: "id", State: int32(user_model.MFAStateReady)}},
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
MultiFactorCheckLifeTime: 10 * time.Hour,
},
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false},
[]domain.NextStep{&domain.PasswordlessStep{}},
nil,
},
{
"passwordless not verified, passwordless check step, downgrade possible",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
@@ -498,7 +533,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
MultiFactorCheckLifeTime: 10 * time.Hour,
},
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false},
[]domain.NextStep{&domain.PasswordlessStep{}},
[]domain.NextStep{&domain.PasswordlessStep{PasswordSet: true}},
nil,
},
{
@@ -533,9 +568,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
"password not set, init password step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
userViewProvider: &mockViewUser{
PasswordInitRequired: true,
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
},
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
[]domain.NextStep{&domain.InitPasswordStep{}},
@@ -1510,6 +1547,7 @@ func Test_userByID(t *testing.T) {
"new user events, new view model state",
args{
viewProvider: &mockViewUser{
PasswordSet: true,
PasswordChangeRequired: true,
},
eventProvider: &mockEventUser{
@@ -1518,7 +1556,7 @@ func Test_userByID(t *testing.T) {
Type: user_es_model.UserPasswordChanged,
CreationDate: time.Now().UTC().Round(1 * time.Second),
Data: func() []byte {
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false})
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false, Secret: &crypto.CryptoValue{}})
return data
}(),
},
@@ -1529,6 +1567,7 @@ func Test_userByID(t *testing.T) {
State: user_model.UserStateActive,
UserName: "UserName",
HumanView: &user_model.HumanView{
PasswordSet: true,
PasswordChangeRequired: false,
PasswordChanged: time.Now().UTC().Round(1 * time.Second),
FirstName: "FirstName",

View File

@@ -105,7 +105,7 @@ func (repo *OrgRepository) GetMyPasswordComplexityPolicy(ctx context.Context) (*
return iam_view_model.PasswordComplexityViewToModel(policy), err
}
func (repo *OrgRepository) GetLabelPolicy(ctx context.Context, orgID string) (*iam_model.LabelPolicyView, error) {
func (repo *OrgRepository) GetLabelPolicy(ctx context.Context, orgID string) (*domain.LabelPolicy, error) {
orgPolicy, err := repo.View.LabelPolicyByAggregateIDAndState(orgID, int32(domain.LabelPolicyStateActive))
if errors.IsNotFound(err) {
orgPolicy, err = repo.View.LabelPolicyByAggregateIDAndState(repo.SystemDefaults.IamID, int32(domain.LabelPolicyStateActive))
@@ -113,7 +113,19 @@ func (repo *OrgRepository) GetLabelPolicy(ctx context.Context, orgID string) (*i
if err != nil {
return nil, err
}
return iam_view_model.LabelPolicyViewToModel(orgPolicy), nil
return orgPolicy.ToDomain(), nil
}
func (repo *OrgRepository) GetLoginText(ctx context.Context, orgID string) ([]*domain.CustomText, error) {
loginTexts, err := repo.View.CustomTextsByAggregateIDAndTemplate(domain.IAMID, domain.LoginCustomText)
if err != nil {
return nil, err
}
orgLoginTexts, err := repo.View.CustomTextsByAggregateIDAndTemplate(orgID, domain.LoginCustomText)
if err != nil {
return nil, err
}
return append(iam_view_model.CustomTextViewsToDomain(loginTexts), iam_view_model.CustomTextViewsToDomain(orgLoginTexts)...), nil
}
func (repo *OrgRepository) GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) {

View File

@@ -16,6 +16,7 @@ import (
org_model "github.com/caos/zitadel/internal/org/model"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/org/repository/view"
user_repo "github.com/caos/zitadel/internal/repository/user"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
)
@@ -142,7 +143,9 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
es_model.HumanPasswordlessTokenRemoved,
es_model.HumanMFAInitSkipped,
es_model.MachineChanged,
es_model.HumanPasswordChanged:
es_model.HumanPasswordChanged,
es_models.EventType(user_repo.HumanPasswordlessInitCodeAddedType),
es_models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType):
user, err = u.view.UserByID(event.AggregateID)
if err != nil {
return err

View File

@@ -2,6 +2,7 @@ package repository
import (
"context"
"github.com/caos/zitadel/internal/domain"
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
)
@@ -12,6 +13,7 @@ type OrgRepository interface {
GetDefaultOrgIAMPolicy(ctx context.Context) (*iam_model.OrgIAMPolicyView, error)
GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error)
GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error)
GetLabelPolicy(ctx context.Context, orgID string) (*iam_model.LabelPolicyView, error)
GetLabelPolicy(ctx context.Context, orgID string) (*domain.LabelPolicy, error)
GetLoginText(ctx context.Context, orgID string) ([]*domain.CustomText, error)
GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error)
}

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()
}

View File

@@ -39,6 +39,7 @@ type SecretGenerators struct {
EmailVerificationCode crypto.GeneratorConfig
PhoneVerificationCode crypto.GeneratorConfig
PasswordVerificationCode crypto.GeneratorConfig
PasswordlessInitCode crypto.GeneratorConfig
MachineKeySize uint32
ApplicationKeySize uint32
}
@@ -73,10 +74,11 @@ type Notifications struct {
}
type Endpoints struct {
InitCode string
PasswordReset string
VerifyEmail string
DomainClaimed string
InitCode string
PasswordReset string
VerifyEmail string
DomainClaimed string
PasswordlessRegistration string
}
type Providers struct {

View File

@@ -159,6 +159,27 @@ const (
LoginKeyPasswordlessNotSupported = LoginKeyPasswordless + "NotSupported"
LoginKeyPasswordlessErrorRetry = LoginKeyPasswordless + "ErrorRetry"
LoginKeyPasswordlessPrompt = "PasswordlessPrompt."
LoginKeyPasswordlessPromptTitle = LoginKeyPasswordlessPrompt + "Title"
LoginKeyPasswordlessPromptDescription = LoginKeyPasswordlessPrompt + "Description"
LoginKeyPasswordlessPromptDescriptionInit = LoginKeyPasswordlessPrompt + "DescriptionInit"
LoginKeyPasswordlessPromptPasswordlessButtonText = LoginKeyPasswordlessPrompt + "PasswordlessButtonText"
LoginKeyPasswordlessPromptNextButtonText = LoginKeyPasswordlessPrompt + "NextButtonText"
LoginKeyPasswordlessPromptSkipButtonText = LoginKeyPasswordlessPrompt + "SkipButtonText"
LoginKeyPasswordlessRegistration = "PasswordlessRegistration."
LoginKeyPasswordlessRegistrationTitle = LoginKeyPasswordlessRegistration + "Title"
LoginKeyPasswordlessRegistrationDescription = LoginKeyPasswordlessRegistration + "Description"
LoginKeyPasswordlessRegistrationRegisterTokenButtonText = LoginKeyPasswordlessRegistration + "RegisterTokenButtonText"
LoginKeyPasswordlessRegistrationTokenNameLabel = LoginKeyPasswordlessRegistration + "TokenNameLabel"
LoginKeyPasswordlessRegistrationNotSupported = LoginKeyPasswordlessRegistration + "NotSupported"
LoginKeyPasswordlessRegistrationErrorRetry = LoginKeyPasswordlessRegistration + "ErrorRetry"
LoginKeyPasswordlessRegistrationDone = "PasswordlessRegistrationDone."
LoginKeyPasswordlessRegistrationDoneTitle = LoginKeyPasswordlessRegistrationDone + "Title"
LoginKeyPasswordlessRegistrationDoneDescription = LoginKeyPasswordlessRegistrationDone + "Description"
LoginKeyPasswordlessRegistrationDoneNextButtonText = LoginKeyPasswordlessRegistrationDone + "NextButtonText"
LoginKeyPasswordChange = "PasswordChange."
LoginKeyPasswordChangeTitle = LoginKeyPasswordChange + "Title"
LoginKeyPasswordChangeDescription = LoginKeyPasswordChange + "Description"
@@ -258,36 +279,39 @@ type CustomLoginText struct {
Default bool
Language language.Tag
SelectAccount SelectAccountScreenText
Login LoginScreenText
Password PasswordScreenText
UsernameChange UsernameChangeScreenText
UsernameChangeDone UsernameChangeDoneScreenText
InitPassword InitPasswordScreenText
InitPasswordDone InitPasswordDoneScreenText
EmailVerification EmailVerificationScreenText
EmailVerificationDone EmailVerificationDoneScreenText
InitUser InitializeUserScreenText
InitUserDone InitializeUserDoneScreenText
InitMFAPrompt InitMFAPromptScreenText
InitMFAOTP InitMFAOTPScreenText
InitMFAU2F InitMFAU2FScreenText
InitMFADone InitMFADoneScreenText
MFAProvider MFAProvidersText
VerifyMFAOTP VerifyMFAOTPScreenText
VerifyMFAU2F VerifyMFAU2FScreenText
Passwordless PasswordlessScreenText
PasswordChange PasswordChangeScreenText
PasswordChangeDone PasswordChangeDoneScreenText
PasswordResetDone PasswordResetDoneScreenText
RegisterOption RegistrationOptionScreenText
RegistrationUser RegistrationUserScreenText
RegistrationOrg RegistrationOrgScreenText
LinkingUsersDone LinkingUserDoneScreenText
ExternalNotFoundOption ExternalUserNotFoundScreenText
LoginSuccess SuccessLoginScreenText
LogoutDone LogoutDoneScreenText
Footer FooterText
SelectAccount SelectAccountScreenText
Login LoginScreenText
Password PasswordScreenText
UsernameChange UsernameChangeScreenText
UsernameChangeDone UsernameChangeDoneScreenText
InitPassword InitPasswordScreenText
InitPasswordDone InitPasswordDoneScreenText
EmailVerification EmailVerificationScreenText
EmailVerificationDone EmailVerificationDoneScreenText
InitUser InitializeUserScreenText
InitUserDone InitializeUserDoneScreenText
InitMFAPrompt InitMFAPromptScreenText
InitMFAOTP InitMFAOTPScreenText
InitMFAU2F InitMFAU2FScreenText
InitMFADone InitMFADoneScreenText
MFAProvider MFAProvidersText
VerifyMFAOTP VerifyMFAOTPScreenText
VerifyMFAU2F VerifyMFAU2FScreenText
Passwordless PasswordlessScreenText
PasswordlessPrompt PasswordlessPromptScreenText
PasswordlessRegistration PasswordlessRegistrationScreenText
PasswordlessRegistrationDone PasswordlessRegistrationDoneScreenText
PasswordChange PasswordChangeScreenText
PasswordChangeDone PasswordChangeDoneScreenText
PasswordResetDone PasswordResetDoneScreenText
RegisterOption RegistrationOptionScreenText
RegistrationUser RegistrationUserScreenText
RegistrationOrg RegistrationOrgScreenText
LinkingUsersDone LinkingUserDoneScreenText
ExternalNotFoundOption ExternalUserNotFoundScreenText
LoginSuccess SuccessLoginScreenText
LogoutDone LogoutDoneScreenText
Footer FooterText
}
func (m *CustomLoginText) IsValid() bool {
@@ -564,3 +588,27 @@ type FooterText struct {
Help string
HelpLink string
}
type PasswordlessPromptScreenText struct {
Title string
Description string
DescriptionInit string
PasswordlessButtonText string
NextButtonText string
SkipButtonText string
}
type PasswordlessRegistrationScreenText struct {
Title string
Description string
RegisterTokenButtonText string
TokenNameLabel string
NotSupported string
ErrorRetry string
}
type PasswordlessRegistrationDoneScreenText struct {
Title string
Description string
NextButtonText string
}

View File

@@ -7,26 +7,28 @@ import (
)
const (
InitCodeMessageType = "InitCode"
PasswordResetMessageType = "PasswordReset"
VerifyEmailMessageType = "VerifyEmail"
VerifyPhoneMessageType = "VerifyPhone"
DomainClaimedMessageType = "DomainClaimed"
MessageTitle = "Title"
MessagePreHeader = "PreHeader"
MessageSubject = "Subject"
MessageGreeting = "Greeting"
MessageText = "Text"
MessageButtonText = "ButtonText"
MessageFooterText = "Footer"
InitCodeMessageType = "InitCode"
PasswordResetMessageType = "PasswordReset"
VerifyEmailMessageType = "VerifyEmail"
VerifyPhoneMessageType = "VerifyPhone"
DomainClaimedMessageType = "DomainClaimed"
PasswordlessRegistrationMessageType = "PasswordlessRegistration"
MessageTitle = "Title"
MessagePreHeader = "PreHeader"
MessageSubject = "Subject"
MessageGreeting = "Greeting"
MessageText = "Text"
MessageButtonText = "ButtonText"
MessageFooterText = "Footer"
)
type MessageTexts struct {
InitCode CustomMessageText
PasswordReset CustomMessageText
VerifyEmail CustomMessageText
VerifyPhone CustomMessageText
DomainClaimed CustomMessageText
InitCode CustomMessageText
PasswordReset CustomMessageText
VerifyEmail CustomMessageText
VerifyPhone CustomMessageText
DomainClaimed CustomMessageText
PasswordlessRegistration CustomMessageText
}
type CustomMessageText struct {
@@ -61,6 +63,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *CustomMessageText {
return &m.VerifyPhone
case DomainClaimedMessageType:
return &m.DomainClaimed
case PasswordlessRegistrationMessageType:
return &m.PasswordlessRegistration
}
return nil
}

View File

@@ -79,8 +79,8 @@ func (u *Human) HashPasswordIfExisting(policy *PasswordComplexityPolicy, passwor
return nil
}
func (u *Human) IsInitialState() bool {
return u.Email == nil || !u.IsEmailVerified || (u.ExternalIDPs == nil || len(u.ExternalIDPs) == 0) && (u.Password == nil || u.SecretString == "")
func (u *Human) IsInitialState(passwordless bool) bool {
return u.Email == nil || !u.IsEmailVerified || (u.ExternalIDPs == nil || len(u.ExternalIDPs) == 0) && !passwordless && (u.Password == nil || u.SecretString == "")
}
func NewInitUserCode(generator crypto.Generator) (*InitUserCode, error) {

View File

@@ -2,6 +2,9 @@ package domain
import (
"bytes"
"fmt"
"time"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
)
@@ -65,3 +68,29 @@ func GetTokenByKeyID(tokens []*WebAuthNToken, keyID []byte) (int, *WebAuthNToken
}
return -1, nil
}
type PasswordlessInitCodeState int32
const (
PasswordlessInitCodeStateUnspecified PasswordlessInitCodeState = iota
PasswordlessInitCodeStateRequested
PasswordlessInitCodeStateActive
PasswordlessInitCodeStateRemoved
)
type PasswordlessInitCode struct {
es_models.ObjectRoot
CodeID string
Code string
Expiration time.Duration
State PasswordlessInitCodeState
}
func (p *PasswordlessInitCode) Link(baseURL string) string {
return PasswordlessInitCodeLink(baseURL, p.AggregateID, p.ResourceOwner, p.CodeID, p.Code)
}
func PasswordlessInitCodeLink(baseURL, userID, resourceOwner, codeID, code string) string {
return fmt.Sprintf("%s?userID=%s&orgID=%s&codeID=%s&code=%s", baseURL, userID, resourceOwner, codeID, code)
}

View File

@@ -24,6 +24,7 @@ const (
NextStepExternalLogin
NextStepGrantRequired
NextStepPasswordless
NextStepPasswordlessRegistrationPrompt
NextStepRegistration
)
@@ -93,12 +94,20 @@ func (s *ExternalLoginStep) Type() NextStepType {
return NextStepExternalLogin
}
type PasswordlessStep struct{}
type PasswordlessStep struct {
PasswordSet bool
}
func (s *PasswordlessStep) Type() NextStepType {
return NextStepPasswordless
}
type PasswordlessRegistrationPromptStep struct{}
func (s *PasswordlessRegistrationPromptStep) Type() NextStepType {
return NextStepPasswordlessRegistrationPrompt
}
type ChangePasswordStep struct{}
func (s *ChangePasswordStep) Type() NextStepType {

View File

@@ -93,7 +93,8 @@ func (r *CustomTextView) IsMessageTemplate() bool {
r.Template == domain.PasswordResetMessageType ||
r.Template == domain.VerifyEmailMessageType ||
r.Template == domain.VerifyPhoneMessageType ||
r.Template == domain.DomainClaimedMessageType
r.Template == domain.DomainClaimedMessageType ||
r.Template == domain.PasswordlessRegistrationMessageType
}
func CustomTextViewsToMessageDomain(aggregateID, lang string, texts []*CustomTextView) *domain.CustomMessageText {
@@ -208,6 +209,15 @@ func CustomTextViewsToLoginDomain(aggregateID, lang string, texts []*CustomTextV
if strings.HasPrefix(text.Key, domain.LoginKeyPasswordless) {
passwordlessKeyToDomain(text, result)
}
if strings.HasPrefix(text.Key, domain.LoginKeyPasswordlessPrompt) {
passwordlessPromptKeyToDomain(text, result)
}
if strings.HasPrefix(text.Key, domain.LoginKeyPasswordlessRegistration) {
passwordlessRegistrationKeyToDomain(text, result)
}
if strings.HasPrefix(text.Key, domain.LoginKeyPasswordlessRegistrationDone) {
passwordlessRegistrationDoneKeyToDomain(text, result)
}
if strings.HasPrefix(text.Key, domain.LoginKeyPasswordChange) {
passwordChangeKeyToDomain(text, result)
}
@@ -638,6 +648,60 @@ func passwordlessKeyToDomain(text *CustomTextView, result *domain.CustomLoginTex
}
}
func passwordlessPromptKeyToDomain(text *CustomTextView, result *domain.CustomLoginText) {
if text.Key == domain.LoginKeyPasswordlessPromptTitle {
result.PasswordlessPrompt.Title = text.Text
}
if text.Key == domain.LoginKeyPasswordlessPromptDescription {
result.PasswordlessPrompt.Description = text.Text
}
if text.Key == domain.LoginKeyPasswordlessPromptDescriptionInit {
result.PasswordlessPrompt.DescriptionInit = text.Text
}
if text.Key == domain.LoginKeyPasswordlessPromptPasswordlessButtonText {
result.PasswordlessPrompt.PasswordlessButtonText = text.Text
}
if text.Key == domain.LoginKeyPasswordlessPromptNextButtonText {
result.PasswordlessPrompt.NextButtonText = text.Text
}
if text.Key == domain.LoginKeyPasswordlessPromptSkipButtonText {
result.PasswordlessPrompt.SkipButtonText = text.Text
}
}
func passwordlessRegistrationKeyToDomain(text *CustomTextView, result *domain.CustomLoginText) {
if text.Key == domain.LoginKeyPasswordlessRegistrationTitle {
result.PasswordlessRegistration.Title = text.Text
}
if text.Key == domain.LoginKeyPasswordlessRegistrationDescription {
result.PasswordlessRegistration.Description = text.Text
}
if text.Key == domain.LoginKeyPasswordlessRegistrationRegisterTokenButtonText {
result.PasswordlessRegistration.RegisterTokenButtonText = text.Text
}
if text.Key == domain.LoginKeyPasswordlessRegistrationTokenNameLabel {
result.PasswordlessRegistration.TokenNameLabel = text.Text
}
if text.Key == domain.LoginKeyPasswordlessRegistrationNotSupported {
result.PasswordlessRegistration.NotSupported = text.Text
}
if text.Key == domain.LoginKeyPasswordlessRegistrationErrorRetry {
result.PasswordlessRegistration.ErrorRetry = text.Text
}
}
func passwordlessRegistrationDoneKeyToDomain(text *CustomTextView, result *domain.CustomLoginText) {
if text.Key == domain.LoginKeyPasswordlessRegistrationDoneTitle {
result.PasswordlessRegistrationDone.Title = text.Text
}
if text.Key == domain.LoginKeyPasswordlessRegistrationDoneDescription {
result.PasswordlessRegistrationDone.Description = text.Text
}
if text.Key == domain.LoginKeyPasswordlessRegistrationDoneNextButtonText {
result.PasswordlessRegistrationDone.NextButtonText = text.Text
}
}
func passwordChangeKeyToDomain(text *CustomTextView, result *domain.CustomLoginText) {
if text.Key == domain.LoginKeyPasswordChangeTitle {
result.PasswordChange.Title = text.Text

View File

@@ -2,7 +2,9 @@ package handler
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1"
@@ -16,6 +18,7 @@ import (
org_model "github.com/caos/zitadel/internal/org/model"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/org/repository/view"
user_repo "github.com/caos/zitadel/internal/repository/user"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
)
@@ -139,7 +142,9 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
es_model.HumanPasswordlessTokenAdded,
es_model.HumanPasswordlessTokenVerified,
es_model.HumanPasswordlessTokenRemoved,
es_model.MachineChanged:
es_model.MachineChanged,
es_models.EventType(user_repo.HumanPasswordlessInitCodeAddedType),
es_models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType):
user, err = u.view.UserByID(event.AggregateID)
if err != nil {
return err

View File

@@ -23,6 +23,7 @@ import (
iam_model "github.com/caos/zitadel/internal/iam/model"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/view/model"
"github.com/caos/zitadel/internal/notification/types"
user_repo "github.com/caos/zitadel/internal/repository/user"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/user/repository/view"
"github.com/caos/zitadel/internal/user/repository/view/model"
@@ -124,6 +125,8 @@ func (n *Notification) Reduce(event *models.Event) (err error) {
err = n.handlePasswordCode(event)
case es_model.DomainClaimed:
err = n.handleDomainClaimed(event)
case models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType):
err = n.handlePasswordlessRegistrationLink(event)
}
if err != nil {
return err
@@ -312,6 +315,52 @@ func (n *Notification) handleDomainClaimed(event *models.Event) (err error) {
return n.command.UserDomainClaimedSent(ctx, event.ResourceOwner, event.AggregateID)
}
func (n *Notification) handlePasswordlessRegistrationLink(event *models.Event) (err error) {
addedEvent := new(user_repo.HumanPasswordlessInitCodeRequestedEvent)
if err := json.Unmarshal(event.Data, addedEvent); err != nil {
return err
}
events, err := n.getUserEvents(event.AggregateID, event.Sequence)
if err != nil {
return err
}
for _, e := range events {
if e.Type == models.EventType(user_repo.HumanPasswordlessInitCodeSentType) {
sentEvent := new(user_repo.HumanPasswordlessInitCodeSentEvent)
if err := json.Unmarshal(e.Data, sentEvent); err != nil {
return err
}
if sentEvent.ID == addedEvent.ID {
return nil
}
}
}
user, err := n.getUserByID(event.AggregateID)
if err != nil {
return err
}
ctx := getSetNotifyContextData(event.ResourceOwner)
colors, err := n.getLabelPolicy(ctx)
if err != nil {
return err
}
template, err := n.getMailTemplate(ctx)
if err != nil {
return err
}
translator, err := n.getTranslatorWithOrgTexts(user.ResourceOwner, domain.PasswordlessRegistrationMessageType)
if err != nil {
return err
}
err = types.SendPasswordlessRegistrationLink(string(template.Template), translator, user, addedEvent, n.systemDefaults, n.AesCrypto, colors, n.apiDomain)
if err != nil {
return err
}
return n.command.HumanPasswordlessInitCodeSent(ctx, event.AggregateID, event.ResourceOwner, addedEvent.ID)
}
func (n *Notification) checkIfCodeAlreadyHandledOrExpired(event *models.Event, expiry time.Duration, eventTypes ...models.EventType) (bool, error) {
if event.CreationDate.Add(expiry).Before(time.Now().UTC()) {
return true, nil

View File

@@ -1,35 +1,42 @@
InitCode:
Title: Zitadel - User initialisieren
Title: ZITADEL - User initialisieren
PreHeader: User initialisieren
Subject: User initialisieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Dieser Benutzer wurde soeben im Zitadel erstellt. Mit dem Benutzernamen &lt;br&gt;&lt;strong&gt;{{.PreferredLoginName}}&lt;/strong&gt;&lt;br&gt; kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen &lt;br&gt;(Code &lt;strong&gt;{{.Code}}&lt;/strong&gt;).&lt;br&gt; Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.
Text: Dieser Benutzer wurde soeben in ZITADEL erstellt. Mit dem Benutzernamen &lt;br&gt;&lt;strong&gt;{{.PreferredLoginName}}&lt;/strong&gt;&lt;br&gt; kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen &lt;br&gt;(Code &lt;strong&gt;{{.Code}}&lt;/strong&gt;).&lt;br&gt; Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.
ButtonText: Initialisierung abschliessen
PasswordReset:
Title: Zitadel - Passwort zurücksetzen
Title: ZITADEL - Passwort zurücksetzen
PreHeader: Passwort zurücksetzen
Subject: Passwort zurücksetzen
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen &lt;br&gt;(Code &lt;strong&gt;{{.Code}}&lt;/strong&gt;).&lt;br&gt; Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren.
ButtonText: Passwort zurücksetzen
VerifyEmail:
Title: Zitadel - Email verifizieren
Title: ZITADEL - Email verifizieren
PreHeader: Email verifizieren
Subject: Email verifizieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren &lt;br&gt;(Code &lt;strong&gt;{{.Code}}&lt;/strong&gt;).&lt;br&gt; Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren.
ButtonText: Email verifizieren
VerifyPhone:
Title: Zitadel - Telefonnummer verifizieren
Title: ZITADEL - Telefonnummer verifizieren
PreHeader: Telefonnummer verifizieren
Subject: Telefonnummer verifizieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst&lt;br&gt;(Code &lt;strong&gt;{{.Code}}&lt;/strong&gt;).&lt;br&gt;
ButtonText: Telefon verifizieren
DomainClaimed:
Title: Zitadel - Domain wurde beansprucht
Title: ZITADEL - Domain wurde beansprucht
PreHeader: Email / Username ändern
Subject: Domain wurde beansprucht
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt.
ButtonText: Login
PasswordlessRegistration:
Title: ZITADEL - Passwortlosen Login hinzufügen
PreHeader: Passwortlosen Login hinzufügen
Subject: Passwortlosen Login hinzufügen
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Wir haben eine Anfrage für das Hinzufügen eines Token für den passwortlosen Login erhalten. Du kannst den untenstehenden Button verwenden, um dein Token oder Gerät hinzuzufügen.
ButtonText: Passwortlosen Login hinzufügen

View File

@@ -1,35 +1,42 @@
InitCode:
Title: Zitadel - Initialize User
Title: ZITADEL - Initialize User
PreHeader: Initialize User
Subject: Initialize User
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: This user was created in Zitadel. Use the username {{.PreferredLoginName}} to login. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it.
Text: This user was created in ZITADEL. Use the username {{.PreferredLoginName}} to login. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it.
ButtonText: Finish initialization
PasswordReset:
Title: Zitadel - Reset password
Title: ZITADEL - Reset password
PreHeader: Reset password
Subject: Reset password
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: We received a password reset request. Please use the button below to reset your password. (Code {{.Code}}) If you didn't ask for this mail, please ignore it.
ButtonText: Reset password
VerifyEmail:
Title: Zitadel - Verify email
Title: ZITADEL - Verify email
PreHeader: Verify email
Subject: Verify email
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email.
ButtonText: Verify email
VerifyPhone:
Title: Zitadel - Verify phone
Title: ZITADEL - Verify phone
PreHeader: Verify phone
Subject: Verify phone
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}
ButtonText: Verify phone
DomainClaimed:
Title: Zitadel - Domain has been claimed
Title: ZITADEL - Domain has been claimed
PreHeader: Change email / username
Subject: Domain has been claimed
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.Username}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login.
ButtonText: Login
PasswordlessRegistration:
Title: ZITADEL - Add Passwordless Login
PreHeader: Add Passwordless Login
Subject: Add Passwordless Login
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: We received a request to add a token for passwordless login. Please use the button below to add your token or device for passwordless login.
ButtonText: Add Passwordless Login

View File

@@ -0,0 +1,37 @@
package types
import (
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
"github.com/caos/zitadel/internal/repository/user"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
)
type PasswordlessRegistrationLinkData struct {
templates.TemplateData
URL string
}
func SendPasswordlessRegistrationLink(mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *user.HumanPasswordlessInitCodeRequestedEvent, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView, apiDomain string) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
}
url := domain.PasswordlessInitCodeLink(systemDefaults.Notifications.Endpoints.PasswordlessRegistration, user.ID, user.ResourceOwner, code.ID, codeString)
var args = mapNotifyUserToArgs(user)
emailCodeData := &PasswordlessRegistrationLinkData{
TemplateData: templates.GetTemplateData(translator, args, apiDomain, url, domain.PasswordlessRegistrationMessageType, user.PreferredLanguage, colors),
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
if err != nil {
return err
}
return generateEmail(user, emailCodeData.Subject, template, systemDefaults.Notifications, true)
}

View File

@@ -96,6 +96,11 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(HumanPasswordlessTokenBeginLoginType, HumanPasswordlessBeginLoginEventMapper).
RegisterFilterEventMapper(HumanPasswordlessTokenCheckSucceededType, HumanPasswordlessCheckSucceededEventMapper).
RegisterFilterEventMapper(HumanPasswordlessTokenCheckFailedType, HumanPasswordlessCheckFailedEventMapper).
RegisterFilterEventMapper(HumanPasswordlessInitCodeAddedType, HumanPasswordlessInitCodeAddedEventMapper).
RegisterFilterEventMapper(HumanPasswordlessInitCodeRequestedType, HumanPasswordlessInitCodeRequestedEventMapper).
RegisterFilterEventMapper(HumanPasswordlessInitCodeSentType, HumanPasswordlessInitCodeSentEventMapper).
RegisterFilterEventMapper(HumanPasswordlessInitCodeCheckFailedType, HumanPasswordlessInitCodeCodeCheckFailedEventMapper).
RegisterFilterEventMapper(HumanPasswordlessInitCodeCheckSucceededType, HumanPasswordlessInitCodeCodeCheckSucceededEventMapper).
RegisterFilterEventMapper(HumanRefreshTokenAddedType, HumanRefreshTokenAddedEventMapper).
RegisterFilterEventMapper(HumanRefreshTokenRenewedType, HumanRefreshTokenRenewedEventEventMapper).
RegisterFilterEventMapper(HumanRefreshTokenRemovedType, HumanRefreshTokenRemovedEventEventMapper).

View File

@@ -2,21 +2,32 @@ package user
import (
"context"
"encoding/json"
"time"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
)
const (
passwordlessEventPrefix = humanEventPrefix + "passwordless.token."
HumanPasswordlessTokenAddedType = passwordlessEventPrefix + "added"
HumanPasswordlessTokenVerifiedType = passwordlessEventPrefix + "verified"
HumanPasswordlessTokenSignCountChangedType = passwordlessEventPrefix + "signcount.changed"
HumanPasswordlessTokenRemovedType = passwordlessEventPrefix + "removed"
HumanPasswordlessTokenBeginLoginType = passwordlessEventPrefix + "begin.login"
HumanPasswordlessTokenCheckSucceededType = passwordlessEventPrefix + "check.succeeded"
HumanPasswordlessTokenCheckFailedType = passwordlessEventPrefix + "check.failed"
passwordlessEventPrefix = humanEventPrefix + "passwordless."
humanPasswordlessTokenEventPrefix = passwordlessEventPrefix + "token."
HumanPasswordlessTokenAddedType = humanPasswordlessTokenEventPrefix + "added"
HumanPasswordlessTokenVerifiedType = humanPasswordlessTokenEventPrefix + "verified"
HumanPasswordlessTokenSignCountChangedType = humanPasswordlessTokenEventPrefix + "signcount.changed"
HumanPasswordlessTokenRemovedType = humanPasswordlessTokenEventPrefix + "removed"
HumanPasswordlessTokenBeginLoginType = humanPasswordlessTokenEventPrefix + "begin.login"
HumanPasswordlessTokenCheckSucceededType = humanPasswordlessTokenEventPrefix + "check.succeeded"
HumanPasswordlessTokenCheckFailedType = humanPasswordlessTokenEventPrefix + "check.failed"
humanPasswordlessInitCodePrefix = passwordlessEventPrefix + "initialization.code."
HumanPasswordlessInitCodeAddedType = humanPasswordlessInitCodePrefix + "added"
HumanPasswordlessInitCodeRequestedType = humanPasswordlessInitCodePrefix + "requested"
HumanPasswordlessInitCodeSentType = humanPasswordlessInitCodePrefix + "sent"
HumanPasswordlessInitCodeCheckFailedType = humanPasswordlessInitCodePrefix + "check.failed"
HumanPasswordlessInitCodeCheckSucceededType = humanPasswordlessInitCodePrefix + "check.succeeded"
)
type HumanPasswordlessAddedEvent struct {
@@ -254,3 +265,215 @@ func HumanPasswordlessCheckFailedEventMapper(event *repository.Event) (eventstor
return &HumanPasswordlessCheckFailedEvent{HumanWebAuthNCheckFailedEvent: *e.(*HumanWebAuthNCheckFailedEvent)}, nil
}
type HumanPasswordlessInitCodeAddedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Code *crypto.CryptoValue `json:"code"`
Expiry time.Duration `json:"expiry"`
}
func (e *HumanPasswordlessInitCodeAddedEvent) Data() interface{} {
return e
}
func (e *HumanPasswordlessInitCodeAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewHumanPasswordlessInitCodeAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
code *crypto.CryptoValue,
expiry time.Duration,
) *HumanPasswordlessInitCodeAddedEvent {
return &HumanPasswordlessInitCodeAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordlessInitCodeAddedType,
),
ID: id,
Code: code,
Expiry: expiry,
}
}
func HumanPasswordlessInitCodeAddedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
webAuthNAdded := &HumanPasswordlessInitCodeAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, webAuthNAdded)
if err != nil {
return nil, errors.ThrowInternal(err, "USER-BDf32", "unable to unmarshal human passwordless code added")
}
return webAuthNAdded, nil
}
type HumanPasswordlessInitCodeRequestedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Code *crypto.CryptoValue `json:"code"`
Expiry time.Duration `json:"expiry"`
}
func (e *HumanPasswordlessInitCodeRequestedEvent) Data() interface{} {
return e
}
func (e *HumanPasswordlessInitCodeRequestedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewHumanPasswordlessInitCodeRequestedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
code *crypto.CryptoValue,
expiry time.Duration,
) *HumanPasswordlessInitCodeRequestedEvent {
return &HumanPasswordlessInitCodeRequestedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordlessInitCodeRequestedType,
),
ID: id,
Code: code,
Expiry: expiry,
}
}
func HumanPasswordlessInitCodeRequestedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
webAuthNAdded := &HumanPasswordlessInitCodeRequestedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, webAuthNAdded)
if err != nil {
return nil, errors.ThrowInternal(err, "USER-VGfg3", "unable to unmarshal human passwordless code delivery added")
}
return webAuthNAdded, nil
}
type HumanPasswordlessInitCodeSentEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
}
func (e *HumanPasswordlessInitCodeSentEvent) Data() interface{} {
return e
}
func (e *HumanPasswordlessInitCodeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewHumanPasswordlessInitCodeSentEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
) *HumanPasswordlessInitCodeSentEvent {
return &HumanPasswordlessInitCodeSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordlessInitCodeSentType,
),
ID: id,
}
}
func HumanPasswordlessInitCodeSentEventMapper(event *repository.Event) (eventstore.EventReader, error) {
webAuthNAdded := &HumanPasswordlessInitCodeSentEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, webAuthNAdded)
if err != nil {
return nil, errors.ThrowInternal(err, "USER-Gtg4j", "unable to unmarshal human passwordless code sent")
}
return webAuthNAdded, nil
}
type HumanPasswordlessInitCodeCheckFailedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
}
func (e *HumanPasswordlessInitCodeCheckFailedEvent) Data() interface{} {
return e
}
func (e *HumanPasswordlessInitCodeCheckFailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewHumanPasswordlessInitCodeCheckFailedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
) *HumanPasswordlessInitCodeCheckFailedEvent {
return &HumanPasswordlessInitCodeCheckFailedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordlessInitCodeCheckFailedType,
),
ID: id,
}
}
func HumanPasswordlessInitCodeCodeCheckFailedEventMapper(event *repository.Event) (eventstore.EventReader, error) {
webAuthNAdded := &HumanPasswordlessInitCodeCheckFailedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, webAuthNAdded)
if err != nil {
return nil, errors.ThrowInternal(err, "USER-Gtg4j", "unable to unmarshal human passwordless code check failed")
}
return webAuthNAdded, nil
}
type HumanPasswordlessInitCodeCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
}
func (e *HumanPasswordlessInitCodeCheckSucceededEvent) Data() interface{} {
return e
}
func (e *HumanPasswordlessInitCodeCheckSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewHumanPasswordlessInitCodeCheckSucceededEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
) *HumanPasswordlessInitCodeCheckSucceededEvent {
return &HumanPasswordlessInitCodeCheckSucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordlessInitCodeCheckSucceededType,
),
ID: id,
}
}
func HumanPasswordlessInitCodeCodeCheckSucceededEventMapper(event *repository.Event) (eventstore.EventReader, error) {
webAuthNAdded := &HumanPasswordlessInitCodeCheckSucceededEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, webAuthNAdded)
if err != nil {
return nil, errors.ThrowInternal(err, "USER-Gtg4j", "unable to unmarshal human passwordless code check succeeded")
}
return webAuthNAdded, nil
}

View File

@@ -22,7 +22,7 @@ type passwordlessFormData struct {
PasswordLogin bool `schema:"passwordlogin"`
}
func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, passwordSet bool, err error) {
var errID, errMessage, credentialData string
var webAuthNLogin *domain.WebAuthNLogin
if err == nil {
@@ -35,16 +35,15 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re
if webAuthNLogin != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
}
var passwordLogin bool
if authReq.LoginPolicy != nil {
passwordLogin = authReq.LoginPolicy.AllowUsernamePassword
if passwordSet && authReq.LoginPolicy != nil {
passwordSet = authReq.LoginPolicy.AllowUsernamePassword
}
data := &passwordlessData{
webAuthNData{
userData: l.getUserData(r, authReq, "Login Passwordless", errID, errMessage),
CredentialCreationData: credentialData,
},
passwordLogin,
passwordSet,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplPasswordlessVerification], data, nil)
}
@@ -62,13 +61,13 @@ func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Re
}
credData, err := base64.URLEncoding.DecodeString(formData.CredentialData)
if err != nil {
l.renderPasswordlessVerification(w, r, authReq, err)
l.renderPasswordlessVerification(w, r, authReq, formData.PasswordLogin, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyPasswordless(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, userAgentID, credData, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderPasswordlessVerification(w, r, authReq, err)
l.renderPasswordlessVerification(w, r, authReq, formData.PasswordLogin, err)
return
}
l.renderNextStep(w, r, authReq)

View File

@@ -0,0 +1,40 @@
package handler
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplPasswordlessPrompt = "passwordlessprompt"
)
type passwordlessPromptData struct {
userData
}
type passwordlessPromptFormData struct{}
func (l *Login) handlePasswordlessPrompt(w http.ResponseWriter, r *http.Request) {
data := new(passwordlessPromptFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderPasswordlessRegistration(w, r, authReq, "", "", "", "", nil)
}
func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := &passwordlessPromptData{
userData: l.getUserData(r, authReq, "Passwordless Prompt", errID, errMessage),
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil)
}

View File

@@ -0,0 +1,132 @@
package handler
import (
"encoding/base64"
"net/http"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplPasswordlessRegistration = "passwordlessregistration"
tmplPasswordlessRegistrationDone = "passwordlessregistrationdone"
queryPasswordlessRegistrationCode = "code"
queryPasswordlessRegistrationCodeID = "codeID"
queryPasswordlessRegistrationUserID = "userID"
queryPasswordlessRegistrationOrgID = "orgID"
)
type passwordlessRegistrationData struct {
webAuthNData
Code string
CodeID string
UserID string
OrgID string
Disabled bool
}
type passwordlessRegistrationFormData struct {
webAuthNFormData
Code string `schema:"code"`
CodeID string `schema:"codeID"`
UserID string `schema:"userID"`
OrgID string `schema:"orgID"`
TokenName string `schema:"name"`
}
func (l *Login) handlePasswordlessRegistration(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue(queryPasswordlessRegistrationUserID)
orgID := r.FormValue(queryPasswordlessRegistrationOrgID)
codeID := r.FormValue(queryPasswordlessRegistrationCodeID)
code := r.FormValue(queryPasswordlessRegistrationCode)
l.renderPasswordlessRegistration(w, r, nil, userID, orgID, codeID, code, nil)
}
func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, orgID, codeID, code string, err error) {
var errID, errMessage, credentialData string
var disabled bool
if authReq != nil {
userID = authReq.UserID
orgID = authReq.UserOrgID
}
var webAuthNToken *domain.WebAuthNToken
if err == nil {
if authReq != nil {
webAuthNToken, err = l.authRepo.BeginPasswordlessSetup(setContext(r.Context(), authReq.UserOrgID), userID, authReq.UserOrgID)
} else {
webAuthNToken, err = l.authRepo.BeginPasswordlessInitCodeSetup(setContext(r.Context(), orgID), userID, orgID, codeID, code)
}
}
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
disabled = true
}
if webAuthNToken != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData)
}
data := &passwordlessRegistrationData{
webAuthNData{
userData: l.getUserData(r, authReq, "Login Passwordless", errID, errMessage),
CredentialCreationData: credentialData,
},
code,
codeID,
userID,
orgID,
disabled,
}
translator := l.getTranslator(authReq)
if authReq == nil {
policy, err := l.authRepo.GetLabelPolicy(r.Context(), orgID)
if err != nil {
}
data.LabelPolicy = policy
texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
if err != nil {
}
translator, _ = l.renderer.NewTranslator()
l.addLoginTranslations(translator, texts)
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessRegistration], data, nil)
}
func (l *Login) handlePasswordlessRegistrationCheck(w http.ResponseWriter, r *http.Request) {
formData := new(passwordlessRegistrationFormData)
authReq, err := l.getAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.checkPasswordlessRegistration(w, r, authReq, formData, nil)
}
func (l *Login) checkPasswordlessRegistration(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, formData *passwordlessRegistrationFormData, err error) {
credData, err := base64.URLEncoding.DecodeString(formData.CredentialData)
if err != nil {
l.renderPasswordlessRegistration(w, r, authReq, formData.UserID, formData.OrgID, formData.CodeID, formData.Code, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
if authReq != nil {
err = l.authRepo.VerifyPasswordlessSetup(setContext(r.Context(), authReq.UserOrgID), formData.UserID, authReq.UserOrgID, userAgentID, formData.TokenName, credData)
} else {
err = l.authRepo.VerifyPasswordlessInitCodeSetup(setContext(r.Context(), formData.OrgID), formData.UserID, formData.OrgID, userAgentID, formData.TokenName, formData.CodeID, formData.Code, credData)
}
if err != nil {
l.renderPasswordlessRegistration(w, r, authReq, formData.UserID, formData.OrgID, formData.CodeID, formData.Code, err)
return
}
l.renderPasswordlessRegistrationDone(w, r, authReq, nil)
}
func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := l.getUserData(r, authReq, "Passwordless Registration Done", errID, errMessage)
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplPasswordlessRegistrationDone], data, nil)
}

View File

@@ -37,35 +37,38 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
staticStorage: staticStorage,
}
tmplMapping := map[string]string{
tmplError: "error.html",
tmplLogin: "login.html",
tmplUserSelection: "select_user.html",
tmplPassword: "password.html",
tmplPasswordlessVerification: "passwordless.html",
tmplMFAVerify: "mfa_verify_otp.html",
tmplMFAPrompt: "mfa_prompt.html",
tmplMFAInitVerify: "mfa_init_otp.html",
tmplMFAU2FInit: "mfa_init_u2f.html",
tmplU2FVerification: "mfa_verification_u2f.html",
tmplMFAInitDone: "mfa_init_done.html",
tmplMailVerification: "mail_verification.html",
tmplMailVerified: "mail_verified.html",
tmplInitPassword: "init_password.html",
tmplInitPasswordDone: "init_password_done.html",
tmplInitUser: "init_user.html",
tmplInitUserDone: "init_user_done.html",
tmplPasswordResetDone: "password_reset_done.html",
tmplChangePassword: "change_password.html",
tmplChangePasswordDone: "change_password_done.html",
tmplRegisterOption: "register_option.html",
tmplRegister: "register.html",
tmplLogoutDone: "logout_done.html",
tmplRegisterOrg: "register_org.html",
tmplChangeUsername: "change_username.html",
tmplChangeUsernameDone: "change_username_done.html",
tmplLinkUsersDone: "link_users_done.html",
tmplExternalNotFoundOption: "external_not_found_option.html",
tmplLoginSuccess: "login_success.html",
tmplError: "error.html",
tmplLogin: "login.html",
tmplUserSelection: "select_user.html",
tmplPassword: "password.html",
tmplPasswordlessVerification: "passwordless.html",
tmplPasswordlessRegistration: "passwordless_registration.html",
tmplPasswordlessRegistrationDone: "passwordless_registration_done.html",
tmplPasswordlessPrompt: "passwordless_prompt.html",
tmplMFAVerify: "mfa_verify_otp.html",
tmplMFAPrompt: "mfa_prompt.html",
tmplMFAInitVerify: "mfa_init_otp.html",
tmplMFAU2FInit: "mfa_init_u2f.html",
tmplU2FVerification: "mfa_verification_u2f.html",
tmplMFAInitDone: "mfa_init_done.html",
tmplMailVerification: "mail_verification.html",
tmplMailVerified: "mail_verified.html",
tmplInitPassword: "init_password.html",
tmplInitPasswordDone: "init_password_done.html",
tmplInitUser: "init_user.html",
tmplInitUserDone: "init_user_done.html",
tmplPasswordResetDone: "password_reset_done.html",
tmplChangePassword: "change_password.html",
tmplChangePasswordDone: "change_password_done.html",
tmplRegisterOption: "register_option.html",
tmplRegister: "register.html",
tmplLogoutDone: "logout_done.html",
tmplRegisterOrg: "register_org.html",
tmplChangeUsername: "change_username.html",
tmplChangeUsernameDone: "change_username_done.html",
tmplLinkUsersDone: "link_users_done.html",
tmplExternalNotFoundOption: "external_not_found_option.html",
tmplLoginSuccess: "login_success.html",
}
funcs := map[string]interface{}{
"resourceUrl": func(file string) string {
@@ -127,6 +130,12 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
"passwordLessVerificationUrl": func() string {
return path.Join(r.pathPrefix, EndpointPasswordlessLogin)
},
"passwordLessRegistrationUrl": func() string {
return path.Join(r.pathPrefix, EndpointPasswordlessRegistration)
},
"passwordlessPromptUrl": func() string {
return path.Join(r.pathPrefix, EndpointPasswordlessPrompt)
},
"passwordResetUrl": func(id string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointPasswordReset, queryAuthRequestID, id))
},
@@ -246,7 +255,9 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
case *domain.PasswordStep:
l.renderPassword(w, r, authReq, nil)
case *domain.PasswordlessStep:
l.renderPasswordlessVerification(w, r, authReq, nil)
l.renderPasswordlessVerification(w, r, authReq, step.PasswordSet, nil)
case *domain.PasswordlessRegistrationPromptStep:
l.renderPasswordlessPrompt(w, r, authReq, nil)
case *domain.MFAVerificationStep:
l.renderMFAVerify(w, r, authReq, step, err)
case *domain.RedirectToCallbackStep:

View File

@@ -14,6 +14,8 @@ const (
EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointPasswordlessLogin = "/login/passwordless"
EndpointPasswordlessRegistration = "/login/passwordless/init"
EndpointPasswordlessPrompt = "/login/passwordless/prompt"
EndpointLoginName = "/loginname"
EndpointUserSelection = "/userselection"
EndpointChangeUsername = "/username/change"
@@ -52,6 +54,9 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost)
router.HandleFunc(EndpointPasswordlessRegistration, login.handlePasswordlessRegistration).Methods(http.MethodGet)
router.HandleFunc(EndpointPasswordlessRegistration, login.handlePasswordlessRegistrationCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointPasswordlessPrompt, login.handlePasswordlessPrompt).Methods(http.MethodPost)
router.HandleFunc(EndpointLoginName, login.handleLoginName).Methods(http.MethodGet)
router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointUserSelection, login.handleSelectUser).Methods(http.MethodPost)

View File

@@ -134,6 +134,27 @@ Passwordless:
LoginWithPwButtonText: Mit Passwort anmelden
ValidateTokenButtonText: Token validieren
PasswordlessPrompt:
Title: Passwortloser Login hinzufügen
Description: Möchtest du einen passwortlosen Login hinzufügen?
DescriptionInit: Du musst zuerst den Passwortlosen Login hinzufügen. Nutze dazu den Link, den du erhalten hast um dein Gerät zu registrieren.
PasswordlessButtonText: Werde Passwortlos
NextButtonText: weiter
SkipButtonText: überspringen
PasswordlessRegistration:
Title: Passwortloser Login hinzufügen
Description: Füge dein Token hinzu, indem du einen Namen eingibst und den 'Token registrieren' Button drückst.
TokenNameLabel: Name des Tokens / Geräts
NotSupported: WebAuthN wird durch deinen Browser nicht unterstützt. Stelle sicher, dass du die aktuelle Version installiert hast oder nutze einen anderen (z.B. Chrome, Safari, Firefox)
RegisterTokenButtonText: Token registrieren
ErrorRetry: Versuche es erneut, erstelle eine neue Abfrage oder wähle eine andere Methode.
PasswordlessRegistrationDone:
Title: Passwortloser Login erstellt
Description: Token für passwortlosen Login erfolgreich hinzugefügt.
NextButtonText: weiter
PasswordChange:
Title: Passwort ändern
Description: Ändere dein Password in dem du dein altes und dann dein neuen Passwort eingibst.

View File

@@ -97,7 +97,7 @@ InitMFAOTP:
InitMFAU2F:
Title: Multifactor Setup U2F / WebAuthN
Description: Add your Token by providing a name and then clicking on the 'Register Token' button below.
TokenNameLabel: Name of the tokens / machine
TokenNameLabel: Name of the token / machine
NotSupported: WebAuthN is not supported by your browser. Please ensure it is up to date or use a different one (e.g. Chrome, Safari, Firefox)
RegisterTokenButtonText: Register Token
ErrorRetry: Retry, create a new challenge or choose a different method.
@@ -134,6 +134,27 @@ Passwordless:
LoginWithPwButtonText: Login with password
ValidateTokenButtonText: Validate Token
PasswordlessPrompt:
Title: Passwordless setup
Description: Would you like to setup passwordless login?
DescriptionInit: You need to set up passwordless login. Use the link you were given to register your device.
PasswordlessButtonText: Go passwordless
NextButtonText: next
SkipButtonText: skip
PasswordlessRegistration:
Title: Passwordless setup
Description: Add your Token by providing a name and then clicking on the 'Register Token' button below.
TokenNameLabel: Name of the token / machine
NotSupported: WebAuthN is not supported by your browser. Please ensure it is up to date or use a different one (e.g. Chrome, Safari, Firefox)
RegisterTokenButtonText: Register Token
ErrorRetry: Retry, create a new challenge or choose a different method.
PasswordlessRegistrationDone:
Title: Passwordless set up
Description: Token for passwordless successfully added.
NextButtonText: next
PasswordChange:
Title: Change Password
Description: Change your password. Enter your old and new password.

View File

@@ -0,0 +1,25 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "PasswordlessPrompt.Title"}}</h1>
{{ template "user-profile" . }}
<p>{{t "PasswordlessPrompt.DescriptionInit"}}</p>
</div>
<form action="{{ passwordlessPromptUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="lgn-actions">
<!-- position element in header -->
<a class="lgn-icon-button lgn-left-action" href="{{ loginUrl }}">
<i class="lgn-icon-arrow-left-solid"></i>
</a>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,52 @@
{{template "main-top" .}}
<div class="head">
<h1>{{t "PasswordlessRegistration.Title"}}</h1>
{{ template "user-profile" . }}
<p>{{t "PasswordlessRegistration.Description"}}</p>
</div>
<form action="{{ passwordLessRegistrationUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<input type="hidden" name="codeID" value="{{ .CodeID }}" />
<input type="hidden" name="code" value="{{ .Code }}" />
<input type="hidden" name="credentialCreationData" value="{{ .CredentialCreationData }}" />
<input type="hidden" name="credentialData" />
<div class="fields">
<p class="wa-no-support lgn-error hidden">{{t "PasswordlessRegistration.NotSupported"}}</p>
{{if not .Disabled}}
<div class="field">
<label class="lgn-label" for="name" disabled="false">{{t "PasswordlessRegistration.TokenNameLabel"}}</label>
<input class="lgn-input" type="text" id="name" name="name" autocomplete="off" autofocus>
</div>
{{end}}
<div id="wa-error" class="lgn-error hidden">
<span class="cause"></span>
<span>{{t "PasswordlessRegistration.ErrorRetry"}}</span>
</div>
</div>
{{ template "error-message" .}}
<div class="lgn-actions">
<span class="fill-space"></span>
{{if not .Disabled}}
<a id="btn-register" class="lgn-raised-button lgn-primary wa-support">{{t "PasswordlessRegistration.RegisterTokenButtonText"}}</a>
{{end}}
</div>
</form>
<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_register.js" }}"></script>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,27 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "PasswordlessRegistrationDone.Title"}}</h1>
{{ template "user-profile" . }}
<p>{{t "PasswordlessRegistrationDone.Description"}}</p>
</div>
<form action="{{ loginUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="lgn-actions">
<a class="lgn-stroked-button lgn-primary" href="{{ loginUrl }}">
{{t "PasswordlessRegistrationDone.CancelButtonText"}}
</a>
<span class="fill-space"></span>
<button class="lgn-raised-button lgn-primary" type="submit">{{t "PasswordlessRegistrationDone.NextButtonText"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -32,34 +32,36 @@ type UserView struct {
}
type HumanView struct {
PasswordSet bool
PasswordChangeRequired bool
UsernameChangeRequired bool
PasswordChanged time.Time
FirstName string
LastName string
NickName string
DisplayName string
AvatarKey string
AvatarURL string
PreSignedAvatar *url.URL
PreferredLanguage string
Gender Gender
Email string
IsEmailVerified bool
Phone string
IsPhoneVerified bool
Country string
Locality string
PostalCode string
Region string
StreetAddress string
OTPState MFAState
U2FTokens []*WebAuthNView
PasswordlessTokens []*WebAuthNView
MFAMaxSetUp req_model.MFALevel
MFAInitSkipped time.Time
InitRequired bool
PasswordSet bool
PasswordInitRequired bool
PasswordChangeRequired bool
UsernameChangeRequired bool
PasswordChanged time.Time
FirstName string
LastName string
NickName string
DisplayName string
AvatarKey string
AvatarURL string
PreSignedAvatar *url.URL
PreferredLanguage string
Gender Gender
Email string
IsEmailVerified bool
Phone string
IsPhoneVerified bool
Country string
Locality string
PostalCode string
Region string
StreetAddress string
OTPState MFAState
U2FTokens []*WebAuthNView
PasswordlessTokens []*WebAuthNView
MFAMaxSetUp req_model.MFALevel
MFAInitSkipped time.Time
InitRequired bool
PasswordlessInitRequired bool
}
type WebAuthNView struct {

View File

@@ -14,6 +14,7 @@ import (
"github.com/caos/zitadel/internal/eventstore/v1/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
org_model "github.com/caos/zitadel/internal/org/model"
user_repo "github.com/caos/zitadel/internal/repository/user"
"github.com/caos/zitadel/internal/user/model"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
)
@@ -69,33 +70,34 @@ const (
)
type HumanView struct {
FirstName string `json:"firstName" gorm:"column:first_name"`
LastName string `json:"lastName" gorm:"column:last_name"`
NickName string `json:"nickName" gorm:"column:nick_name"`
DisplayName string `json:"displayName" gorm:"column:display_name"`
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
Gender int32 `json:"gender" gorm:"column:gender"`
AvatarKey string `json:"storeKey" gorm:"column:avatar_key"`
Email string `json:"email" gorm:"column:email"`
IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"`
Phone string `json:"phone" gorm:"column:phone"`
IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"`
Country string `json:"country" gorm:"column:country"`
Locality string `json:"locality" gorm:"column:locality"`
PostalCode string `json:"postalCode" gorm:"column:postal_code"`
Region string `json:"region" gorm:"column:region"`
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
OTPState int32 `json:"-" gorm:"column:otp_state"`
U2FTokens WebAuthNTokens `json:"-" gorm:"column:u2f_tokens"`
MFAMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"`
MFAInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"`
InitRequired bool `json:"-" gorm:"column:init_required"`
PasswordSet bool `json:"-" gorm:"column:password_set"`
PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"`
UsernameChangeRequired bool `json:"-" gorm:"column:username_change_required"`
PasswordChanged time.Time `json:"-" gorm:"column:password_change"`
PasswordlessTokens WebAuthNTokens `json:"-" gorm:"column:passwordless_tokens"`
FirstName string `json:"firstName" gorm:"column:first_name"`
LastName string `json:"lastName" gorm:"column:last_name"`
NickName string `json:"nickName" gorm:"column:nick_name"`
DisplayName string `json:"displayName" gorm:"column:display_name"`
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
Gender int32 `json:"gender" gorm:"column:gender"`
AvatarKey string `json:"storeKey" gorm:"column:avatar_key"`
Email string `json:"email" gorm:"column:email"`
IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"`
Phone string `json:"phone" gorm:"column:phone"`
IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"`
Country string `json:"country" gorm:"column:country"`
Locality string `json:"locality" gorm:"column:locality"`
PostalCode string `json:"postalCode" gorm:"column:postal_code"`
Region string `json:"region" gorm:"column:region"`
StreetAddress string `json:"streetAddress" gorm:"column:street_address"`
OTPState int32 `json:"-" gorm:"column:otp_state"`
U2FTokens WebAuthNTokens `json:"-" gorm:"column:u2f_tokens"`
MFAMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"`
MFAInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"`
InitRequired bool `json:"-" gorm:"column:init_required"`
PasswordlessInitRequired bool `json:"-" gorm:"column:passwordless_init_required"`
PasswordInitRequired bool `json:"-" gorm:"column:password_init_required"`
PasswordSet bool `json:"-" gorm:"column:password_set"`
PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"`
UsernameChangeRequired bool `json:"-" gorm:"column:username_change_required"`
PasswordChanged time.Time `json:"-" gorm:"column:password_change"`
PasswordlessTokens WebAuthNTokens `json:"-" gorm:"column:passwordless_tokens"`
}
type WebAuthNTokens []*WebAuthNView
@@ -151,32 +153,34 @@ func UserToModel(user *UserView, prefixAvatarURL string) *model.UserView {
}
if !user.HumanView.IsZero() {
userView.HumanView = &model.HumanView{
PasswordSet: user.PasswordSet,
PasswordChangeRequired: user.PasswordChangeRequired,
PasswordChanged: user.PasswordChanged,
PasswordlessTokens: WebauthnTokensToModel(user.PasswordlessTokens),
U2FTokens: WebauthnTokensToModel(user.U2FTokens),
FirstName: user.FirstName,
LastName: user.LastName,
NickName: user.NickName,
DisplayName: user.DisplayName,
AvatarKey: user.AvatarKey,
AvatarURL: domain.AvatarURL(prefixAvatarURL, user.ResourceOwner, user.AvatarKey),
PreferredLanguage: user.PreferredLanguage,
Gender: model.Gender(user.Gender),
Email: user.Email,
IsEmailVerified: user.IsEmailVerified,
Phone: user.Phone,
IsPhoneVerified: user.IsPhoneVerified,
Country: user.Country,
Locality: user.Locality,
PostalCode: user.PostalCode,
Region: user.Region,
StreetAddress: user.StreetAddress,
OTPState: model.MFAState(user.OTPState),
MFAMaxSetUp: req_model.MFALevel(user.MFAMaxSetUp),
MFAInitSkipped: user.MFAInitSkipped,
InitRequired: user.InitRequired,
PasswordSet: user.PasswordSet,
PasswordInitRequired: user.PasswordInitRequired,
PasswordChangeRequired: user.PasswordChangeRequired,
PasswordChanged: user.PasswordChanged,
PasswordlessTokens: WebauthnTokensToModel(user.PasswordlessTokens),
U2FTokens: WebauthnTokensToModel(user.U2FTokens),
FirstName: user.FirstName,
LastName: user.LastName,
NickName: user.NickName,
DisplayName: user.DisplayName,
AvatarKey: user.AvatarKey,
AvatarURL: domain.AvatarURL(prefixAvatarURL, user.ResourceOwner, user.AvatarKey),
PreferredLanguage: user.PreferredLanguage,
Gender: model.Gender(user.Gender),
Email: user.Email,
IsEmailVerified: user.IsEmailVerified,
Phone: user.Phone,
IsPhoneVerified: user.IsPhoneVerified,
Country: user.Country,
Locality: user.Locality,
PostalCode: user.PostalCode,
Region: user.Region,
StreetAddress: user.StreetAddress,
OTPState: model.MFAState(user.OTPState),
MFAMaxSetUp: req_model.MFALevel(user.MFAMaxSetUp),
MFAInitSkipped: user.MFAInitSkipped,
InitRequired: user.InitRequired,
PasswordlessInitRequired: user.PasswordlessInitRequired,
}
}
@@ -345,6 +349,12 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) {
err = u.setData(event)
case es_model.HumanAvatarRemoved:
u.AvatarKey = ""
case models.EventType(user_repo.HumanPasswordlessInitCodeAddedType),
models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType):
if !u.PasswordSet {
u.PasswordlessInitRequired = true
u.PasswordInitRequired = false
}
}
u.ComputeObject()
return err
@@ -370,6 +380,7 @@ func (u *UserView) setPasswordData(event *models.Event) error {
return caos_errs.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data")
}
u.PasswordSet = password.Secret != nil
u.PasswordInitRequired = !u.PasswordSet
u.PasswordChangeRequired = password.ChangeRequired
u.PasswordChanged = event.CreationDate
return nil
@@ -498,6 +509,7 @@ func (u *UserView) ComputeMFAMaxSetUp() {
for _, token := range u.PasswordlessTokens {
if token.State == int32(model.MFAStateReady) {
u.MFAMaxSetUp = int32(req_model.MFALevelMultiFactor)
u.PasswordlessInitRequired = false
return
}
}