@@ -29,4 +30,4 @@
-{{template "main-bottom" .}}
\ No newline at end of file
+{{template "main-bottom" .}}
diff --git a/internal/api/ui/login/static/templates/passwordless_registration_done.html b/internal/api/ui/login/static/templates/passwordless_registration_done.html
index 92e6b0cc5..46e1ee73a 100644
--- a/internal/api/ui/login/static/templates/passwordless_registration_done.html
+++ b/internal/api/ui/login/static/templates/passwordless_registration_done.html
@@ -16,6 +16,7 @@
{{ .CSRF }}
+
{{if not .HideNextButton }}
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
index 1488df474..03d73a2ae 100644
--- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go
+++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go
@@ -27,6 +27,8 @@ import (
user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
)
+const unknownUserID = "UNKNOWN"
+
type AuthRequestRepo struct {
Command *command.Commands
Query *query.Queries
@@ -284,7 +286,7 @@ func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID, userAge
if err != nil {
return err
}
- user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, userID)
+ user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, userID, false)
if err != nil {
return err
}
@@ -304,13 +306,28 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, authReqID, user
defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequestEnsureUser(ctx, authReqID, userAgentID, userID)
if err != nil {
+ if isIgnoreUserNotFoundError(err, request) {
+ return errors.ThrowInvalidArgument(nil, "EVENT-SDe2f", "Errors.User.UsernameOrPassword.Invalid")
+ }
return err
}
policy, err := repo.getLockoutPolicy(ctx, resourceOwner)
if err != nil {
return err
}
- return repo.Command.HumanCheckPassword(ctx, resourceOwner, userID, password, request.WithCurrentInfo(info), lockoutPolicyToDomain(policy))
+ err = repo.Command.HumanCheckPassword(ctx, resourceOwner, userID, password, request.WithCurrentInfo(info), lockoutPolicyToDomain(policy))
+ if isIgnoreUserInvalidPasswordError(err, request) {
+ return errors.ThrowInvalidArgument(nil, "EVENT-Jsf32", "Errors.User.UsernameOrPassword.Invalid")
+ }
+ return err
+}
+
+func isIgnoreUserNotFoundError(err error, request *domain.AuthRequest) bool {
+ return request != nil && request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames && errors.IsNotFound(err) && errors.Contains(err, "Errors.User.NotFound")
+}
+
+func isIgnoreUserInvalidPasswordError(err error, request *domain.AuthRequest) bool {
+ return request != nil && request.LoginPolicy != nil && request.LoginPolicy.IgnoreUnknownUsernames && errors.IsErrorInvalidArgument(err) && errors.Contains(err, "Errors.User.Password.Invalid")
}
func lockoutPolicyToDomain(policy *query.LockoutPolicy) *domain.LockoutPolicy {
@@ -499,9 +516,9 @@ func (repo *AuthRequestRepo) getAuthRequestEnsureUser(ctx context.Context, authR
if request.UserID != userID {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-GBH32", "Errors.User.NotMatchingUserID")
}
- _, err = activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID)
+ _, err = activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID, false)
if err != nil {
- return nil, err
+ return request, err
}
return request, nil
}
@@ -613,8 +630,8 @@ func (repo *AuthRequestRepo) tryUsingOnlyUserSession(request *domain.AuthRequest
func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain.AuthRequest, loginName string) (err error) {
user := new(user_view_model.UserView)
+ preferredLoginName := loginName
if request.RequestedOrgID != "" {
- preferredLoginName := loginName
if request.RequestedOrgID != "" {
preferredLoginName += "@" + request.RequestedPrimaryDomain
}
@@ -628,6 +645,15 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain
}
}
}
+ if request.LoginPolicy.IgnoreUnknownUsernames {
+ if errors.IsNotFound(err) || (user != nil && user.State == int32(domain.UserStateInactive)) {
+ if request.LabelPolicy.HideLoginNameSuffix {
+ preferredLoginName = loginName
+ }
+ request.SetUserInfo(unknownUserID, preferredLoginName, preferredLoginName, preferredLoginName, "", request.RequestedOrgID)
+ return nil
+ }
+ }
if err != nil {
return err
}
@@ -675,6 +701,7 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy {
MultiFactors: policy.MultiFactors,
PasswordlessType: policy.PasswordlessType,
HidePasswordReset: policy.HidePasswordReset,
+ IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,
PasswordCheckLifetime: policy.PasswordCheckLifetime,
ExternalLoginCheckLifetime: policy.ExternalLoginCheckLifetime,
MFAInitSkipLifetime: policy.MFAInitSkipLifetime,
@@ -703,7 +730,7 @@ func (repo *AuthRequestRepo) checkExternalUserLogin(ctx context.Context, request
if err != nil {
return err
}
- user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, externalIDP.UserID)
+ user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, externalIDP.UserID, false)
if err != nil {
return err
}
@@ -749,11 +776,13 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
}
return steps, nil
}
- user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID)
+ user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID, request.LoginPolicy.IgnoreUnknownUsernames)
if err != nil {
return nil, err
}
- request.LoginName = user.PreferredLoginName
+ if user.PreferredLoginName != "" {
+ request.LoginName = user.PreferredLoginName
+ }
userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user)
if err != nil {
return nil, err
@@ -1129,10 +1158,16 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve
return user_view_model.UserSessionToModel(&sessionCopy, provider.PrefixAvatarURL()), nil
}
-func activeUserByID(ctx context.Context, userViewProvider userViewProvider, userEventProvider userEventProvider, queries orgViewProvider, lockoutPolicyProvider lockoutPolicyViewProvider, userID string) (*user_model.UserView, error) {
+func activeUserByID(ctx context.Context, userViewProvider userViewProvider, userEventProvider userEventProvider, queries orgViewProvider, lockoutPolicyProvider lockoutPolicyViewProvider, userID string, ignoreUnknownUsernames bool) (user *user_model.UserView, err error) {
// PLANNED: Check LockoutPolicy
- user, err := userByID(ctx, userViewProvider, userEventProvider, userID)
+ user, err = userByID(ctx, userViewProvider, userEventProvider, userID)
if err != nil {
+ if ignoreUnknownUsernames && errors.IsNotFound(err) {
+ return &user_model.UserView{
+ ID: userID,
+ HumanView: &user_model.HumanView{},
+ }, nil
+ }
return nil, err
}
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
index 8df7c187a..b18a02847 100644
--- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
+++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go
@@ -421,7 +421,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewNoUser{},
userEventProvider: &mockEventUser{},
},
- args{&domain.AuthRequest{UserID: "UserID"}, false},
+ args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
nil,
errors.IsNotFound,
},
@@ -443,7 +443,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
},
},
- args{&domain.AuthRequest{UserID: "UserID"}, false},
+ args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
nil,
errors.IsPreconditionFailed,
},
@@ -464,7 +464,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
},
},
- args{&domain.AuthRequest{UserID: "UserID"}, false},
+ args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
nil,
errors.IsPreconditionFailed,
},
@@ -480,7 +480,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
},
},
- args{&domain.AuthRequest{UserID: "UserID"}, false},
+ args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
nil,
errors.IsInternal,
},
@@ -496,7 +496,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
},
},
- args{&domain.AuthRequest{UserID: "UserID"}, false},
+ args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
nil,
errors.IsPreconditionFailed,
},
@@ -532,7 +532,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
},
},
- args{&domain.AuthRequest{UserID: "UserID"}, false},
+ args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
nil,
errors.IsInternal,
},
@@ -552,7 +552,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
},
},
},
- args{&domain.AuthRequest{UserID: "UserID"}, false},
+ args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
[]domain.NextStep{&domain.InitUserStep{
PasswordSet: true,
}},
diff --git a/internal/command/instance.go b/internal/command/instance.go
index 4004aac31..7bcdc6d01 100644
--- a/internal/command/instance.go
+++ b/internal/command/instance.go
@@ -68,7 +68,9 @@ type InstanceSetup struct {
AllowExternalIDP bool
ForceMFA bool
HidePasswordReset bool
+ IgnoreUnknownUsername bool
PasswordlessType domain.PasswordlessType
+ DefaultRedirectURI string
PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration
MfaInitSkipLifetime time.Duration
@@ -205,7 +207,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
setup.LoginPolicy.AllowExternalIDP,
setup.LoginPolicy.ForceMFA,
setup.LoginPolicy.HidePasswordReset,
+ setup.LoginPolicy.IgnoreUnknownUsername,
setup.LoginPolicy.PasswordlessType,
+ setup.LoginPolicy.DefaultRedirectURI,
setup.LoginPolicy.PasswordCheckLifetime,
setup.LoginPolicy.ExternalLoginCheckLifetime,
setup.LoginPolicy.MfaInitSkipLifetime,
diff --git a/internal/command/instance_converter.go b/internal/command/instance_converter.go
index 75320bdcb..82701fca0 100644
--- a/internal/command/instance_converter.go
+++ b/internal/command/instance_converter.go
@@ -31,8 +31,10 @@ func writeModelToLoginPolicy(wm *LoginPolicyWriteModel) *domain.LoginPolicy {
AllowRegister: wm.AllowRegister,
AllowExternalIDP: wm.AllowExternalIDP,
HidePasswordReset: wm.HidePasswordReset,
+ IgnoreUnknownUsernames: wm.IgnoreUnknownUsernames,
ForceMFA: wm.ForceMFA,
PasswordlessType: wm.PasswordlessType,
+ DefaultRedirectURI: wm.DefaultRedirectURI,
PasswordCheckLifetime: wm.PasswordCheckLifetime,
ExternalLoginCheckLifetime: wm.ExternalLoginCheckLifetime,
MFAInitSkipLifetime: wm.MFAInitSkipLifetime,
diff --git a/internal/command/instance_login_policy.go b/internal/command/instance_login_policy.go
index aac47285a..f76571ea2 100644
--- a/internal/command/instance_login_policy.go
+++ b/internal/command/instance_login_policy.go
@@ -17,7 +17,9 @@ func AddDefaultLoginPolicy(
allowExternalIDP bool,
forceMFA bool,
hidePasswordReset bool,
+ ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ defaultRedirectURI string,
passwordCheckLifetime time.Duration,
externalLoginCheckLifetime time.Duration,
mfaInitSkipLifetime time.Duration,
@@ -34,7 +36,9 @@ func AddDefaultLoginPolicy(
allowExternalIDP,
forceMFA,
hidePasswordReset,
+ ignoreUnknownUsernames,
passwordlessType,
+ defaultRedirectURI,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
diff --git a/internal/command/instance_policy_login.go b/internal/command/instance_policy_login.go
index 991a349ac..ccca5caba 100644
--- a/internal/command/instance_policy_login.go
+++ b/internal/command/instance_policy_login.go
@@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/logging"
+
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -56,7 +57,9 @@ func (c *Commands) addDefaultLoginPolicy(ctx context.Context, instanceAgg *event
policy.AllowExternalIDP,
policy.ForceMFA,
policy.HidePasswordReset,
+ policy.IgnoreUnknownUsernames,
policy.PasswordlessType,
+ policy.DefaultRedirectURI,
policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime,
@@ -83,6 +86,9 @@ func (c *Commands) ChangeDefaultLoginPolicy(ctx context.Context, policy *domain.
}
func (c *Commands) changeDefaultLoginPolicy(ctx context.Context, instanceAgg *eventstore.Aggregate, existingPolicy *InstanceLoginPolicyWriteModel, policy *domain.LoginPolicy) (eventstore.Command, error) {
+ if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-SFdqd", "Errors.IAM.LoginPolicy.RedirectURIInvalid")
+ }
err := c.defaultLoginPolicyWriteModelByID(ctx, existingPolicy)
if err != nil {
return nil, err
@@ -97,12 +103,15 @@ func (c *Commands) changeDefaultLoginPolicy(ctx context.Context, instanceAgg *ev
policy.AllowExternalIDP,
policy.ForceMFA,
policy.HidePasswordReset,
+ policy.IgnoreUnknownUsernames,
policy.PasswordlessType,
+ policy.DefaultRedirectURI,
policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime,
policy.SecondFactorCheckLifetime,
- policy.MultiFactorCheckLifetime)
+ policy.MultiFactorCheckLifetime,
+ )
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-5M9vdd", "Errors.IAM.LoginPolicy.NotChanged")
}
diff --git a/internal/command/instance_policy_login_model.go b/internal/command/instance_policy_login_model.go
index b78103914..1e30528ed 100644
--- a/internal/command/instance_policy_login_model.go
+++ b/internal/command/instance_policy_login_model.go
@@ -65,8 +65,10 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
- hidePasswordReset bool,
+ hidePasswordReset,
+ ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ defaultRedirectURI string,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
@@ -93,6 +95,12 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
if wm.HidePasswordReset != hidePasswordReset {
changes = append(changes, policy.ChangeHidePasswordReset(hidePasswordReset))
}
+ if wm.IgnoreUnknownUsernames != ignoreUnknownUsernames {
+ changes = append(changes, policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames))
+ }
+ if wm.DefaultRedirectURI != defaultRedirectURI {
+ changes = append(changes, policy.ChangeDefaultRedirectURI(defaultRedirectURI))
+ }
if wm.PasswordCheckLifetime != passwordCheckLifetime {
changes = append(changes, policy.ChangePasswordCheckLifetime(passwordCheckLifetime))
}
diff --git a/internal/command/instance_policy_login_test.go b/internal/command/instance_policy_login_test.go
index 09a49bb2c..83b650745 100644
--- a/internal/command/instance_policy_login_test.go
+++ b/internal/command/instance_policy_login_test.go
@@ -50,7 +50,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*1,
time.Hour*1,
@@ -90,7 +92,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -110,7 +114,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: true,
ForceMFA: true,
HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3,
@@ -130,7 +136,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: true,
ForceMFA: true,
HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3,
@@ -210,7 +218,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -229,7 +239,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: true,
ForceMFA: true,
HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3,
@@ -256,7 +268,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -275,7 +289,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*10,
time.Hour*20,
time.Hour*30,
@@ -294,7 +310,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: false,
ForceMFA: false,
HidePasswordReset: false,
+ IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed,
+ DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30,
@@ -314,7 +332,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: false,
ForceMFA: false,
HidePasswordReset: false,
+ IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed,
+ DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30,
@@ -408,7 +428,9 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -444,7 +466,9 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -500,7 +524,9 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -638,7 +664,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -674,7 +702,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -723,7 +753,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -777,7 +809,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -839,7 +873,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1392,8 +1428,9 @@ func TestCommandSide_RemoveMultiFactorDefaultLoginPolicy(t *testing.T) {
}
}
-func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, hidePasswordReset bool,
+func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA, hidePasswordReset, ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ redirectURI string,
passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime time.Duration) *instance.LoginPolicyChangedEvent {
event, _ := instance.NewLoginPolicyChangedEvent(ctx,
&instance.NewAggregate("INSTANCE").Aggregate,
@@ -1403,7 +1440,9 @@ func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allow
policy.ChangeForceMFA(forceMFA),
policy.ChangeAllowUserNamePassword(allowUsernamePassword),
policy.ChangeHidePasswordReset(hidePasswordReset),
+ policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames),
policy.ChangePasswordlessType(passwordlessType),
+ policy.ChangeDefaultRedirectURI(redirectURI),
policy.ChangePasswordCheckLifetime(passwordLifetime),
policy.ChangeExternalLoginCheckLifetime(externalLoginLifetime),
policy.ChangeMFAInitSkipLifetime(mfaInitSkipLifetime),
diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go
index 11cc1ece9..ec8069608 100644
--- a/internal/command/org_policy_login.go
+++ b/internal/command/org_policy_login.go
@@ -16,6 +16,9 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Fn8ds", "Errors.ResourceOwnerMissing")
}
+ if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "Org-WSfdq", "Errors.Org.LoginPolicy.RedirectURIInvalid")
+ }
addedPolicy := NewOrgLoginPolicyWriteModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy)
if err != nil {
@@ -36,7 +39,9 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
policy.AllowExternalIDP,
policy.ForceMFA,
policy.HidePasswordReset,
+ policy.IgnoreUnknownUsernames,
policy.PasswordlessType,
+ policy.DefaultRedirectURI,
policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime,
@@ -76,6 +81,9 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing")
}
+ if ok := domain.ValidateDefaultRedirectURI(policy.DefaultRedirectURI); !ok {
+ return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Sfd21", "Errors.Org.LoginPolicy.RedirectURIInvalid")
+ }
existingPolicy := NewOrgLoginPolicyWriteModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy)
if err != nil {
@@ -94,7 +102,9 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
policy.AllowExternalIDP,
policy.ForceMFA,
policy.HidePasswordReset,
+ policy.IgnoreUnknownUsernames,
policy.PasswordlessType,
+ policy.DefaultRedirectURI,
policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime,
diff --git a/internal/command/org_policy_login_model.go b/internal/command/org_policy_login_model.go
index e9799a240..06236188e 100644
--- a/internal/command/org_policy_login_model.go
+++ b/internal/command/org_policy_login_model.go
@@ -67,8 +67,10 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
- hidePasswordReset bool,
+ hidePasswordReset,
+ ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ defaultRedirectURI string,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
@@ -92,6 +94,9 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent(
if wm.HidePasswordReset != hidePasswordReset {
changes = append(changes, policy.ChangeHidePasswordReset(hidePasswordReset))
}
+ if wm.IgnoreUnknownUsernames != ignoreUnknownUsernames {
+ changes = append(changes, policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames))
+ }
if wm.PasswordCheckLifetime != passwordCheckLifetime {
changes = append(changes, policy.ChangePasswordCheckLifetime(passwordCheckLifetime))
}
@@ -110,6 +115,9 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent(
if passwordlessType.Valid() && wm.PasswordlessType != passwordlessType {
changes = append(changes, policy.ChangePasswordlessType(passwordlessType))
}
+ if wm.DefaultRedirectURI != defaultRedirectURI {
+ changes = append(changes, policy.ChangeDefaultRedirectURI(defaultRedirectURI))
+ }
if len(changes) == 0 {
return nil, false
}
diff --git a/internal/command/org_policy_login_test.go b/internal/command/org_policy_login_test.go
index 89d61e8bf..6d5906153 100644
--- a/internal/command/org_policy_login_test.go
+++ b/internal/command/org_policy_login_test.go
@@ -77,7 +77,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -96,7 +98,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true,
AllowExternalIDP: true,
ForceMFA: true,
+ IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3,
@@ -124,7 +128,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -145,7 +151,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowExternalIDP: true,
ForceMFA: true,
HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3,
@@ -164,7 +172,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowExternalIDP: true,
ForceMFA: true,
HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3,
@@ -222,11 +232,13 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
args: args{
ctx: context.Background(),
policy: &domain.LoginPolicy{
- AllowRegister: true,
- AllowUsernamePassword: true,
- AllowExternalIDP: true,
- ForceMFA: true,
- PasswordlessType: domain.PasswordlessTypeAllowed,
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
},
},
res: res{
@@ -245,11 +257,13 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
ctx: context.Background(),
orgID: "org1",
policy: &domain.LoginPolicy{
- AllowRegister: true,
- AllowUsernamePassword: true,
- AllowExternalIDP: true,
- ForceMFA: true,
- PasswordlessType: domain.PasswordlessTypeAllowed,
+ AllowRegister: true,
+ AllowUsernamePassword: true,
+ AllowExternalIDP: true,
+ ForceMFA: true,
+ IgnoreUnknownUsernames: true,
+ PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
},
},
res: res{
@@ -270,7 +284,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -290,7 +306,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowExternalIDP: true,
ForceMFA: true,
HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3,
@@ -316,7 +334,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "https://example.com/redirect",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -335,7 +355,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
&duration10,
&duration20,
&duration30,
@@ -355,7 +377,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: false,
AllowExternalIDP: false,
ForceMFA: false,
+ IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed,
+ DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30,
@@ -374,7 +398,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowExternalIDP: false,
ForceMFA: false,
HidePasswordReset: false,
+ IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed,
+ DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30,
@@ -465,7 +491,9 @@ func TestCommandSide_RemoveLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -603,7 +631,9 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -642,7 +672,9 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -701,7 +733,9 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -863,7 +897,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -902,7 +938,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -953,7 +991,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1011,7 +1051,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1077,7 +1119,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true,
true,
true,
+ true,
domain.PasswordlessTypeAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1694,8 +1738,9 @@ func TestCommandSide_RemoveMultiFactorLoginPolicy(t *testing.T) {
}
}
-func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa, passwordReset bool,
+func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassword, register, externalIDP, mfa, passwordReset, ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ redirectURI string,
passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime *time.Duration) *org.LoginPolicyChangedEvent {
changes := []policy.LoginPolicyChanges{
policy.ChangeAllowUserNamePassword(usernamePassword),
@@ -1703,7 +1748,9 @@ func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassw
policy.ChangeAllowExternalIDP(externalIDP),
policy.ChangeForceMFA(mfa),
policy.ChangeHidePasswordReset(passwordReset),
+ policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames),
policy.ChangePasswordlessType(passwordlessType),
+ policy.ChangeDefaultRedirectURI(redirectURI),
}
if passwordLifetime != nil {
changes = append(changes, policy.ChangePasswordCheckLifetime(*passwordLifetime))
diff --git a/internal/command/policy_login_model.go b/internal/command/policy_login_model.go
index cfefc7781..6206f99a4 100644
--- a/internal/command/policy_login_model.go
+++ b/internal/command/policy_login_model.go
@@ -16,7 +16,9 @@ type LoginPolicyWriteModel struct {
AllowExternalIDP bool
ForceMFA bool
HidePasswordReset bool
+ IgnoreUnknownUsernames bool
PasswordlessType domain.PasswordlessType
+ DefaultRedirectURI string
PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration
MFAInitSkipLifetime time.Duration
@@ -35,6 +37,8 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
wm.ForceMFA = e.ForceMFA
wm.PasswordlessType = e.PasswordlessType
wm.HidePasswordReset = e.HidePasswordReset
+ wm.IgnoreUnknownUsernames = e.IgnoreUnknownUsernames
+ wm.DefaultRedirectURI = e.DefaultRedirectURI
wm.PasswordCheckLifetime = e.PasswordCheckLifetime
wm.ExternalLoginCheckLifetime = e.ExternalLoginCheckLifetime
wm.MFAInitSkipLifetime = e.MFAInitSkipLifetime
@@ -57,9 +61,15 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
if e.HidePasswordReset != nil {
wm.HidePasswordReset = *e.HidePasswordReset
}
+ if e.IgnoreUnknownUsernames != nil {
+ wm.IgnoreUnknownUsernames = *e.IgnoreUnknownUsernames
+ }
if e.PasswordlessType != nil {
wm.PasswordlessType = *e.PasswordlessType
}
+ if e.DefaultRedirectURI != nil {
+ wm.DefaultRedirectURI = *e.DefaultRedirectURI
+ }
if e.PasswordCheckLifetime != nil {
wm.PasswordCheckLifetime = *e.PasswordCheckLifetime
}
diff --git a/internal/command/user_human_password_test.go b/internal/command/user_human_password_test.go
index b8eaba434..b4c2ec0ca 100644
--- a/internal/command/user_human_password_test.go
+++ b/internal/command/user_human_password_test.go
@@ -1156,7 +1156,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1191,7 +1193,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1227,7 +1231,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1279,7 +1285,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1365,7 +1373,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1458,7 +1468,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go
index c0a7f4cf8..f3cd304a9 100644
--- a/internal/command/user_human_test.go
+++ b/internal/command/user_human_test.go
@@ -1687,7 +1687,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1750,7 +1752,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1813,7 +1817,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -1893,7 +1899,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -2031,7 +2039,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -2137,7 +2147,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -2237,7 +2249,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
@@ -2359,7 +2373,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false,
false,
false,
+ false,
domain.PasswordlessTypeNotAllowed,
+ "",
time.Hour*1,
time.Hour*2,
time.Hour*3,
diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go
index 06a26628a..24073e0f6 100644
--- a/internal/domain/policy_login.go
+++ b/internal/domain/policy_login.go
@@ -1,6 +1,7 @@
package domain
import (
+ "net/url"
"time"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -19,6 +20,8 @@ type LoginPolicy struct {
MultiFactors []MultiFactorType
PasswordlessType PasswordlessType
HidePasswordReset bool
+ IgnoreUnknownUsernames bool
+ DefaultRedirectURI string
PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration
MFAInitSkipLifetime time.Duration
@@ -26,6 +29,24 @@ type LoginPolicy struct {
MultiFactorCheckLifetime time.Duration
}
+func ValidateDefaultRedirectURI(rawURL string) bool {
+ if rawURL == "" {
+ return true
+ }
+ parsedURL, err := url.Parse(rawURL)
+ if err != nil {
+ return false
+ }
+ switch parsedURL.Scheme {
+ case "":
+ return false
+ case "http", "https":
+ return parsedURL.Host != ""
+ default:
+ return true
+ }
+}
+
type IDPProvider struct {
models.ObjectRoot
Type IdentityProviderType
diff --git a/internal/domain/policy_login_test.go b/internal/domain/policy_login_test.go
new file mode 100644
index 000000000..59e86786c
--- /dev/null
+++ b/internal/domain/policy_login_test.go
@@ -0,0 +1,73 @@
+package domain
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestValidateDefaultRedirectURI(t *testing.T) {
+ type args struct {
+ rawURL string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ }{
+ {
+ "invalid url, false",
+ args{
+ rawURL: string('\n'),
+ },
+ false,
+ },
+ {
+ "empty schema, false",
+ args{
+ rawURL: "url",
+ },
+ false,
+ },
+ {
+ "empty http host, false",
+ args{
+ rawURL: "http://",
+ },
+ false,
+ },
+ {
+ "empty https host, false",
+ args{
+ rawURL: "https://",
+ },
+ false,
+ },
+ {
+ "https, ok",
+ args{
+ rawURL: "https://test",
+ },
+ true,
+ },
+ {
+ "custom schema, ok",
+ args{
+ rawURL: "custom://",
+ },
+ true,
+ },
+ {
+ "empty url, ok",
+ args{
+ rawURL: "",
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equalf(t, tt.want, ValidateDefaultRedirectURI(tt.args.rawURL), "ValidateDefaultRedirectURI(%v)", tt.args.rawURL)
+ })
+ }
+}
diff --git a/internal/notification/types/domain_claimed.go b/internal/notification/types/domain_claimed.go
index dfafe6738..4ebad6708 100644
--- a/internal/notification/types/domain_claimed.go
+++ b/internal/notification/types/domain_claimed.go
@@ -21,7 +21,7 @@ type DomainClaimedData struct {
}
func SendDomainClaimed(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, username string, emailConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), colors *query.LabelPolicy, assetsPrefix string, origin string) error {
- url := login.LoginLink(origin)
+ url := login.LoginLink(origin, user.ResourceOwner)
var args = mapNotifyUserToArgs(user)
args["TempUsername"] = username
args["Domain"] = strings.Split(user.LastEmail, "@")[1]
diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go
index 58dce87e8..9170e41b7 100644
--- a/internal/notification/types/email_verification_code.go
+++ b/internal/notification/types/email_verification_code.go
@@ -26,7 +26,7 @@ func SendEmailVerificationCode(ctx context.Context, mailhtml string, translator
if err != nil {
return err
}
- url := login.MailVerificationLink(origin, user.ID, codeString)
+ url := login.MailVerificationLink(origin, user.ID, codeString, user.ResourceOwner)
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
diff --git a/internal/notification/types/init_code.go b/internal/notification/types/init_code.go
index a05a1c375..e4b6095e2 100644
--- a/internal/notification/types/init_code.go
+++ b/internal/notification/types/init_code.go
@@ -25,6 +25,7 @@ type UrlData struct {
UserID string
Code string
PasswordSet bool
+ OrgID string
}
func SendUserInitCode(ctx context.Context, mailhtml string, translator *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, smtpConfig func(ctx context.Context) (*smtp.EmailConfig, error), getFileSystemProvider func(ctx context.Context) (*fs.FSConfig, error), getLogProvider func(ctx context.Context) (*log.LogConfig, error), alg crypto.EncryptionAlgorithm, colors *query.LabelPolicy, assetsPrefix, origin string) error {
@@ -32,7 +33,7 @@ func SendUserInitCode(ctx context.Context, mailhtml string, translator *i18n.Tra
if err != nil {
return err
}
- url := login.InitUserLink(origin, user.ID, codeString, user.PasswordSet)
+ url := login.InitUserLink(origin, user.ID, codeString, user.ResourceOwner, user.PasswordSet)
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
diff --git a/internal/notification/types/password_code.go b/internal/notification/types/password_code.go
index da7699c35..9baa41123 100644
--- a/internal/notification/types/password_code.go
+++ b/internal/notification/types/password_code.go
@@ -29,7 +29,7 @@ func SendPasswordCode(ctx context.Context, mailhtml string, translator *i18n.Tra
if err != nil {
return err
}
- url := login.InitPasswordLink(origin, user.ID, codeString)
+ url := login.InitPasswordLink(origin, user.ID, codeString, user.ResourceOwner)
var args = mapNotifyUserToArgs(user)
args["Code"] = codeString
diff --git a/internal/query/label_policy.go b/internal/query/label_policy.go
index 4bfbc4055..2c5a02eaf 100644
--- a/internal/query/label_policy.go
+++ b/internal/query/label_policy.go
@@ -307,3 +307,25 @@ func prepareLabelPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LabelPolicy,
return policy, nil
}
}
+
+func (p *LabelPolicy) ToDomain() *domain.LabelPolicy {
+ return &domain.LabelPolicy{
+ Default: p.IsDefault,
+ PrimaryColor: p.Light.PrimaryColor,
+ BackgroundColor: p.Light.BackgroundColor,
+ WarnColor: p.Light.WarnColor,
+ FontColor: p.Light.FontColor,
+ LogoURL: p.Light.LogoURL,
+ IconURL: p.Light.IconURL,
+ PrimaryColorDark: p.Dark.PrimaryColor,
+ BackgroundColorDark: p.Dark.BackgroundColor,
+ WarnColorDark: p.Dark.WarnColor,
+ FontColorDark: p.Dark.FontColor,
+ LogoDarkURL: p.Dark.LogoURL,
+ IconDarkURL: p.Dark.IconURL,
+ Font: p.FontURL,
+ HideLoginNameSuffix: p.HideLoginNameSuffix,
+ ErrorMsgPopup: p.ShouldErrorPopup,
+ DisableWatermark: p.WatermarkDisabled,
+ }
+}
diff --git a/internal/query/login_policy.go b/internal/query/login_policy.go
index 9b072fc16..69befcff7 100644
--- a/internal/query/login_policy.go
+++ b/internal/query/login_policy.go
@@ -10,7 +10,6 @@ import (
"github.com/lib/pq"
"github.com/zitadel/zitadel/internal/api/authz"
-
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query/projection"
@@ -30,6 +29,8 @@ type LoginPolicy struct {
PasswordlessType domain.PasswordlessType
IsDefault bool
HidePasswordReset bool
+ IgnoreUnknownUsernames bool
+ DefaultRedirectURI string
PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration
MFAInitSkipLifetime time.Duration
@@ -107,6 +108,14 @@ var (
name: projection.LoginPolicyHidePWResetCol,
table: loginPolicyTable,
}
+ LoginPolicyColumnIgnoreUnknownUsernames = Column{
+ name: projection.IgnoreUnknownUsernames,
+ table: loginPolicyTable,
+ }
+ LoginPolicyColumnDefaultRedirectURI = Column{
+ name: projection.DefaultRedirectURI,
+ table: loginPolicyTable,
+ }
LoginPolicyColumnPasswordCheckLifetime = Column{
name: projection.PasswordCheckLifetimeCol,
table: loginPolicyTable,
@@ -284,6 +293,8 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
LoginPolicyColumnPasswordlessType.identifier(),
LoginPolicyColumnIsDefault.identifier(),
LoginPolicyColumnHidePasswordReset.identifier(),
+ LoginPolicyColumnIgnoreUnknownUsernames.identifier(),
+ LoginPolicyColumnDefaultRedirectURI.identifier(),
LoginPolicyColumnPasswordCheckLifetime.identifier(),
LoginPolicyColumnExternalLoginCheckLifetime.identifier(),
LoginPolicyColumnMFAInitSkipLifetime.identifier(),
@@ -294,6 +305,7 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
p := new(LoginPolicy)
secondFactors := pq.Int32Array{}
multiFactors := pq.Int32Array{}
+ defaultRedirectURI := sql.NullString{}
err := row.Scan(
&p.OrgID,
&p.CreationDate,
@@ -308,6 +320,8 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
&p.PasswordlessType,
&p.IsDefault,
&p.HidePasswordReset,
+ &p.IgnoreUnknownUsernames,
+ &defaultRedirectURI,
&p.PasswordCheckLifetime,
&p.ExternalLoginCheckLifetime,
&p.MFAInitSkipLifetime,
@@ -320,7 +334,7 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
}
return nil, errors.ThrowInternal(err, "QUERY-YcC53", "Errors.Internal")
}
-
+ p.DefaultRedirectURI = defaultRedirectURI.String
p.MultiFactors = make([]domain.MultiFactorType, len(multiFactors))
for i, mfa := range multiFactors {
p.MultiFactors[i] = domain.MultiFactorType(mfa)
diff --git a/internal/query/login_policy_test.go b/internal/query/login_policy_test.go
index 5546ed584..e586ecffb 100644
--- a/internal/query/login_policy_test.go
+++ b/internal/query/login_policy_test.go
@@ -44,6 +44,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
` projections.login_policies.passwordless_type,`+
` projections.login_policies.is_default,`+
` projections.login_policies.hide_password_reset,`+
+ ` projections.login_policies.ignore_unknown_usernames,`+
+ ` projections.login_policies.default_redirect_uri,`+
` projections.login_policies.password_check_lifetime,`+
` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+
@@ -80,6 +82,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
` projections.login_policies.passwordless_type,`+
` projections.login_policies.is_default,`+
` projections.login_policies.hide_password_reset,`+
+ ` projections.login_policies.ignore_unknown_usernames,`+
+ ` projections.login_policies.default_redirect_uri,`+
` projections.login_policies.password_check_lifetime,`+
` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+
@@ -100,6 +104,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
"passwordless_type",
"is_default",
"hide_password_reset",
+ "ignore_unknown_usernames",
+ "default_redirect_uri",
"password_check_lifetime",
"external_login_check_lifetime",
"mfa_init_skip_lifetime",
@@ -120,6 +126,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
domain.PasswordlessTypeAllowed,
true,
true,
+ true,
+ "https://example.com/redirect",
time.Hour * 2,
time.Hour * 2,
time.Hour * 2,
@@ -142,6 +150,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
PasswordlessType: domain.PasswordlessTypeAllowed,
IsDefault: true,
HidePasswordReset: true,
+ IgnoreUnknownUsernames: true,
+ DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 2,
ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 2,
@@ -167,6 +177,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
` projections.login_policies.passwordless_type,`+
` projections.login_policies.is_default,`+
` projections.login_policies.hide_password_reset,`+
+ ` projections.login_policies.ignore_unknown_usernames,`+
+ ` projections.login_policies.default_redirect_uri,`+
` projections.login_policies.password_check_lifetime,`+
` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+
diff --git a/internal/query/projection/login_policy.go b/internal/query/projection/login_policy.go
index 5ed074f47..c5c075fbc 100644
--- a/internal/query/projection/login_policy.go
+++ b/internal/query/projection/login_policy.go
@@ -29,6 +29,8 @@ const (
LoginPolicyMFAsCol = "multi_factors"
LoginPolicyPasswordlessTypeCol = "passwordless_type"
LoginPolicyHidePWResetCol = "hide_password_reset"
+ IgnoreUnknownUsernames = "ignore_unknown_usernames"
+ DefaultRedirectURI = "default_redirect_uri"
PasswordCheckLifetimeCol = "password_check_lifetime"
ExternalLoginCheckLifetimeCol = "external_login_check_lifetime"
MFAInitSkipLifetimeCol = "mfa_init_skip_lifetime"
@@ -60,6 +62,8 @@ func NewLoginPolicyProjection(ctx context.Context, config crdb.StatementHandlerC
crdb.NewColumn(LoginPolicyMFAsCol, crdb.ColumnTypeEnumArray, crdb.Nullable()),
crdb.NewColumn(LoginPolicyPasswordlessTypeCol, crdb.ColumnTypeEnum),
crdb.NewColumn(LoginPolicyHidePWResetCol, crdb.ColumnTypeBool),
+ crdb.NewColumn(IgnoreUnknownUsernames, crdb.ColumnTypeBool),
+ crdb.NewColumn(DefaultRedirectURI, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(PasswordCheckLifetimeCol, crdb.ColumnTypeInt64),
crdb.NewColumn(ExternalLoginCheckLifetimeCol, crdb.ColumnTypeInt64),
crdb.NewColumn(MFAInitSkipLifetimeCol, crdb.ColumnTypeInt64),
@@ -167,6 +171,8 @@ func (p *LoginPolicyProjection) reduceLoginPolicyAdded(event eventstore.Event) (
handler.NewCol(LoginPolicyPasswordlessTypeCol, policyEvent.PasswordlessType),
handler.NewCol(LoginPolicyIsDefaultCol, isDefault),
handler.NewCol(LoginPolicyHidePWResetCol, policyEvent.HidePasswordReset),
+ handler.NewCol(IgnoreUnknownUsernames, policyEvent.IgnoreUnknownUsernames),
+ handler.NewCol(DefaultRedirectURI, policyEvent.DefaultRedirectURI),
handler.NewCol(PasswordCheckLifetimeCol, policyEvent.PasswordCheckLifetime),
handler.NewCol(ExternalLoginCheckLifetimeCol, policyEvent.ExternalLoginCheckLifetime),
handler.NewCol(MFAInitSkipLifetimeCol, policyEvent.MFAInitSkipLifetime),
@@ -208,6 +214,12 @@ func (p *LoginPolicyProjection) reduceLoginPolicyChanged(event eventstore.Event)
if policyEvent.HidePasswordReset != nil {
cols = append(cols, handler.NewCol(LoginPolicyHidePWResetCol, *policyEvent.HidePasswordReset))
}
+ if policyEvent.IgnoreUnknownUsernames != nil {
+ cols = append(cols, handler.NewCol(IgnoreUnknownUsernames, *policyEvent.IgnoreUnknownUsernames))
+ }
+ if policyEvent.DefaultRedirectURI != nil {
+ cols = append(cols, handler.NewCol(DefaultRedirectURI, *policyEvent.DefaultRedirectURI))
+ }
if policyEvent.PasswordCheckLifetime != nil {
cols = append(cols, handler.NewCol(PasswordCheckLifetimeCol, *policyEvent.PasswordCheckLifetime))
}
diff --git a/internal/query/projection/login_policy_test.go b/internal/query/projection/login_policy_test.go
index 1dbd6d3c8..755346b14 100644
--- a/internal/query/projection/login_policy_test.go
+++ b/internal/query/projection/login_policy_test.go
@@ -35,7 +35,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": false,
"forceMFA": false,
"hidePasswordReset": true,
+ "ignoreUnknownUsernames": true,
"passwordlessType": 1,
+ "defaultRedirectURI": "https://example.com/redirect",
"passwordCheckLifetime": 10000000,
"externalLoginCheckLifetime": 10000000,
"mfaInitSkipLifetime": 10000000,
@@ -53,7 +55,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "INSERT INTO projections.login_policies (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)",
+ expectedStmt: "INSERT INTO projections.login_policies (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -67,6 +69,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
domain.PasswordlessTypeAllowed,
false,
true,
+ true,
+ "https://example.com/redirect",
time.Millisecond * 10,
time.Millisecond * 10,
time.Millisecond * 10,
@@ -91,7 +95,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": true,
"forceMFA": true,
"hidePasswordReset": true,
+ "ignoreUnknownUsernames": true,
"passwordlessType": 1,
+ "defaultRedirectURI": "https://example.com/redirect",
"passwordCheckLifetime": 10000000,
"externalLoginCheckLifetime": 10000000,
"mfaInitSkipLifetime": 10000000,
@@ -108,7 +114,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) WHERE (aggregate_id = $14)",
+ expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) WHERE (aggregate_id = $16)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -118,6 +124,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
domain.PasswordlessTypeAllowed,
true,
+ true,
+ "https://example.com/redirect",
time.Millisecond * 10,
time.Millisecond * 10,
time.Millisecond * 10,
@@ -298,7 +306,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": false,
"forceMFA": false,
"hidePasswordReset": true,
+ "ignoreUnknownUsernames": true,
"passwordlessType": 1,
+ "defaultRedirectURI": "https://example.com/redirect",
"passwordCheckLifetime": 10000000,
"externalLoginCheckLifetime": 10000000,
"mfaInitSkipLifetime": 10000000,
@@ -315,7 +325,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "INSERT INTO projections.login_policies (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)",
+ expectedStmt: "INSERT INTO projections.login_policies (aggregate_id, instance_id, creation_date, change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, is_default, hide_password_reset, ignore_unknown_usernames, default_redirect_uri, password_check_lifetime, external_login_check_lifetime, mfa_init_skip_lifetime, second_factor_check_lifetime, multi_factor_check_lifetime) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -329,6 +339,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
domain.PasswordlessTypeAllowed,
true,
true,
+ true,
+ "https://example.com/redirect",
time.Millisecond * 10,
time.Millisecond * 10,
time.Millisecond * 10,
@@ -353,7 +365,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": true,
"forceMFA": true,
"hidePasswordReset": true,
- "passwordlessType": 1
+ "ignoreUnknownUsernames": true,
+ "passwordlessType": 1,
+ "defaultRedirectURI": "https://example.com/redirect"
}`),
), instance.LoginPolicyChangedEventMapper),
},
@@ -365,7 +379,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
- expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset) = ($1, $2, $3, $4, $5, $6, $7, $8) WHERE (aggregate_id = $9)",
+ expectedStmt: "UPDATE projections.login_policies SET (change_date, sequence, allow_register, allow_username_password, allow_external_idps, force_mfa, passwordless_type, hide_password_reset, ignore_unknown_usernames, default_redirect_uri) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) WHERE (aggregate_id = $11)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
@@ -375,6 +389,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true,
domain.PasswordlessTypeAllowed,
true,
+ true,
+ "https://example.com/redirect",
"agg-id",
},
},
diff --git a/internal/repository/instance/policy_login.go b/internal/repository/instance/policy_login.go
index 3c2941fb7..83631e475 100644
--- a/internal/repository/instance/policy_login.go
+++ b/internal/repository/instance/policy_login.go
@@ -27,8 +27,10 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
- hidePasswordReset bool,
+ hidePasswordReset,
+ ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ defaultRedirectURI string,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
@@ -46,7 +48,9 @@ func NewLoginPolicyAddedEvent(
allowExternalIDP,
forceMFA,
hidePasswordReset,
+ ignoreUnknownUsernames,
passwordlessType,
+ defaultRedirectURI,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
diff --git a/internal/repository/org/policy_login.go b/internal/repository/org/policy_login.go
index 87ccb7bd4..d05d39086 100644
--- a/internal/repository/org/policy_login.go
+++ b/internal/repository/org/policy_login.go
@@ -28,8 +28,10 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
- hidePasswordReset bool,
+ hidePasswordReset,
+ ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ defaultRedirectURI string,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
@@ -47,7 +49,9 @@ func NewLoginPolicyAddedEvent(
allowExternalIDP,
forceMFA,
hidePasswordReset,
+ ignoreUnknownUsernames,
passwordlessType,
+ defaultRedirectURI,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
diff --git a/internal/repository/policy/login.go b/internal/repository/policy/login.go
index 12a082d28..c6165aad1 100644
--- a/internal/repository/policy/login.go
+++ b/internal/repository/policy/login.go
@@ -25,7 +25,9 @@ type LoginPolicyAddedEvent struct {
AllowExternalIDP bool `json:"allowExternalIdp,omitempty"`
ForceMFA bool `json:"forceMFA,omitempty"`
HidePasswordReset bool `json:"hidePasswordReset,omitempty"`
+ IgnoreUnknownUsernames bool `json:"ignoreUnknownUsernames,omitempty"`
PasswordlessType domain.PasswordlessType `json:"passwordlessType,omitempty"`
+ DefaultRedirectURI string `json:"defaultRedirectURI,omitempty"`
PasswordCheckLifetime time.Duration `json:"passwordCheckLifetime,omitempty"`
ExternalLoginCheckLifetime time.Duration `json:"externalLoginCheckLifetime,omitempty"`
MFAInitSkipLifetime time.Duration `json:"mfaInitSkipLifetime,omitempty"`
@@ -47,8 +49,10 @@ func NewLoginPolicyAddedEvent(
allowRegister,
allowExternalIDP,
forceMFA,
- hidePasswordReset bool,
+ hidePasswordReset,
+ ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType,
+ defaultRedirectURI string,
passwordCheckLifetime,
externalLoginCheckLifetime,
mfaInitSkipLifetime,
@@ -63,6 +67,8 @@ func NewLoginPolicyAddedEvent(
ForceMFA: forceMFA,
PasswordlessType: passwordlessType,
HidePasswordReset: hidePasswordReset,
+ IgnoreUnknownUsernames: ignoreUnknownUsernames,
+ DefaultRedirectURI: defaultRedirectURI,
PasswordCheckLifetime: passwordCheckLifetime,
ExternalLoginCheckLifetime: externalLoginCheckLifetime,
MFAInitSkipLifetime: mfaInitSkipLifetime,
@@ -92,7 +98,9 @@ type LoginPolicyChangedEvent struct {
AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"`
ForceMFA *bool `json:"forceMFA,omitempty"`
HidePasswordReset *bool `json:"hidePasswordReset,omitempty"`
+ IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"`
PasswordlessType *domain.PasswordlessType `json:"passwordlessType,omitempty"`
+ DefaultRedirectURI *string `json:"defaultRedirectURI,omitempty"`
PasswordCheckLifetime *time.Duration `json:"passwordCheckLifetime,omitempty"`
ExternalLoginCheckLifetime *time.Duration `json:"externalLoginCheckLifetime,omitempty"`
MFAInitSkipLifetime *time.Duration `json:"mfaInitSkipLifetime,omitempty"`
@@ -167,26 +175,43 @@ func ChangePasswordCheckLifetime(passwordCheckLifetime time.Duration) func(*Logi
e.PasswordCheckLifetime = &passwordCheckLifetime
}
}
+
func ChangeExternalLoginCheckLifetime(externalLoginCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) {
e.ExternalLoginCheckLifetime = &externalLoginCheckLifetime
}
}
+
func ChangeMFAInitSkipLifetime(mfaInitSkipLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) {
e.MFAInitSkipLifetime = &mfaInitSkipLifetime
}
}
+
func ChangeSecondFactorCheckLifetime(secondFactorCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) {
e.SecondFactorCheckLifetime = &secondFactorCheckLifetime
}
}
+
func ChangeMultiFactorCheckLifetime(multiFactorCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) {
e.MultiFactorCheckLifetime = &multiFactorCheckLifetime
}
}
+
+func ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames bool) func(*LoginPolicyChangedEvent) {
+ return func(e *LoginPolicyChangedEvent) {
+ e.IgnoreUnknownUsernames = &ignoreUnknownUsernames
+ }
+}
+
+func ChangeDefaultRedirectURI(defaultRedirectURI string) func(*LoginPolicyChangedEvent) {
+ return func(e *LoginPolicyChangedEvent) {
+ e.DefaultRedirectURI = &defaultRedirectURI
+ }
+}
+
func LoginPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &LoginPolicyChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml
index c739420a4..c4db70990 100644
--- a/internal/static/i18n/de.yaml
+++ b/internal/static/i18n/de.yaml
@@ -176,6 +176,7 @@ Errors:
LoginPolicy:
NotFound: Login Policy konnte nicht gefunden werden
Invalid: Login Policy ist ungültig
+ RedirectURIInvalid: Default Redirect URI ist ungültig
NotExisting: Login Policy existiert nicht auf dieser Organisation
AlreadyExists: Login Policy existiert bereits
IdpProviderAlreadyExisting: Idp Provider existiert bereits
@@ -285,6 +286,7 @@ Errors:
NotChanged: Default Login Policy wurde nicht verändert
NotExisting: Default Login Policy existiert nicht
AlreadyExists: Default Login Policy existiert bereits
+ RedirectURIInvalid: Default Redirect URI ist ungültig
MFA:
AlreadyExists: Multifaktor existiert bereits
NotExisting: Multifaktor existiert nicht
diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml
index df4b83772..e5033029b 100644
--- a/internal/static/i18n/en.yaml
+++ b/internal/static/i18n/en.yaml
@@ -176,7 +176,8 @@ Errors:
LoginPolicy:
NotFound: Login Policy not found
Invalid: Login Policy is invalid
- NotExisting: Login Policy not existig
+ RedirectURIInvalid: Default Redirect URI is invalid
+ NotExisting: Login Policy not existing
AlreadyExists: Login Policy already exists
IdpProviderAlreadyExisting: Idp Provider already existing
IdpProviderNotExisting: Idp Provider not existing
@@ -285,6 +286,7 @@ Errors:
NotChanged: Default Login Policy has not been changed
NotExisting: Default Login Policy not existig
AlreadyExists: Default Login Policy already exists
+ RedirectURIInvalid: Default Redirect URI is invalid
MFA:
AlreadyExists: Multifactor already exists
NotExisting: Multifactor not existing
diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml
index 56d5437d2..c55375a74 100644
--- a/internal/static/i18n/it.yaml
+++ b/internal/static/i18n/it.yaml
@@ -176,6 +176,7 @@ Errors:
LoginPolicy:
NotFound: Impostazioni di accesso non trovati
Invalid: Impostazioni di accesso non sono validi
+ RedirectURIInvalid: Default Redirect URI non valido
NotExisting: Impostazioni di accesso non esistenti
AlreadyExists: Impostazioni di accesso già esistenti
IdpProviderAlreadyExisting: IDP già esistente
@@ -283,6 +284,7 @@ Errors:
NotChanged: Le impostazioni di accesso predefinite non sono state cambiate
NotExisting: Impostazioni di accesso predefinite non esistenti
AlreadyExists: Impostazioni di accesso predefinite già esistenti
+ RedirectURIInvalid: Default Redirect URI non valido
MFA:
AlreadyExists: Multifattore già esistente
NotExisting: Multifattore non esistente
diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto
index 57165fa4a..d21084016 100644
--- a/proto/zitadel/admin.proto
+++ b/proto/zitadel/admin.proto
@@ -3478,7 +3478,7 @@ message AddCustomDomainPolicyRequest {
required: ["org_id"]
};
};
-
+
string org_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
@@ -3677,11 +3677,21 @@ message UpdateLoginPolicyRequest {
description: "defines if password reset link should be shown in the login screen"
}
];
- google.protobuf.Duration password_check_lifetime = 7;
- google.protobuf.Duration external_login_check_lifetime = 8;
- google.protobuf.Duration mfa_init_skip_lifetime = 9;
- google.protobuf.Duration second_factor_check_lifetime = 10;
- google.protobuf.Duration multi_factor_check_lifetime = 11;
+ bool ignore_unknown_usernames = 7 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines if unknown username on login screen directly return an error or always display the password screen"
+ }
+ ];
+ string default_redirect_uri = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines where the user will be redirected to if the login is started without app context (e.g. from mail)"
+ }
+ ];
+ google.protobuf.Duration password_check_lifetime = 9;
+ google.protobuf.Duration external_login_check_lifetime = 10;
+ google.protobuf.Duration mfa_init_skip_lifetime = 11;
+ google.protobuf.Duration second_factor_check_lifetime = 12;
+ google.protobuf.Duration multi_factor_check_lifetime = 13;
}
message UpdateLoginPolicyResponse {
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index 31c9ddead..ca34ca774 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -4344,11 +4344,21 @@ message AddCustomLoginPolicyRequest {
bool force_mfa = 4;
zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}];
bool hide_password_reset = 6;
- google.protobuf.Duration password_check_lifetime = 7;
- google.protobuf.Duration external_login_check_lifetime = 8;
- google.protobuf.Duration mfa_init_skip_lifetime = 9;
- google.protobuf.Duration second_factor_check_lifetime = 10;
- google.protobuf.Duration multi_factor_check_lifetime = 11;
+ bool ignore_unknown_usernames = 7 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines if unknown username on login screen directly return an error or always display the password screen"
+ }
+ ];
+ string default_redirect_uri = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines where the user will be redirected to if the login is started without app context (e.g. from mail)"
+ }
+ ];
+ google.protobuf.Duration password_check_lifetime = 9;
+ google.protobuf.Duration external_login_check_lifetime = 10;
+ google.protobuf.Duration mfa_init_skip_lifetime = 11;
+ google.protobuf.Duration second_factor_check_lifetime = 12;
+ google.protobuf.Duration multi_factor_check_lifetime = 13;
}
message AddCustomLoginPolicyResponse {
@@ -4362,11 +4372,21 @@ message UpdateCustomLoginPolicyRequest {
bool force_mfa = 4;
zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}];
bool hide_password_reset = 6;
- google.protobuf.Duration password_check_lifetime = 7;
- google.protobuf.Duration external_login_check_lifetime = 8;
- google.protobuf.Duration mfa_init_skip_lifetime = 9;
- google.protobuf.Duration second_factor_check_lifetime = 10;
- google.protobuf.Duration multi_factor_check_lifetime = 11;
+ bool ignore_unknown_usernames = 7 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines if unknown username on login screen directly return an error or always display the password screen"
+ }
+ ];
+ string default_redirect_uri = 8 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines where the user will be redirected to if the login is started without app context (e.g. from mail)"
+ }
+ ];
+ google.protobuf.Duration password_check_lifetime = 9;
+ google.protobuf.Duration external_login_check_lifetime = 10;
+ google.protobuf.Duration mfa_init_skip_lifetime = 11;
+ google.protobuf.Duration second_factor_check_lifetime = 12;
+ google.protobuf.Duration multi_factor_check_lifetime = 13;
}
message UpdateCustomLoginPolicyResponse {
diff --git a/proto/zitadel/policy.proto b/proto/zitadel/policy.proto
index 1d7598255..a8029a35c 100644
--- a/proto/zitadel/policy.proto
+++ b/proto/zitadel/policy.proto
@@ -149,11 +149,21 @@ message LoginPolicy {
description: "defines if password reset link should be shown in the login screen"
}
];
- google.protobuf.Duration password_check_lifetime = 9;
- google.protobuf.Duration external_login_check_lifetime = 10;
- google.protobuf.Duration mfa_init_skip_lifetime = 11;
- google.protobuf.Duration second_factor_check_lifetime = 12;
- google.protobuf.Duration multi_factor_check_lifetime = 13;
+ bool ignore_unknown_usernames = 9 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines if unknown username on login screen directly return an error or always display the password screen"
+ }
+ ];
+ string default_redirect_uri = 10 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ description: "defines where the user will be redirected to if the login is started without app context (e.g. from mail)"
+ }
+ ];
+ google.protobuf.Duration password_check_lifetime = 11;
+ google.protobuf.Duration external_login_check_lifetime = 12;
+ google.protobuf.Duration mfa_init_skip_lifetime = 13;
+ google.protobuf.Duration second_factor_check_lifetime = 14;
+ google.protobuf.Duration multi_factor_check_lifetime = 15;
}