mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 11:27:33 +00:00
feat: passwordless registration (#2103)
* begin pw less registration * create pwless one time codes * send pwless link * separate send and add passwordless link * separate send and add passwordless link events * custom message text for passwordless registration * begin custom login texts for passwordless * i18n * i18n message * i18n message * custom message text * custom login text * org design and texts * create link in human import process * fix import human tests * begin passwordless init required step * passwordless init * passwordless init * do not return link in mgmt api * prompt * passwordless init only (no additional prompt) * cleanup * cleanup * add passwordless prompt to custom login text * increase init code complexity * fix grpc * cleanup * fix and add some cases for nextStep tests * fix tests * Update internal/notification/static/i18n/en.yaml * Update internal/notification/static/i18n/de.yaml * Update proto/zitadel/management.proto * Update internal/ui/login/static/i18n/de.yaml * Update internal/ui/login/static/i18n/de.yaml * Update internal/ui/login/static/i18n/de.yaml Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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{}
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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{}
|
||||
}
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ type Commands struct {
|
||||
emailVerificationCode crypto.Generator
|
||||
phoneVerificationCode crypto.Generator
|
||||
passwordVerificationCode crypto.Generator
|
||||
passwordlessInitCode crypto.Generator
|
||||
machineKeyAlg crypto.EncryptionAlgorithm
|
||||
machineKeySize int
|
||||
applicationKeySize int
|
||||
@@ -90,6 +91,7 @@ func StartCommands(eventstore *eventstore.Eventstore, defaults sd.SystemDefaults
|
||||
repo.emailVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.EmailVerificationCode, userEncryptionAlgorithm)
|
||||
repo.phoneVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PhoneVerificationCode, userEncryptionAlgorithm)
|
||||
repo.passwordVerificationCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PasswordVerificationCode, userEncryptionAlgorithm)
|
||||
repo.passwordlessInitCode = crypto.NewEncryptionGenerator(defaults.SecretGenerators.PasswordlessInitCode, userEncryptionAlgorithm)
|
||||
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||
repo.machineKeyAlg = userEncryptionAlgorithm
|
||||
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)
|
||||
|
@@ -32,6 +32,9 @@ func (c *Commands) createAllLoginTextEvents(ctx context.Context, agg *eventstore
|
||||
events = append(events, c.createVerifyMFAOTPEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createVerifyMFAU2FEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createPasswordlessEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createPasswordlessPromptEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createPasswordlessRegistrationEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createPasswordlessRegistrationDoneEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createPasswordChangeEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createPasswordChangeDoneEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
events = append(events, c.createPasswordResetDoneEvents(ctx, agg, existingText, text, defaultText)...)
|
||||
@@ -589,6 +592,81 @@ func (c *Commands) createPasswordlessEvents(ctx context.Context, agg *eventstore
|
||||
return events
|
||||
}
|
||||
|
||||
func (c *Commands) createPasswordlessRegistrationEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
|
||||
events := make([]eventstore.EventPusher, 0)
|
||||
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationTitle, existingText.PasswordlessRegistrationTitle, text.PasswordlessRegistration.Title, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDescription, existingText.PasswordlessRegistrationDescription, text.PasswordlessRegistration.Description, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationRegisterTokenButtonText, existingText.PasswordlessRegistrationRegisterTokenButtonText, text.PasswordlessRegistration.RegisterTokenButtonText, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationTokenNameLabel, existingText.PasswordlessRegistrationTokenNameLabel, text.PasswordlessRegistration.TokenNameLabel, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationNotSupported, existingText.PasswordlessRegistrationNotSupported, text.PasswordlessRegistration.NotSupported, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationErrorRetry, existingText.PasswordlessRegistrationErrorRetry, text.PasswordlessRegistration.ErrorRetry, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func (c *Commands) createPasswordlessPromptEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
|
||||
events := make([]eventstore.EventPusher, 0)
|
||||
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptTitle, existingText.PasswordlessPromptTitle, text.PasswordlessPrompt.Title, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptDescription, existingText.PasswordlessPromptDescription, text.PasswordlessPrompt.Description, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptDescriptionInit, existingText.PasswordlessPromptDescriptionInit, text.PasswordlessPrompt.DescriptionInit, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptPasswordlessButtonText, existingText.PasswordlessPromptPasswordlessButtonText, text.PasswordlessPrompt.PasswordlessButtonText, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptNextButtonText, existingText.PasswordlessPromptNextButtonText, text.PasswordlessPrompt.NextButtonText, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessPromptSkipButtonText, existingText.PasswordlessPromptSkipButtonText, text.PasswordlessPrompt.SkipButtonText, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func (c *Commands) createPasswordlessRegistrationDoneEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
|
||||
events := make([]eventstore.EventPusher, 0)
|
||||
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDoneTitle, existingText.PasswordlessRegistrationDoneTitle, text.PasswordlessRegistrationDone.Title, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDoneDescription, existingText.PasswordlessRegistrationDoneDescription, text.PasswordlessRegistrationDone.Description, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
event = c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordlessRegistrationDoneNextButtonText, existingText.PasswordlessRegistrationDoneNextButtonText, text.PasswordlessRegistrationDone.NextButtonText, text.Language, defaultText)
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func (c *Commands) createPasswordChangeEvents(ctx context.Context, agg *eventstore.Aggregate, existingText *CustomLoginTextReadModel, text *domain.CustomLoginText, defaultText bool) []eventstore.EventPusher {
|
||||
events := make([]eventstore.EventPusher, 0)
|
||||
event := c.createCustomLoginTextEvent(ctx, agg, domain.LoginKeyPasswordChangeTitle, existingText.PasswordChangeTitle, text.PasswordChange.Title, text.Language, defaultText)
|
||||
|
@@ -147,6 +147,24 @@ type CustomLoginTextReadModel struct {
|
||||
PasswordlessNotSupported string
|
||||
PasswordlessErrorRetry string
|
||||
|
||||
PasswordlessPromptTitle string
|
||||
PasswordlessPromptDescription string
|
||||
PasswordlessPromptDescriptionInit string
|
||||
PasswordlessPromptPasswordlessButtonText string
|
||||
PasswordlessPromptNextButtonText string
|
||||
PasswordlessPromptSkipButtonText string
|
||||
|
||||
PasswordlessRegistrationTitle string
|
||||
PasswordlessRegistrationDescription string
|
||||
PasswordlessRegistrationRegisterTokenButtonText string
|
||||
PasswordlessRegistrationTokenNameLabel string
|
||||
PasswordlessRegistrationNotSupported string
|
||||
PasswordlessRegistrationErrorRetry string
|
||||
|
||||
PasswordlessRegistrationDoneTitle string
|
||||
PasswordlessRegistrationDoneDescription string
|
||||
PasswordlessRegistrationDoneNextButtonText string
|
||||
|
||||
PasswordChangeTitle string
|
||||
PasswordChangeDescription string
|
||||
PasswordChangeOldPasswordLabel string
|
||||
@@ -314,6 +332,18 @@ func (wm *CustomLoginTextReadModel) Reduce() error {
|
||||
wm.handlePasswordlessScreenSetEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessPrompt) {
|
||||
wm.handlePasswordlessPromptScreenSetEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistration) {
|
||||
wm.handlePasswordlessRegistrationScreenSetEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistrationDone) {
|
||||
wm.handlePasswordlessRegistrationDoneScreenSetEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordChange) {
|
||||
wm.handlePasswordChangeScreenSetEvent(e)
|
||||
continue
|
||||
@@ -438,6 +468,18 @@ func (wm *CustomLoginTextReadModel) Reduce() error {
|
||||
wm.handlePasswordlessScreenRemoveEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessPrompt) {
|
||||
wm.handlePasswordlessPromptScreenRemoveEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistration) {
|
||||
wm.handlePasswordlessRegistrationScreenRemoveEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordlessRegistrationDone) {
|
||||
wm.handlePasswordlessRegistrationDoneScreenRemoveEvent(e)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(e.Key, domain.LoginKeyPasswordChange) {
|
||||
wm.handlePasswordChangeScreenRemoveEvent(e)
|
||||
continue
|
||||
@@ -1489,6 +1531,144 @@ func (wm *CustomLoginTextReadModel) handlePasswordlessScreenRemoveEvent(e *polic
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *CustomLoginTextReadModel) handlePasswordlessPromptScreenSetEvent(e *policy.CustomTextSetEvent) {
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptTitle {
|
||||
wm.PasswordlessPromptTitle = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptDescription {
|
||||
wm.PasswordlessPromptDescription = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptDescriptionInit {
|
||||
wm.PasswordlessPromptDescriptionInit = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptPasswordlessButtonText {
|
||||
wm.PasswordlessPromptPasswordlessButtonText = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptNextButtonText {
|
||||
wm.PasswordlessPromptNextButtonText = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptSkipButtonText {
|
||||
wm.PasswordlessPromptSkipButtonText = e.Text
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *CustomLoginTextReadModel) handlePasswordlessPromptScreenRemoveEvent(e *policy.CustomTextRemovedEvent) {
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptTitle {
|
||||
wm.PasswordlessPromptTitle = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptDescription {
|
||||
wm.PasswordlessPromptDescription = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptDescriptionInit {
|
||||
wm.PasswordlessPromptDescriptionInit = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptPasswordlessButtonText {
|
||||
wm.PasswordlessPromptPasswordlessButtonText = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptNextButtonText {
|
||||
wm.PasswordlessPromptNextButtonText = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessPromptSkipButtonText {
|
||||
wm.PasswordlessPromptSkipButtonText = ""
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationScreenSetEvent(e *policy.CustomTextSetEvent) {
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationTitle {
|
||||
wm.PasswordlessRegistrationTitle = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDescription {
|
||||
wm.PasswordlessRegistrationDescription = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationRegisterTokenButtonText {
|
||||
wm.PasswordlessRegistrationRegisterTokenButtonText = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationTokenNameLabel {
|
||||
wm.PasswordlessRegistrationTokenNameLabel = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationNotSupported {
|
||||
wm.PasswordlessRegistrationNotSupported = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationErrorRetry {
|
||||
wm.PasswordlessRegistrationErrorRetry = e.Text
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationScreenRemoveEvent(e *policy.CustomTextRemovedEvent) {
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationTitle {
|
||||
wm.PasswordlessRegistrationTitle = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDescription {
|
||||
wm.PasswordlessRegistrationDescription = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationRegisterTokenButtonText {
|
||||
wm.PasswordlessRegistrationRegisterTokenButtonText = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationTokenNameLabel {
|
||||
wm.PasswordlessRegistrationTokenNameLabel = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationNotSupported {
|
||||
wm.PasswordlessRegistrationNotSupported = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationErrorRetry {
|
||||
wm.PasswordlessRegistrationErrorRetry = ""
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationDoneScreenSetEvent(e *policy.CustomTextSetEvent) {
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneTitle {
|
||||
wm.PasswordlessRegistrationDoneTitle = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneDescription {
|
||||
wm.PasswordlessRegistrationDoneDescription = e.Text
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneNextButtonText {
|
||||
wm.PasswordlessRegistrationDoneNextButtonText = e.Text
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *CustomLoginTextReadModel) handlePasswordlessRegistrationDoneScreenRemoveEvent(e *policy.CustomTextRemovedEvent) {
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneTitle {
|
||||
wm.PasswordlessRegistrationDoneTitle = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneDescription {
|
||||
wm.PasswordlessRegistrationDoneDescription = ""
|
||||
return
|
||||
}
|
||||
if e.Key == domain.LoginKeyPasswordlessRegistrationDoneNextButtonText {
|
||||
wm.PasswordlessRegistrationDoneNextButtonText = ""
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *CustomLoginTextReadModel) handlePasswordChangeScreenSetEvent(e *policy.CustomTextSetEvent) {
|
||||
if e.Key == domain.LoginKeyPasswordChangeTitle {
|
||||
wm.PasswordChangeTitle = e.Text
|
||||
|
@@ -143,3 +143,13 @@ func authRequestDomainToAuthRequestInfo(authRequest *domain.AuthRequest) *user.A
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func writeModelToPasswordlessInitCode(initCodeModel *HumanPasswordlessInitCodeWriteModel, code string) *domain.PasswordlessInitCode {
|
||||
return &domain.PasswordlessInitCode{
|
||||
ObjectRoot: writeModelToObjectRoot(initCodeModel.WriteModel),
|
||||
CodeID: initCodeModel.CodeID,
|
||||
Code: code,
|
||||
Expiration: initCodeModel.Expiration,
|
||||
State: initCodeModel.State,
|
||||
}
|
||||
}
|
||||
|
@@ -50,33 +50,40 @@ func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Hum
|
||||
return writeModelToHuman(addedHuman), nil
|
||||
}
|
||||
|
||||
func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human) (*domain.Human, error) {
|
||||
func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) {
|
||||
if orgID == "" {
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
|
||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
|
||||
}
|
||||
orgIAMPolicy, err := c.getOrgIAMPolicy(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.OrgIAMPolicy.NotFound")
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.OrgIAMPolicy.NotFound")
|
||||
}
|
||||
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexity.NotFound")
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexity.NotFound")
|
||||
}
|
||||
events, addedHuman, err := c.importHuman(ctx, orgID, human, orgIAMPolicy, pwPolicy)
|
||||
events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, orgIAMPolicy, pwPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = AppendAndReduce(addedHuman, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if addedCode != nil {
|
||||
err = AppendAndReduce(addedCode, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
passwordlessCode = writeModelToPasswordlessInitCode(addedCode, code)
|
||||
}
|
||||
|
||||
return writeModelToHuman(addedHuman), nil
|
||||
return writeModelToHuman(addedHuman), passwordlessCode, nil
|
||||
}
|
||||
|
||||
func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
|
||||
@@ -86,14 +93,26 @@ func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Hum
|
||||
if human.Password != nil && human.SecretString != "" {
|
||||
human.ChangeRequired = true
|
||||
}
|
||||
return c.createHuman(ctx, orgID, human, nil, false, orgIAMPolicy, pwPolicy)
|
||||
return c.createHuman(ctx, orgID, human, nil, false, false, orgIAMPolicy, pwPolicy)
|
||||
}
|
||||
|
||||
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
|
||||
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) (events []eventstore.EventPusher, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
|
||||
if orgID == "" || !human.IsValid() {
|
||||
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid")
|
||||
return nil, nil, nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid")
|
||||
}
|
||||
return c.createHuman(ctx, orgID, human, nil, false, orgIAMPolicy, pwPolicy)
|
||||
events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, orgIAMPolicy, pwPolicy)
|
||||
if err != nil {
|
||||
return nil, nil, nil, "", err
|
||||
}
|
||||
if passwordless {
|
||||
var codeEvent eventstore.EventPusher
|
||||
codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true)
|
||||
if err != nil {
|
||||
return nil, nil, nil, "", err
|
||||
}
|
||||
events = append(events, codeEvent)
|
||||
}
|
||||
return events, humanWriteModel, passwordlessCodeWriteModel, code, nil
|
||||
}
|
||||
|
||||
func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, orgMemberRoles []string) (*domain.Human, error) {
|
||||
@@ -149,10 +168,10 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai
|
||||
if human.Password != nil && human.SecretString != "" {
|
||||
human.ChangeRequired = false
|
||||
}
|
||||
return c.createHuman(ctx, orgID, human, externalIDP, true, orgIAMPolicy, pwPolicy)
|
||||
return c.createHuman(ctx, orgID, human, externalIDP, true, false, orgIAMPolicy, pwPolicy)
|
||||
}
|
||||
|
||||
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, selfregister bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
|
||||
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, selfregister, passwordless bool, orgIAMPolicy *domain.OrgIAMPolicy, pwPolicy *domain.PasswordComplexityPolicy) ([]eventstore.EventPusher, *HumanWriteModel, error) {
|
||||
if err := human.CheckOrgIAMPolicy(orgIAMPolicy); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -187,7 +206,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
if human.IsInitialState() {
|
||||
if human.IsInitialState(passwordless) {
|
||||
initCode, err := domain.NewInitUserCode(c.initializeUserCode)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@@ -652,19 +652,22 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
|
||||
func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
secretGenerator crypto.Generator
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
secretGenerator crypto.Generator
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
passwordlessInitCode crypto.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
orgID string
|
||||
human *domain.Human
|
||||
ctx context.Context
|
||||
orgID string
|
||||
human *domain.Human
|
||||
passwordless bool
|
||||
}
|
||||
type res struct {
|
||||
want *domain.Human
|
||||
err func(error) bool
|
||||
wantHuman *domain.Human
|
||||
wantCode *domain.PasswordlessInitCode
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -869,7 +872,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.Human{
|
||||
wantHuman: &domain.Human{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
@@ -950,7 +953,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.Human{
|
||||
wantHuman: &domain.Human{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
@@ -970,6 +973,218 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human email verified passwordless only, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgIAMPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"code1",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("a"),
|
||||
},
|
||||
time.Hour,
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
passwordlessInitCode: GetMockSecretGenerator(t),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &domain.Human{
|
||||
Username: "username",
|
||||
Profile: &domain.Profile{
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
},
|
||||
Email: &domain.Email{
|
||||
EmailAddress: "email@test.ch",
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
},
|
||||
passwordless: true,
|
||||
},
|
||||
res: res{
|
||||
wantHuman: &domain.Human{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Username: "username",
|
||||
Profile: &domain.Profile{
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
DisplayName: "firstname lastname",
|
||||
PreferredLanguage: language.Und,
|
||||
},
|
||||
Email: &domain.Email{
|
||||
EmailAddress: "email@test.ch",
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
State: domain.UserStateActive,
|
||||
},
|
||||
wantCode: &domain.PasswordlessInitCode{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Expiration: time.Hour,
|
||||
CodeID: "code1",
|
||||
Code: "a",
|
||||
State: domain.PasswordlessInitCodeStateActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human email verified passwordless and password change not required, ok",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(
|
||||
t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewOrgIAMPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
1,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
newAddHumanEvent("password", false, ""),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanEmailVerifiedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"code1",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("a"),
|
||||
},
|
||||
time.Hour,
|
||||
),
|
||||
),
|
||||
},
|
||||
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||
),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1", "code1"),
|
||||
secretGenerator: GetMockSecretGenerator(t),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
passwordlessInitCode: GetMockSecretGenerator(t),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
orgID: "org1",
|
||||
human: &domain.Human{
|
||||
Username: "username",
|
||||
Password: &domain.Password{
|
||||
SecretString: "password",
|
||||
ChangeRequired: false,
|
||||
},
|
||||
Profile: &domain.Profile{
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
},
|
||||
Email: &domain.Email{
|
||||
EmailAddress: "email@test.ch",
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
},
|
||||
passwordless: true,
|
||||
},
|
||||
res: res{
|
||||
wantHuman: &domain.Human{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Username: "username",
|
||||
Profile: &domain.Profile{
|
||||
FirstName: "firstname",
|
||||
LastName: "lastname",
|
||||
DisplayName: "firstname lastname",
|
||||
PreferredLanguage: language.Und,
|
||||
},
|
||||
Email: &domain.Email{
|
||||
EmailAddress: "email@test.ch",
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
State: domain.UserStateActive,
|
||||
},
|
||||
wantCode: &domain.PasswordlessInitCode{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Expiration: time.Hour,
|
||||
CodeID: "code1",
|
||||
Code: "a",
|
||||
State: domain.PasswordlessInitCodeStateActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add human (with phone), ok",
|
||||
fields: fields{
|
||||
@@ -1052,7 +1267,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.Human{
|
||||
wantHuman: &domain.Human{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
@@ -1151,7 +1366,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.Human{
|
||||
wantHuman: &domain.Human{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
@@ -1182,8 +1397,9 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
initializeUserCode: tt.fields.secretGenerator,
|
||||
phoneVerificationCode: tt.fields.secretGenerator,
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
passwordlessInitCode: tt.fields.passwordlessInitCode,
|
||||
}
|
||||
got, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human)
|
||||
gotHuman, gotCode, err := r.ImportHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.passwordless)
|
||||
if tt.res.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -1191,7 +1407,8 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
if tt.res.err == nil {
|
||||
assert.Equal(t, tt.res.want, got)
|
||||
assert.Equal(t, tt.res.wantHuman, gotHuman)
|
||||
assert.Equal(t, tt.res.wantCode, gotCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -2,9 +2,11 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
@@ -127,8 +129,16 @@ func (c *Commands) HumanAddPasswordlessSetup(ctx context.Context, userID, resour
|
||||
return createdWebAuthN, nil
|
||||
}
|
||||
|
||||
func (c *Commands) HumanAddPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, codeID, verificationCode string) (*domain.WebAuthNToken, error) {
|
||||
err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.HumanAddPasswordlessSetup(ctx, userID, resourceowner, true)
|
||||
}
|
||||
|
||||
func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner string, isLoginUI bool, tokens []*domain.WebAuthNToken) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
if userID == "" || resourceowner == "" {
|
||||
if userID == "" {
|
||||
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing")
|
||||
}
|
||||
user, err := c.getHuman(ctx, userID, resourceowner)
|
||||
@@ -198,7 +208,24 @@ func (c *Commands) HumanVerifyU2FSetup(ctx context.Context, userID, resourceowne
|
||||
return writeModelToObjectDetails(&verifyWebAuthN.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) HumanPasswordlessSetupInitCode(ctx context.Context, userID, resourceowner, tokenName, userAgentID, codeID, verificationCode string, credentialData []byte) (*domain.ObjectDetails, error) {
|
||||
err := c.humanVerifyPasswordlessInitCode(ctx, userID, resourceowner, codeID, verificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
succeededEvent := func(userAgg *eventstore.Aggregate) *usr_repo.HumanPasswordlessInitCodeCheckSucceededEvent {
|
||||
return usr_repo.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, codeID)
|
||||
}
|
||||
return c.humanHumanPasswordlessSetup(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, succeededEvent)
|
||||
}
|
||||
|
||||
func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) (*domain.ObjectDetails, error) {
|
||||
return c.humanHumanPasswordlessSetup(ctx, userID, resourceowner, tokenName, userAgentID, credentialData, nil)
|
||||
}
|
||||
|
||||
func (c *Commands) humanHumanPasswordlessSetup(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte,
|
||||
codeCheckEvent func(*eventstore.Aggregate) *usr_repo.HumanPasswordlessInitCodeCheckSucceededEvent) (*domain.ObjectDetails, error) {
|
||||
|
||||
u2fTokens, err := c.getHumanPasswordlessTokens(ctx, userID, resourceowner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -208,7 +235,7 @@ func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, reso
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx,
|
||||
events := []eventstore.EventPusher{
|
||||
usr_repo.NewHumanPasswordlessVerifiedEvent(
|
||||
ctx,
|
||||
userAgg,
|
||||
@@ -221,7 +248,11 @@ func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, reso
|
||||
webAuthN.SignCount,
|
||||
userAgentID,
|
||||
),
|
||||
)
|
||||
}
|
||||
if codeCheckEvent != nil {
|
||||
events = append(events, codeCheckEvent(userAgg))
|
||||
}
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, events...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -233,7 +264,7 @@ func (c *Commands) HumanHumanPasswordlessSetup(ctx context.Context, userID, reso
|
||||
}
|
||||
|
||||
func (c *Commands) verifyHumanWebAuthN(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte, tokens []*domain.WebAuthNToken) (*eventstore.Aggregate, *domain.WebAuthNToken, *HumanWebAuthNWriteModel, error) {
|
||||
if userID == "" || resourceowner == "" {
|
||||
if userID == "" {
|
||||
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0od", "Errors.IDMissing")
|
||||
}
|
||||
user, err := c.getHuman(ctx, userID, resourceowner)
|
||||
@@ -452,6 +483,102 @@ func (c *Commands) HumanRemovePasswordless(ctx context.Context, userID, webAuthN
|
||||
return c.removeHumanWebAuthN(ctx, userID, webAuthNID, resourceOwner, event)
|
||||
}
|
||||
|
||||
func (c *Commands) HumanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string) (*domain.PasswordlessInitCode, error) {
|
||||
codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, true)
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, codeEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(initCode, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToPasswordlessInitCode(initCode, code), nil
|
||||
}
|
||||
|
||||
func (c *Commands) HumanSendPasswordlessInitCode(ctx context.Context, userID, resourceOwner string) (*domain.PasswordlessInitCode, error) {
|
||||
codeEvent, initCode, code, err := c.humanAddPasswordlessInitCode(ctx, userID, resourceOwner, true)
|
||||
pushedEvents, err := c.eventstore.PushEvents(ctx, codeEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = AppendAndReduce(initCode, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToPasswordlessInitCode(initCode, code), nil
|
||||
}
|
||||
|
||||
func (c *Commands) humanAddPasswordlessInitCode(ctx context.Context, userID, resourceOwner string, direct bool) (eventstore.EventPusher, *HumanPasswordlessInitCodeWriteModel, string, error) {
|
||||
if userID == "" {
|
||||
return nil, nil, "", caos_errs.ThrowPreconditionFailed(nil, "COMMAND-GVfg3", "Errors.IDMissing")
|
||||
}
|
||||
|
||||
codeID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
initCode := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, initCode)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
|
||||
cryptoCode, code, err := crypto.NewCode(c.passwordlessInitCode)
|
||||
if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
codeEventCreator := func(ctx context.Context, agg *eventstore.Aggregate, id string, cryptoCode *crypto.CryptoValue, exp time.Duration) eventstore.EventPusher {
|
||||
return usr_repo.NewHumanPasswordlessInitCodeAddedEvent(ctx, agg, id, cryptoCode, exp)
|
||||
}
|
||||
if !direct {
|
||||
codeEventCreator = func(ctx context.Context, agg *eventstore.Aggregate, id string, cryptoCode *crypto.CryptoValue, exp time.Duration) eventstore.EventPusher {
|
||||
return usr_repo.NewHumanPasswordlessInitCodeRequestedEvent(ctx, agg, id, cryptoCode, exp)
|
||||
}
|
||||
}
|
||||
codeEvent := codeEventCreator(ctx, UserAggregateFromWriteModel(&initCode.WriteModel), codeID, cryptoCode, c.passwordlessInitCode.Expiry())
|
||||
return codeEvent, initCode, code, nil
|
||||
}
|
||||
|
||||
func (c *Commands) HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error {
|
||||
if userID == "" || codeID == "" {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-ADggh", "Errors.IDMissing")
|
||||
}
|
||||
initCode := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, initCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if initCode.State != domain.PasswordlessInitCodeStateRequested {
|
||||
return caos_errs.ThrowNotFound(nil, "COMMAND-Gdfg3", "Errors.User.Code.NotFound")
|
||||
}
|
||||
|
||||
_, err = c.eventstore.PushEvents(ctx,
|
||||
usr_repo.NewHumanPasswordlessInitCodeSentEvent(ctx, UserAggregateFromWriteModel(&initCode.WriteModel), codeID),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) humanVerifyPasswordlessInitCode(ctx context.Context, userID, resourceOwner, codeID, verificationCode string) error {
|
||||
if userID == "" || codeID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-GVfg3", "Errors.IDMissing")
|
||||
}
|
||||
initCode := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, initCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = crypto.VerifyCode(initCode.ChangeDate, initCode.Expiration, initCode.CryptoCode, verificationCode, c.passwordlessInitCode)
|
||||
if err != nil || initCode.State != domain.PasswordlessInitCodeStateActive {
|
||||
userAgg := UserAggregateFromWriteModel(&initCode.WriteModel)
|
||||
_, err = c.eventstore.PushEvents(ctx, usr_repo.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, codeID))
|
||||
logging.LogWithFields("COMMAND-Gkuud", "userID", userAgg.ID).OnError(err).Error("NewHumanPasswordlessInitCodeCheckFailedEvent push failed")
|
||||
return caos_errs.ThrowInvalidArgument(err, "COMMAND-Dhz8i", "Errors.User.Code.Invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commands) removeHumanWebAuthN(ctx context.Context, userID, webAuthNID, resourceOwner string, preparedEvent func(*eventstore.Aggregate) eventstore.EventPusher) (*domain.ObjectDetails, error) {
|
||||
if userID == "" || webAuthNID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M9de", "Errors.IDMissing")
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/repository/user"
|
||||
@@ -430,3 +433,106 @@ func (rm *HumanPasswordlessLoginReadModel) Query() *eventstore.SearchQueryBuilde
|
||||
Builder()
|
||||
|
||||
}
|
||||
|
||||
type HumanPasswordlessInitCodeWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
CodeID string
|
||||
Attempts uint8
|
||||
CryptoCode *crypto.CryptoValue
|
||||
Expiration time.Duration
|
||||
State domain.PasswordlessInitCodeState
|
||||
}
|
||||
|
||||
func NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner string) *HumanPasswordlessInitCodeWriteModel {
|
||||
return &HumanPasswordlessInitCodeWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: userID,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
CodeID: codeID,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *HumanPasswordlessInitCodeWriteModel) AppendEvents(events ...eventstore.EventReader) {
|
||||
for _, event := range events {
|
||||
switch e := event.(type) {
|
||||
case *user.HumanPasswordlessInitCodeAddedEvent:
|
||||
if wm.CodeID == e.ID {
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
case *user.HumanPasswordlessInitCodeRequestedEvent:
|
||||
if wm.CodeID == e.ID {
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
case *user.HumanPasswordlessInitCodeSentEvent:
|
||||
if wm.CodeID == e.ID {
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
case *user.HumanPasswordlessInitCodeCheckFailedEvent:
|
||||
if wm.CodeID == e.ID {
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
case *user.HumanPasswordlessInitCodeCheckSucceededEvent:
|
||||
if wm.CodeID == e.ID {
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
case *user.UserRemovedEvent:
|
||||
wm.WriteModel.AppendEvents(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *HumanPasswordlessInitCodeWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *user.HumanPasswordlessInitCodeAddedEvent:
|
||||
wm.appendAddedEvent(e)
|
||||
case *user.HumanPasswordlessInitCodeRequestedEvent:
|
||||
wm.appendRequestedEvent(e)
|
||||
case *user.HumanPasswordlessInitCodeSentEvent:
|
||||
wm.State = domain.PasswordlessInitCodeStateActive
|
||||
case *user.HumanPasswordlessInitCodeCheckFailedEvent:
|
||||
wm.appendCheckFailedEvent(e)
|
||||
case *user.HumanPasswordlessInitCodeCheckSucceededEvent:
|
||||
wm.State = domain.PasswordlessInitCodeStateRemoved
|
||||
case *user.UserRemovedEvent:
|
||||
wm.State = domain.PasswordlessInitCodeStateRemoved
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *HumanPasswordlessInitCodeWriteModel) appendAddedEvent(e *user.HumanPasswordlessInitCodeAddedEvent) {
|
||||
wm.CryptoCode = e.Code
|
||||
wm.Expiration = e.Expiry
|
||||
wm.State = domain.PasswordlessInitCodeStateActive
|
||||
}
|
||||
|
||||
func (wm *HumanPasswordlessInitCodeWriteModel) appendRequestedEvent(e *user.HumanPasswordlessInitCodeRequestedEvent) {
|
||||
wm.CryptoCode = e.Code
|
||||
wm.Expiration = e.Expiry
|
||||
wm.State = domain.PasswordlessInitCodeStateRequested
|
||||
}
|
||||
|
||||
func (wm *HumanPasswordlessInitCodeWriteModel) appendCheckFailedEvent(e *user.HumanPasswordlessInitCodeCheckFailedEvent) {
|
||||
wm.Attempts++
|
||||
if wm.Attempts == 3 { //TODO: config?
|
||||
wm.State = domain.PasswordlessInitCodeStateRemoved
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *HumanPasswordlessInitCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(user.HumanPasswordlessInitCodeAddedType,
|
||||
user.HumanPasswordlessInitCodeRequestedType,
|
||||
user.HumanPasswordlessInitCodeSentType,
|
||||
user.HumanPasswordlessInitCodeCheckFailedType,
|
||||
user.HumanPasswordlessInitCodeCheckSucceededType,
|
||||
user.UserRemovedType).
|
||||
Builder()
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.
|
||||
Text: Dieser Benutzer wurde soeben in ZITADEL erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> 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 <br>(Code <strong>{{.Code}}</strong>).<br> 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 <br>(Code <strong>{{.Code}}</strong>).<br> 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<br>(Code <strong>{{.Code}}</strong>).<br>
|
||||
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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
@@ -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).
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
40
internal/ui/login/handler/passwordless_prompt_handler.go
Normal file
40
internal/ui/login/handler/passwordless_prompt_handler.go
Normal 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)
|
||||
}
|
132
internal/ui/login/handler/passwordless_registration_handler.go
Normal file
132
internal/ui/login/handler/passwordless_registration_handler.go
Normal 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)
|
||||
}
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
25
internal/ui/login/static/templates/passwordless_prompt.html
Normal file
25
internal/ui/login/static/templates/passwordless_prompt.html
Normal 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" .}}
|
@@ -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" .}}
|
||||
|
@@ -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" .}}
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user