mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:27:42 +00:00
feat: passwordless registration (#2103)
* begin pw less registration * create pwless one time codes * send pwless link * separate send and add passwordless link * separate send and add passwordless link events * custom message text for passwordless registration * begin custom login texts for passwordless * i18n * i18n message * i18n message * custom message text * custom login text * org design and texts * create link in human import process * fix import human tests * begin passwordless init required step * passwordless init * passwordless init * do not return link in mgmt api * prompt * passwordless init only (no additional prompt) * cleanup * cleanup * add passwordless prompt to custom login text * increase init code complexity * fix grpc * cleanup * fix and add some cases for nextStep tests * fix tests * Update internal/notification/static/i18n/en.yaml * Update internal/notification/static/i18n/de.yaml * Update proto/zitadel/management.proto * Update internal/ui/login/static/i18n/de.yaml * Update internal/ui/login/static/i18n/de.yaml * Update internal/ui/login/static/i18n/de.yaml Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,10 @@ type AuthRequestRepository interface {
|
||||
VerifyMFAOTP(ctx context.Context, authRequestID, userID, resourceOwner, code, userAgentID string, info *domain.BrowserInfo) error
|
||||
BeginMFAU2FLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (*domain.WebAuthNLogin, error)
|
||||
VerifyMFAU2F(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string, credentialData []byte, info *domain.BrowserInfo) error
|
||||
BeginPasswordlessSetup(ctx context.Context, userID, resourceOwner string) (login *domain.WebAuthNToken, err error)
|
||||
VerifyPasswordlessSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName string, credentialData []byte) (err error)
|
||||
BeginPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, codeID, verificationCode string) (login *domain.WebAuthNToken, err error)
|
||||
VerifyPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode string, credentialData []byte) (err error)
|
||||
BeginPasswordlessLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (*domain.WebAuthNLogin, error)
|
||||
VerifyPasswordless(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string, credentialData []byte, info *domain.BrowserInfo) error
|
||||
|
||||
|
@@ -296,6 +296,32 @@ func (repo *AuthRequestRepo) VerifyMFAU2F(ctx context.Context, userID, resourceO
|
||||
return repo.Command.HumanFinishU2FLogin(ctx, userID, resourceOwner, credentialData, request, true)
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) BeginPasswordlessSetup(ctx context.Context, userID, resourceOwner string) (login *domain.WebAuthNToken, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
return repo.Command.HumanAddPasswordlessSetup(ctx, userID, resourceOwner, true)
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) VerifyPasswordlessSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName string, credentialData []byte) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
_, err = repo.Command.HumanHumanPasswordlessSetup(ctx, userID, resourceOwner, tokenName, userAgentID, credentialData)
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) BeginPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, codeID, verificationCode string) (login *domain.WebAuthNToken, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
return repo.Command.HumanAddPasswordlessSetupInitCode(ctx, userID, resourceOwner, codeID, verificationCode)
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) VerifyPasswordlessInitCodeSetup(ctx context.Context, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode string, credentialData []byte) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
_, err = repo.Command.HumanPasswordlessSetupInitCode(ctx, userID, resourceOwner, userAgentID, tokenName, codeID, verificationCode, credentialData)
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) BeginPasswordlessLogin(ctx context.Context, userID, resourceOwner, authRequestID, userAgentID string) (login *domain.WebAuthNLogin, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
@@ -610,7 +636,6 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
|
||||
|
||||
if request.LinkingUsers != nil && len(request.LinkingUsers) != 0 {
|
||||
return append(steps, &domain.LinkUsersStep{}), nil
|
||||
|
||||
}
|
||||
//PLANNED: consent step
|
||||
|
||||
@@ -657,10 +682,16 @@ func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, use
|
||||
request.AuthTime = userSession.PasswordlessVerification
|
||||
return nil
|
||||
}
|
||||
step = &domain.PasswordlessStep{}
|
||||
step = &domain.PasswordlessStep{
|
||||
PasswordSet: user.PasswordSet,
|
||||
}
|
||||
}
|
||||
|
||||
if !user.PasswordSet {
|
||||
if user.PasswordlessInitRequired {
|
||||
return &domain.PasswordlessRegistrationPromptStep{}
|
||||
}
|
||||
|
||||
if user.PasswordInitRequired {
|
||||
return &domain.InitPasswordStep{}
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
|
||||
@@ -131,14 +132,16 @@ func (m *mockEventErrUser) BulkAddExternalIDPs(ctx context.Context, userID strin
|
||||
}
|
||||
|
||||
type mockViewUser struct {
|
||||
InitRequired bool
|
||||
PasswordSet bool
|
||||
PasswordChangeRequired bool
|
||||
IsEmailVerified bool
|
||||
OTPState int32
|
||||
MFAMaxSetUp int32
|
||||
MFAInitSkipped time.Time
|
||||
PasswordlessTokens user_view_model.WebAuthNTokens
|
||||
InitRequired bool
|
||||
PasswordInitRequired bool
|
||||
PasswordSet bool
|
||||
PasswordChangeRequired bool
|
||||
IsEmailVerified bool
|
||||
OTPState int32
|
||||
MFAMaxSetUp int32
|
||||
MFAInitSkipped time.Time
|
||||
PasswordlessInitRequired bool
|
||||
PasswordlessTokens user_view_model.WebAuthNTokens
|
||||
}
|
||||
|
||||
type mockLoginPolicy struct {
|
||||
@@ -154,15 +157,17 @@ func (m *mockViewUser) UserByID(string) (*user_view_model.UserView, error) {
|
||||
State: int32(user_model.UserStateActive),
|
||||
UserName: "UserName",
|
||||
HumanView: &user_view_model.HumanView{
|
||||
FirstName: "FirstName",
|
||||
InitRequired: m.InitRequired,
|
||||
PasswordSet: m.PasswordSet,
|
||||
PasswordChangeRequired: m.PasswordChangeRequired,
|
||||
IsEmailVerified: m.IsEmailVerified,
|
||||
OTPState: m.OTPState,
|
||||
MFAMaxSetUp: m.MFAMaxSetUp,
|
||||
MFAInitSkipped: m.MFAInitSkipped,
|
||||
PasswordlessTokens: m.PasswordlessTokens,
|
||||
FirstName: "FirstName",
|
||||
InitRequired: m.InitRequired,
|
||||
PasswordInitRequired: m.PasswordInitRequired,
|
||||
PasswordSet: m.PasswordSet,
|
||||
PasswordChangeRequired: m.PasswordChangeRequired,
|
||||
IsEmailVerified: m.IsEmailVerified,
|
||||
OTPState: m.OTPState,
|
||||
MFAMaxSetUp: m.MFAMaxSetUp,
|
||||
MFAInitSkipped: m.MFAInitSkipped,
|
||||
PasswordlessInitRequired: m.PasswordlessInitRequired,
|
||||
PasswordlessTokens: m.PasswordlessTokens,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -486,7 +491,37 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"passwordless not verified, passwordless check step",
|
||||
"passwordless not initialised, passwordless prompt step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordlessInitRequired: true,
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
|
||||
MultiFactorCheckLifeTime: 10 * time.Hour,
|
||||
},
|
||||
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false},
|
||||
[]domain.NextStep{&domain.PasswordlessRegistrationPromptStep{}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"passwordless not verified, no password set, passwordless check step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordlessTokens: user_view_model.WebAuthNTokens{&user_view_model.WebAuthNView{ID: "id", State: int32(user_model.MFAStateReady)}},
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
|
||||
MultiFactorCheckLifeTime: 10 * time.Hour,
|
||||
},
|
||||
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false},
|
||||
[]domain.NextStep{&domain.PasswordlessStep{}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"passwordless not verified, passwordless check step, downgrade possible",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{},
|
||||
userViewProvider: &mockViewUser{
|
||||
@@ -498,7 +533,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
MultiFactorCheckLifeTime: 10 * time.Hour,
|
||||
},
|
||||
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{PasswordlessType: domain.PasswordlessTypeAllowed}}, false},
|
||||
[]domain.NextStep{&domain.PasswordlessStep{}},
|
||||
[]domain.NextStep{&domain.PasswordlessStep{PasswordSet: true}},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
@@ -533,9 +568,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
|
||||
"password not set, init password step",
|
||||
fields{
|
||||
userSessionViewProvider: &mockViewUserSession{},
|
||||
userViewProvider: &mockViewUser{},
|
||||
userEventProvider: &mockEventUser{},
|
||||
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
|
||||
userViewProvider: &mockViewUser{
|
||||
PasswordInitRequired: true,
|
||||
},
|
||||
userEventProvider: &mockEventUser{},
|
||||
orgViewProvider: &mockViewOrg{State: org_model.OrgStateActive},
|
||||
},
|
||||
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
|
||||
[]domain.NextStep{&domain.InitPasswordStep{}},
|
||||
@@ -1510,6 +1547,7 @@ func Test_userByID(t *testing.T) {
|
||||
"new user events, new view model state",
|
||||
args{
|
||||
viewProvider: &mockViewUser{
|
||||
PasswordSet: true,
|
||||
PasswordChangeRequired: true,
|
||||
},
|
||||
eventProvider: &mockEventUser{
|
||||
@@ -1518,7 +1556,7 @@ func Test_userByID(t *testing.T) {
|
||||
Type: user_es_model.UserPasswordChanged,
|
||||
CreationDate: time.Now().UTC().Round(1 * time.Second),
|
||||
Data: func() []byte {
|
||||
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false})
|
||||
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false, Secret: &crypto.CryptoValue{}})
|
||||
return data
|
||||
}(),
|
||||
},
|
||||
@@ -1529,6 +1567,7 @@ func Test_userByID(t *testing.T) {
|
||||
State: user_model.UserStateActive,
|
||||
UserName: "UserName",
|
||||
HumanView: &user_model.HumanView{
|
||||
PasswordSet: true,
|
||||
PasswordChangeRequired: false,
|
||||
PasswordChanged: time.Now().UTC().Round(1 * time.Second),
|
||||
FirstName: "FirstName",
|
||||
|
@@ -105,7 +105,7 @@ func (repo *OrgRepository) GetMyPasswordComplexityPolicy(ctx context.Context) (*
|
||||
return iam_view_model.PasswordComplexityViewToModel(policy), err
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) GetLabelPolicy(ctx context.Context, orgID string) (*iam_model.LabelPolicyView, error) {
|
||||
func (repo *OrgRepository) GetLabelPolicy(ctx context.Context, orgID string) (*domain.LabelPolicy, error) {
|
||||
orgPolicy, err := repo.View.LabelPolicyByAggregateIDAndState(orgID, int32(domain.LabelPolicyStateActive))
|
||||
if errors.IsNotFound(err) {
|
||||
orgPolicy, err = repo.View.LabelPolicyByAggregateIDAndState(repo.SystemDefaults.IamID, int32(domain.LabelPolicyStateActive))
|
||||
@@ -113,7 +113,19 @@ func (repo *OrgRepository) GetLabelPolicy(ctx context.Context, orgID string) (*i
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iam_view_model.LabelPolicyViewToModel(orgPolicy), nil
|
||||
return orgPolicy.ToDomain(), nil
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) GetLoginText(ctx context.Context, orgID string) ([]*domain.CustomText, error) {
|
||||
loginTexts, err := repo.View.CustomTextsByAggregateIDAndTemplate(domain.IAMID, domain.LoginCustomText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orgLoginTexts, err := repo.View.CustomTextsByAggregateIDAndTemplate(orgID, domain.LoginCustomText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(iam_view_model.CustomTextViewsToDomain(loginTexts), iam_view_model.CustomTextViewsToDomain(orgLoginTexts)...), nil
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error) {
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
|
||||
"github.com/caos/zitadel/internal/org/repository/view"
|
||||
user_repo "github.com/caos/zitadel/internal/repository/user"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
@@ -142,7 +143,9 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) {
|
||||
es_model.HumanPasswordlessTokenRemoved,
|
||||
es_model.HumanMFAInitSkipped,
|
||||
es_model.MachineChanged,
|
||||
es_model.HumanPasswordChanged:
|
||||
es_model.HumanPasswordChanged,
|
||||
es_models.EventType(user_repo.HumanPasswordlessInitCodeAddedType),
|
||||
es_models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType):
|
||||
user, err = u.view.UserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
iam_model "github.com/caos/zitadel/internal/iam/model"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
@@ -12,6 +13,7 @@ type OrgRepository interface {
|
||||
GetDefaultOrgIAMPolicy(ctx context.Context) (*iam_model.OrgIAMPolicyView, error)
|
||||
GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error)
|
||||
GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error)
|
||||
GetLabelPolicy(ctx context.Context, orgID string) (*iam_model.LabelPolicyView, error)
|
||||
GetLabelPolicy(ctx context.Context, orgID string) (*domain.LabelPolicy, error)
|
||||
GetLoginText(ctx context.Context, orgID string) ([]*domain.CustomText, error)
|
||||
GetDefaultPrivacyPolicy(ctx context.Context) (*iam_model.PrivacyPolicyView, error)
|
||||
}
|
||||
|
Reference in New Issue
Block a user