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

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