feat: passwordless registration (#2103)

* begin pw less registration

* create pwless one time codes

* send pwless link

* separate send and add passwordless link

* separate send and add passwordless link events

* custom message text for passwordless registration

* begin custom login texts for passwordless

* i18n

* i18n message

* i18n message

* custom message text

* custom login text

* org design and texts

* create link in human import process

* fix import human tests

* begin passwordless init required step

* passwordless init

* passwordless init

* do not return link in mgmt api

* prompt

* passwordless init only (no additional prompt)

* cleanup

* cleanup

* add passwordless prompt to custom login text

* increase init code complexity

* fix grpc

* cleanup

* fix and add some cases for nextStep tests

* fix tests

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

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

* Update proto/zitadel/management.proto

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

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

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

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

View File

@@ -216,7 +216,7 @@ func startAPI(ctx context.Context, conf *Config, verifier *internal_authz.TokenV
apis.RegisterServer(ctx, management.CreateServer(command, query, managementRepo, conf.SystemDefaults))
}
if *authEnabled {
apis.RegisterServer(ctx, auth.CreateServer(command, query, authRepo))
apis.RegisterServer(ctx, auth.CreateServer(command, query, authRepo, conf.SystemDefaults))
}
if *oidcEnabled {
op := oidc.NewProvider(ctx, conf.API.OIDC, command, query, authRepo, conf.SystemDefaults.KeyConfig.EncryptionConfig, *localDevMode)

View File

@@ -44,6 +44,13 @@ SystemDefaults:
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
PasswordlessInitCode:
Length: 12
Expiry: '1h'
IncludeLowerLetters: true
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
MachineKeySize: 2048
ApplicationKeySize: 2048
Multifactors:
@@ -74,6 +81,7 @@ SystemDefaults:
PasswordReset: '$ZITADEL_ACCOUNTS/password/init?userID={{.UserID}}&code={{.Code}}'
VerifyEmail: '$ZITADEL_ACCOUNTS/mail/verification?userID={{.UserID}}&code={{.Code}}'
DomainClaimed: '$ZITADEL_ACCOUNTS/login'
PasswordlessRegistration: '$ZITADEL_ACCOUNTS/login/passwordless/init'
Providers:
Chat:
Url: $CHAT_URL

View File

@@ -851,13 +851,52 @@ Returns the custom text for domain claimed message (overwritten in eventstore)
[SetDefaultDomainClaimedMessageTextResponse](#setdefaultdomainclaimedmessagetextresponse)
Sets the default custom text for domain claimed phone message
it impacts all organisations without customized verify phone message text
it impacts all organisations without customized domain claimed message text
The Following Variables can be used:
{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
PUT: /text/message/verifyphone/{language}
PUT: /text/message/domainclaimed/{language}
### GetDefaultPasswordlessRegistrationMessageText
> **rpc** GetDefaultPasswordlessRegistrationMessageText([GetDefaultPasswordlessRegistrationMessageTextRequest](#getdefaultpasswordlessregistrationmessagetextrequest))
[GetDefaultPasswordlessRegistrationMessageTextResponse](#getdefaultpasswordlessregistrationmessagetextresponse)
Returns the default text for passwordless registration message (translation file)
GET: /text/default/message/passwordless_registration/{language}
### GetCustomPasswordlessRegistrationMessageText
> **rpc** GetCustomPasswordlessRegistrationMessageText([GetCustomPasswordlessRegistrationMessageTextRequest](#getcustompasswordlessregistrationmessagetextrequest))
[GetCustomPasswordlessRegistrationMessageTextResponse](#getcustompasswordlessregistrationmessagetextresponse)
Returns the custom text for passwordless registration message (overwritten in eventstore)
GET: /text/message/passwordless_registration/{language}
### SetDefaultPasswordlessRegistrationMessageText
> **rpc** SetDefaultPasswordlessRegistrationMessageText([SetDefaultPasswordlessRegistrationMessageTextRequest](#setdefaultpasswordlessregistrationmessagetextrequest))
[SetDefaultPasswordlessRegistrationMessageTextResponse](#setdefaultpasswordlessregistrationmessagetextresponse)
Sets the default custom text for passwordless registration message
it impacts all organisations without customized passwordless registration message text
The Following Variables can be used:
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
PUT: /text/message/passwordless_registration/{language}
### GetDefaultLoginTexts
@@ -1366,6 +1405,28 @@ This is an empty response
### GetCustomPasswordlessRegistrationMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetCustomPasswordlessRegistrationMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetCustomVerifyEmailMessageTextRequest
@@ -1515,6 +1576,28 @@ This is an empty response
### GetDefaultPasswordlessRegistrationMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetDefaultPasswordlessRegistrationMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetDefaultVerifyEmailMessageTextRequest
@@ -2351,6 +2434,9 @@ This is an empty request
| success_login_text | zitadel.text.v1.SuccessLoginScreenText | - | |
| logout_text | zitadel.text.v1.LogoutDoneScreenText | - | |
| footer_text | zitadel.text.v1.FooterText | - | |
| passwordless_prompt_text | zitadel.text.v1.PasswordlessPromptScreenText | - | |
| passwordless_registration_text | zitadel.text.v1.PasswordlessRegistrationScreenText | - | |
| passwordless_registration_done_text | zitadel.text.v1.PasswordlessRegistrationDoneScreenText | - | |
@@ -2490,6 +2576,35 @@ This is an empty request
### SetDefaultPasswordlessRegistrationMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| title | string | - | string.max_len: 200<br /> |
| pre_header | string | - | string.max_len: 200<br /> |
| subject | string | - | string.max_len: 200<br /> |
| greeting | string | - | string.max_len: 200<br /> |
| text | string | - | string.max_len: 800<br /> |
| button_text | string | - | string.max_len: 200<br /> |
| footer_text | string | - | string.max_len: 200<br /> |
### SetDefaultPasswordlessRegistrationMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### SetDefaultVerifyEmailMessageTextRequest

View File

@@ -401,7 +401,7 @@ Removes the U2F Authentication from the authorized user
> **rpc** ListMyPasswordless([ListMyPasswordlessRequest](#listmypasswordlessrequest))
[ListMyPasswordlessResponse](#listmypasswordlessresponse)
Returns all configured passwordless authentications of the authorized user
Returns all configured passwordless authenticators of the authorized user
@@ -413,7 +413,7 @@ Returns all configured passwordless authentications of the authorized user
> **rpc** AddMyPasswordless([AddMyPasswordlessRequest](#addmypasswordlessrequest))
[AddMyPasswordlessResponse](#addmypasswordlessresponse)
Adds a new passwordless authentications to the authorized user
Adds a new passwordless authenticator to the authorized user
Multiple passwordless authentications can be configured
@@ -421,6 +421,34 @@ Multiple passwordless authentications can be configured
POST: /users/me/passwordless
### AddMyPasswordlessLink
> **rpc** AddMyPasswordlessLink([AddMyPasswordlessLinkRequest](#addmypasswordlesslinkrequest))
[AddMyPasswordlessLinkResponse](#addmypasswordlesslinkresponse)
Adds a new passwordless authenticator link to the authorized user and returns it directly
This link enables the user to register a new device if current passwordless devices are all platform authenticators
e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone
POST: /users/me/passwordless/_link
### SendMyPasswordlessLink
> **rpc** SendMyPasswordlessLink([SendMyPasswordlessLinkRequest](#sendmypasswordlesslinkrequest))
[SendMyPasswordlessLinkResponse](#sendmypasswordlesslinkresponse)
Adds a new passwordless authenticator link to the authorized user and sends it to the registered email address
This link enables the user to register a new device if current passwordless devices are all platform authenticators
e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone
POST: /users/me/passwordless/_send_link
### VerifyMyPasswordless
> **rpc** VerifyMyPasswordless([VerifyMyPasswordlessRequest](#verifymypasswordlessrequest))
@@ -550,6 +578,25 @@ This is an empty request
### AddMyPasswordlessLinkRequest
This is an empty request
### AddMyPasswordlessLinkResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
| link | string | - | |
| expiration | google.protobuf.Duration | - | |
### AddMyPasswordlessRequest
This is an empty request
@@ -1086,6 +1133,23 @@ This is an empty response
### SendMyPasswordlessLinkRequest
This is an empty request
### SendMyPasswordlessLinkResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### SetMyEmailRequest

View File

@@ -450,19 +450,33 @@ The u2f (universial second factor) will be removed from the user
> **rpc** ListHumanPasswordless([ListHumanPasswordlessRequest](#listhumanpasswordlessrequest))
[ListHumanPasswordlessResponse](#listhumanpasswordlessresponse)
Returns all configured passwordless authentications
Returns all configured passwordless authenticators
POST: /users/{user_id}/passwordless/_search
### SendPasswordlessRegistration
> **rpc** SendPasswordlessRegistration([SendPasswordlessRegistrationRequest](#sendpasswordlessregistrationrequest))
[SendPasswordlessRegistrationResponse](#sendpasswordlessregistrationresponse)
Adds a new passwordless authenticator link to the user and sends it to the registered email address
This link enables the user to register a new device if current passwordless devices are all platform authenticators
e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone
POST: /users/{user_id}/passwordless/_send_link
### RemoveHumanPasswordless
> **rpc** RemoveHumanPasswordless([RemoveHumanPasswordlessRequest](#removehumanpasswordlessrequest))
[RemoveHumanPasswordlessResponse](#removehumanpasswordlessresponse)
Removed a configured passwordless authentication
Removed a configured passwordless authenticator
@@ -2144,8 +2158,7 @@ Returns the default text for initial message
> **rpc** SetCustomInitMessageText([SetCustomInitMessageTextRequest](#setcustominitmessagetextrequest))
[SetCustomInitMessageTextResponse](#setcustominitmessagetextresponse)
Sets the default custom text for initial message
it impacts all organisations without customized initial message text
Sets the custom text for initial message
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
@@ -2196,8 +2209,7 @@ Returns the default text for password reset message
> **rpc** SetCustomPasswordResetMessageText([SetCustomPasswordResetMessageTextRequest](#setcustompasswordresetmessagetextrequest))
[SetCustomPasswordResetMessageTextResponse](#setcustompasswordresetmessagetextresponse)
Sets the default custom text for password reset message
it impacts all organisations without customized password reset message text
Sets the custom text for password reset message
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
@@ -2248,8 +2260,7 @@ Returns the default text for verify email message
> **rpc** SetCustomVerifyEmailMessageText([SetCustomVerifyEmailMessageTextRequest](#setcustomverifyemailmessagetextrequest))
[SetCustomVerifyEmailMessageTextResponse](#setcustomverifyemailmessagetextresponse)
Sets the default custom text for verify email message
it impacts all organisations without customized verify email message text
Sets the custom text for verify email message
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
@@ -2301,7 +2312,6 @@ Returns the custom text for verify email message
[SetCustomVerifyPhoneMessageTextResponse](#setcustomverifyphonemessagetextresponse)
Sets the default custom text for verify email message
it impacts all organisations without customized verify email message text
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
@@ -2352,8 +2362,7 @@ Returns the custom text for domain claimed message
> **rpc** SetCustomDomainClaimedMessageCustomText([SetCustomDomainClaimedMessageTextRequest](#setcustomdomainclaimedmessagetextrequest))
[SetCustomDomainClaimedMessageTextResponse](#setcustomdomainclaimedmessagetextresponse)
Sets the default custom text for domain claimed message
it impacts all organisations without customized domain claimed message text
Sets the custom text for domain claimed message
The Following Variables can be used:
{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
@@ -2367,7 +2376,7 @@ The Following Variables can be used:
> **rpc** ResetCustomDomainClaimedMessageTextToDefault([ResetCustomDomainClaimedMessageTextToDefaultRequest](#resetcustomdomainclaimedmessagetexttodefaultrequest))
[ResetCustomDomainClaimedMessageTextToDefaultResponse](#resetcustomdomainclaimedmessagetexttodefaultresponse)
Removes the custom init message text of the organisation
Removes the custom domain claimed message text of the organisation
The default text of the IAM will trigger after
@@ -2375,6 +2384,57 @@ The default text of the IAM will trigger after
DELETE: /text/message/domainclaimed/{language}
### GetCustomPasswordlessRegistrationMessageText
> **rpc** GetCustomPasswordlessRegistrationMessageText([GetCustomPasswordlessRegistrationMessageTextRequest](#getcustompasswordlessregistrationmessagetextrequest))
[GetCustomPasswordlessRegistrationMessageTextResponse](#getcustompasswordlessregistrationmessagetextresponse)
Returns the custom text for passwordless link message
GET: /text/message/passwordless_registration/{language}
### GetDefaultPasswordlessRegistrationMessageText
> **rpc** GetDefaultPasswordlessRegistrationMessageText([GetDefaultPasswordlessRegistrationMessageTextRequest](#getdefaultpasswordlessregistrationmessagetextrequest))
[GetDefaultPasswordlessRegistrationMessageTextResponse](#getdefaultpasswordlessregistrationmessagetextresponse)
Returns the custom text for passwordless link message
GET: /text/default/message/passwordless_registration/{language}
### SetCustomPasswordlessRegistrationMessageCustomText
> **rpc** SetCustomPasswordlessRegistrationMessageCustomText([SetCustomPasswordlessRegistrationMessageTextRequest](#setcustompasswordlessregistrationmessagetextrequest))
[SetCustomPasswordlessRegistrationMessageTextResponse](#setcustompasswordlessregistrationmessagetextresponse)
Sets the custom text for passwordless link message
The Following Variables can be used:
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
PUT: /text/message/passwordless_registration/{language}
### ResetCustomPasswordlessRegistrationMessageTextToDefault
> **rpc** ResetCustomPasswordlessRegistrationMessageTextToDefault([ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest](#resetcustompasswordlessregistrationmessagetexttodefaultrequest))
[ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse](#resetcustompasswordlessregistrationmessagetexttodefaultresponse)
Removes the custom passwordless link message text of the organisation
The default text of the IAM will trigger after
DELETE: /text/message/passwordless_registration/{language}
### GetCustomLoginTexts
> **rpc** GetCustomLoginTexts([GetCustomLoginTextsRequest](#getcustomlogintextsrequest))
@@ -3598,6 +3658,28 @@ This is an empty request
### GetCustomPasswordlessRegistrationMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetCustomPasswordlessRegistrationMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetCustomVerifyEmailMessageTextRequest
@@ -3815,6 +3897,28 @@ This is an empty request
### GetDefaultPasswordlessRegistrationMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetDefaultPasswordlessRegistrationMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetDefaultPrivacyPolicyRequest
This is an empty request
@@ -4413,6 +4517,7 @@ This is an empty response
| phone | ImportHumanUserRequest.Phone | - | |
| password | string | - | |
| password_change_required | bool | - | |
| request_passwordless_registration | bool | - | |
@@ -4465,6 +4570,19 @@ This is an empty response
| ----- | ---- | ----------- | ----------- |
| user_id | string | - | |
| details | zitadel.v1.ObjectDetails | - | |
| passwordless_registration | ImportHumanUserResponse.PasswordlessRegistration | - | |
### ImportHumanUserResponse.PasswordlessRegistration
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| link | string | - | |
| lifetime | google.protobuf.Duration | - | |
@@ -6127,6 +6245,28 @@ This is an empty request
### ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest
This is an empty request
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### ResetCustomVerifyEmailMessageTextToDefaultRequest
@@ -6296,6 +6436,28 @@ This is an empty request
### SendPasswordlessRegistrationRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| user_id | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### SendPasswordlessRegistrationResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### SetCustomDomainClaimedMessageTextRequest
@@ -6391,6 +6553,9 @@ This is an empty request
| success_login_text | zitadel.text.v1.SuccessLoginScreenText | - | |
| logout_text | zitadel.text.v1.LogoutDoneScreenText | - | |
| footer_text | zitadel.text.v1.FooterText | - | |
| passwordless_prompt_text | zitadel.text.v1.PasswordlessPromptScreenText | - | |
| passwordless_registration_text | zitadel.text.v1.PasswordlessRegistrationScreenText | - | |
| passwordless_registration_done_text | zitadel.text.v1.PasswordlessRegistrationDoneScreenText | - | |
@@ -6435,6 +6600,35 @@ This is an empty request
### SetCustomPasswordlessRegistrationMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| title | string | - | string.max_len: 200<br /> |
| pre_header | string | - | string.max_len: 200<br /> |
| subject | string | - | string.max_len: 200<br /> |
| greeting | string | - | string.max_len: 200<br /> |
| text | string | - | string.max_len: 800<br /> |
| button_text | string | - | string.max_len: 200<br /> |
| footer_text | string | - | string.max_len: 200<br /> |
### SetCustomPasswordlessRegistrationMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### SetCustomVerifyEmailMessageTextRequest

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
ALTER TABLE adminapi.users ADD COLUMN passwordless_init_required boolean;
ALTER TABLE auth.users ADD COLUMN passwordless_init_required boolean;
ALTER TABLE management.users ADD COLUMN passwordless_init_required boolean;
ALTER TABLE adminapi.users ADD COLUMN password_init_required BOOLEAN;
UPDATE adminapi.users set password_init_required = NOT password_set;
ALTER TABLE auth.users ADD COLUMN password_init_required BOOLEAN;
UPDATE auth.users set password_init_required = NOT password_set;
ALTER TABLE management.users ADD COLUMN password_init_required BOOLEAN;
UPDATE management.users set password_init_required = NOT password_set;

View File

@@ -1692,12 +1692,49 @@ service AdminService {
}
//Sets the default custom text for domain claimed phone message
// it impacts all organisations without customized verify phone message text
// it impacts all organisations without customized domain claimed message text
// The Following Variables can be used:
// {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetDefaultDomainClaimedMessageText(SetDefaultDomainClaimedMessageTextRequest) returns (SetDefaultDomainClaimedMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/verifyphone/{language}";
put: "/text/message/domainclaimed/{language}";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write";
};
}
//Returns the default text for passwordless registration message (translation file)
rpc GetDefaultPasswordlessRegistrationMessageText(GetDefaultPasswordlessRegistrationMessageTextRequest) returns (GetDefaultPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
get: "/text/default/message/passwordless_registration/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
}
//Returns the custom text for passwordless registration message (overwritten in eventstore)
rpc GetCustomPasswordlessRegistrationMessageText(GetCustomPasswordlessRegistrationMessageTextRequest) returns (GetCustomPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/passwordless_registration/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
}
//Sets the default custom text for passwordless registration message
// it impacts all organisations without customized passwordless registration message text
// The Following Variables can be used:
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetDefaultPasswordlessRegistrationMessageText(SetDefaultPasswordlessRegistrationMessageTextRequest) returns (SetDefaultPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/passwordless_registration/{language}";
body: "*";
};
@@ -3267,6 +3304,42 @@ message SetDefaultDomainClaimedMessageTextResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultPasswordlessRegistrationMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetDefaultPasswordlessRegistrationMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message GetCustomPasswordlessRegistrationMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetCustomPasswordlessRegistrationMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message SetDefaultPasswordlessRegistrationMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"de\""
}
];
string title = 2 [(validate.rules).string = {max_len: 200}];
string pre_header = 3 [(validate.rules).string = {max_len: 200}];
string subject = 4 [(validate.rules).string = {max_len: 200}];
string greeting = 5 [(validate.rules).string = {max_len: 200}];
string text = 6 [(validate.rules).string = {max_len: 800}];
string button_text = 7 [(validate.rules).string = {max_len: 200}];
string footer_text = 8 [(validate.rules).string = {max_len: 200}];
}
message SetDefaultPasswordlessRegistrationMessageTextResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultLoginTextsRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
@@ -3320,6 +3393,9 @@ message SetCustomLoginTextsRequest {
zitadel.text.v1.SuccessLoginScreenText success_login_text = 29;
zitadel.text.v1.LogoutDoneScreenText logout_text = 30;
zitadel.text.v1.FooterText footer_text = 31;
zitadel.text.v1.PasswordlessPromptScreenText passwordless_prompt_text = 32;
zitadel.text.v1.PasswordlessRegistrationScreenText passwordless_registration_text = 33;
zitadel.text.v1.PasswordlessRegistrationDoneScreenText passwordless_registration_done_text = 34;
}
message SetCustomLoginTextsResponse {

View File

@@ -9,6 +9,7 @@ import "zitadel/policy.proto";
import "zitadel/idp.proto";
import "validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
@@ -407,7 +408,7 @@ service AuthService {
};
}
// Returns all configured passwordless authentications of the authorized user
// Returns all configured passwordless authenticators of the authorized user
rpc ListMyPasswordless(ListMyPasswordlessRequest) returns (ListMyPasswordlessResponse) {
option (google.api.http) = {
post: "/users/me/passwordless/_search"
@@ -417,7 +418,7 @@ service AuthService {
};
}
// Adds a new passwordless authentications to the authorized user
// Adds a new passwordless authenticator to the authorized user
// Multiple passwordless authentications can be configured
rpc AddMyPasswordless(AddMyPasswordlessRequest) returns (AddMyPasswordlessResponse) {
option (google.api.http) = {
@@ -429,6 +430,32 @@ service AuthService {
};
}
// Adds a new passwordless authenticator link to the authorized user and returns it directly
// This link enables the user to register a new device if current passwordless devices are all platform authenticators
// e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone
rpc AddMyPasswordlessLink(AddMyPasswordlessLinkRequest) returns (AddMyPasswordlessLinkResponse) {
option (google.api.http) = {
post: "/users/me/passwordless/_link"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Adds a new passwordless authenticator link to the authorized user and sends it to the registered email address
// This link enables the user to register a new device if current passwordless devices are all platform authenticators
// e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone
rpc SendMyPasswordlessLink(SendMyPasswordlessLinkRequest) returns (SendMyPasswordlessLinkResponse) {
option (google.api.http) = {
post: "/users/me/passwordless/_send_link"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Verifies the last added passwordless configuration
rpc VerifyMyPasswordless(VerifyMyPasswordlessRequest) returns (VerifyMyPasswordlessResponse) {
option (google.api.http) = {
@@ -786,6 +813,22 @@ message AddMyPasswordlessResponse {
zitadel.v1.ObjectDetails details = 2;
}
//This is an empty request
message AddMyPasswordlessLinkRequest {}
message AddMyPasswordlessLinkResponse {
zitadel.v1.ObjectDetails details = 1;
string link = 2;
google.protobuf.Duration expiration = 3;
}
//This is an empty request
message SendMyPasswordlessLinkRequest {}
message SendMyPasswordlessLinkResponse {
zitadel.v1.ObjectDetails details = 1;
}
message VerifyMyPasswordlessRequest {
zitadel.user.v1.WebAuthNVerification verification = 1 [(validate.rules).message.required = true];
}

View File

@@ -485,7 +485,7 @@ service ManagementService {
};
}
// Returns all configured passwordless authentications
// Returns all configured passwordless authenticators
rpc ListHumanPasswordless(ListHumanPasswordlessRequest) returns (ListHumanPasswordlessResponse) {
option (google.api.http) = {
post: "/users/{user_id}/passwordless/_search"
@@ -496,7 +496,20 @@ service ManagementService {
};
}
// Removed a configured passwordless authentication
// Adds a new passwordless authenticator link to the user and sends it to the registered email address
// This link enables the user to register a new device if current passwordless devices are all platform authenticators
// e.g. User has already registered Windows Hello and wants to register FaceID on the iPhone
rpc SendPasswordlessRegistration(SendPasswordlessRegistrationRequest) returns (SendPasswordlessRegistrationResponse) {
option (google.api.http) = {
post: "/users/{user_id}/passwordless/_send_link"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "authenticated"
};
}
// Removed a configured passwordless authenticator
rpc RemoveHumanPasswordless(RemoveHumanPasswordlessRequest) returns (RemoveHumanPasswordlessResponse) {
option (google.api.http) = {
delete: "/users/{user_id}/passwordless/{token_id}"
@@ -2175,8 +2188,7 @@ service ManagementService {
};
}
//Sets the default custom text for initial message
// it impacts all organisations without customized initial message text
// Sets the custom text for initial message
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetCustomInitMessageText(SetCustomInitMessageTextRequest) returns (SetCustomInitMessageTextResponse) {
@@ -2224,8 +2236,7 @@ service ManagementService {
};
}
//Sets the default custom text for password reset message
// it impacts all organisations without customized password reset message text
// Sets the custom text for password reset message
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetCustomPasswordResetMessageText(SetCustomPasswordResetMessageTextRequest) returns (SetCustomPasswordResetMessageTextResponse) {
@@ -2274,8 +2285,7 @@ service ManagementService {
};
}
//Sets the default custom text for verify email message
// it impacts all organisations without customized verify email message text
// Sets the custom text for verify email message
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetCustomVerifyEmailMessageText(SetCustomVerifyEmailMessageTextRequest) returns (SetCustomVerifyEmailMessageTextResponse) {
@@ -2324,8 +2334,7 @@ service ManagementService {
};
}
//Sets the default custom text for verify email message
// it impacts all organisations without customized verify email message text
// Sets the default custom text for verify email message
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetCustomVerifyPhoneMessageText(SetCustomVerifyPhoneMessageTextRequest) returns (SetCustomVerifyPhoneMessageTextResponse) {
@@ -2374,8 +2383,7 @@ service ManagementService {
};
}
// Sets the default custom text for domain claimed message
// it impacts all organisations without customized domain claimed message text
// Sets the custom text for domain claimed message
// The Following Variables can be used:
// {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetCustomDomainClaimedMessageCustomText(SetCustomDomainClaimedMessageTextRequest) returns (SetCustomDomainClaimedMessageTextResponse) {
@@ -2390,7 +2398,7 @@ service ManagementService {
};
}
// Removes the custom init message text of the organisation
// Removes the custom domain claimed message text of the organisation
// The default text of the IAM will trigger after
rpc ResetCustomDomainClaimedMessageTextToDefault(ResetCustomDomainClaimedMessageTextToDefaultRequest) returns (ResetCustomDomainClaimedMessageTextToDefaultResponse) {
option (google.api.http) = {
@@ -2402,6 +2410,55 @@ service ManagementService {
};
}
//Returns the custom text for passwordless link message
rpc GetCustomPasswordlessRegistrationMessageText(GetCustomPasswordlessRegistrationMessageTextRequest) returns (GetCustomPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/passwordless_registration/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "policy.read";
};
}
//Returns the custom text for passwordless link message
rpc GetDefaultPasswordlessRegistrationMessageText(GetDefaultPasswordlessRegistrationMessageTextRequest) returns (GetDefaultPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
get: "/text/default/message/passwordless_registration/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "policy.read";
};
}
// Sets the custom text for passwordless link message
// The Following Variables can be used:
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
rpc SetCustomPasswordlessRegistrationMessageCustomText(SetCustomPasswordlessRegistrationMessageTextRequest) returns (SetCustomPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/passwordless_registration/{language}";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "policy.write";
feature: "custom_text"
};
}
// Removes the custom passwordless link message text of the organisation
// The default text of the IAM will trigger after
rpc ResetCustomPasswordlessRegistrationMessageTextToDefault(ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest) returns (ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse) {
option (google.api.http) = {
delete: "/text/message/passwordless_registration/{language}"
};
option (zitadel.v1.auth_option) = {
permission: "policy.delete"
};
}
//Returns the custom texts for login ui
rpc GetCustomLoginTexts(GetCustomLoginTextsRequest) returns (GetCustomLoginTextsResponse) {
option (google.api.http) = {
@@ -2695,11 +2752,18 @@ message ImportHumanUserRequest {
Phone phone = 4;
string password = 5;
bool password_change_required = 6;
bool request_passwordless_registration = 7;
}
message ImportHumanUserResponse {
message PasswordlessRegistration {
string link = 1;
google.protobuf.Duration lifetime = 2;
}
string user_id = 1;
zitadel.v1.ObjectDetails details = 2;
PasswordlessRegistration passwordless_registration = 3;
}
message AddMachineUserRequest {
@@ -2934,6 +2998,14 @@ message ListHumanPasswordlessResponse {
repeated zitadel.user.v1.WebAuthNToken result = 1;
}
message SendPasswordlessRegistrationRequest {
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message SendPasswordlessRegistrationResponse {
zitadel.v1.ObjectDetails details = 1;
}
message RemoveHumanPasswordlessRequest {
string user_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string token_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];
@@ -4354,6 +4426,9 @@ message SetCustomLoginTextsRequest {
zitadel.text.v1.SuccessLoginScreenText success_login_text = 29;
zitadel.text.v1.LogoutDoneScreenText logout_text = 30;
zitadel.text.v1.FooterText footer_text = 31;
zitadel.text.v1.PasswordlessPromptScreenText passwordless_prompt_text = 32;
zitadel.text.v1.PasswordlessRegistrationScreenText passwordless_registration_text = 33;
zitadel.text.v1.PasswordlessRegistrationDoneScreenText passwordless_registration_done_text = 34;
}
message SetCustomLoginTextsResponse {
@@ -4544,6 +4619,50 @@ message ResetCustomDomainClaimedMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetCustomPasswordlessRegistrationMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetCustomPasswordlessRegistrationMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message GetDefaultPasswordlessRegistrationMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetDefaultPasswordlessRegistrationMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message SetCustomPasswordlessRegistrationMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"de\""
}
];
string title = 2 [(validate.rules).string = {max_len: 200}];
string pre_header = 3 [(validate.rules).string = {max_len: 200}];
string subject = 4 [(validate.rules).string = {max_len: 200}];
string greeting = 5 [(validate.rules).string = {max_len: 200}];
string text = 6 [(validate.rules).string = {max_len: 800}];
string button_text = 7 [(validate.rules).string = {max_len: 200}];
string footer_text = 8 [(validate.rules).string = {max_len: 200}];
}
message SetCustomPasswordlessRegistrationMessageTextResponse {
zitadel.v1.ObjectDetails details = 1;
}
message ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetOrgIDPByIDRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}

View File

@@ -79,6 +79,9 @@ message LoginCustomText {
SuccessLoginScreenText success_login_text = 29;
LogoutDoneScreenText logout_text = 30;
FooterText footer_text = 31;
PasswordlessPromptScreenText passwordless_prompt_text = 32;
PasswordlessRegistrationScreenText passwordless_registration_text = 33;
PasswordlessRegistrationDoneScreenText passwordless_registration_done_text = 34;
}
message SelectAccountScreenText {
@@ -359,3 +362,27 @@ message FooterText {
string help = 5 [(validate.rules).string = {max_len: 200}];
string help_link = 6 [(validate.rules).string = {max_len: 500}];
}
message PasswordlessPromptScreenText {
string title = 1 [(validate.rules).string = {max_len: 200}];
string description = 2 [(validate.rules).string = {max_len: 500}];
string description_init = 3 [(validate.rules).string = {max_len: 500}];
string passwordless_button_text = 4 [(validate.rules).string = {max_len: 100}];
string next_button_text = 5 [(validate.rules).string = {max_len: 100}];
string skip_button_text = 6 [(validate.rules).string = {max_len: 100}];
}
message PasswordlessRegistrationScreenText {
string title = 1 [(validate.rules).string = {max_len: 200}];
string description = 2 [(validate.rules).string = {max_len: 500}];
string token_name_label = 3 [(validate.rules).string = {max_len: 200}];
string not_supported = 4 [(validate.rules).string = {max_len: 500}];
string register_token_button_text = 5 [(validate.rules).string = {max_len: 100}];
string error_retry = 6 [(validate.rules).string = {max_len: 500}];
}
message PasswordlessRegistrationDoneScreenText {
string title = 1 [(validate.rules).string = {max_len: 200}];
string description = 2 [(validate.rules).string = {max_len: 500}];
string next_button_text = 3 [(validate.rules).string = {max_len: 100}];
}