diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index c2e31dacd..56aa2df2b 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -247,7 +247,9 @@ DefaultInstance: AllowExternalIDP: true ForceMFA: false HidePasswordReset: false + IgnoreUnknownUsernames: false PasswordlessType: 1 #1: allowed 0: not allowed + DefaultRedirectURI: #empty because we use the Console UI PasswordCheckLifetime: 240h #10d ExternalLoginCheckLifetime: 240h #10d MfaInitSkipLifetime: 720h #30d diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.html b/console/src/app/modules/policies/login-policy/login-policy.component.html index b8459cdf5..923342d24 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.html +++ b/console/src/app/modules/policies/login-policy/login-policy.component.html @@ -129,6 +129,19 @@ --> + +
+ + {{ 'POLICY.DATA.IGNOREUNKNOWNUSERNAMES' | translate }} + +
+ +
+ + {{ 'POLICY.DATA.DEFAULTREDIRECTURI' | translate }} + + +
diff --git a/console/src/app/modules/policies/login-policy/login-policy.component.ts b/console/src/app/modules/policies/login-policy/login-policy.component.ts index ab61cdf82..c66922951 100644 --- a/console/src/app/modules/policies/login-policy/login-policy.component.ts +++ b/console/src/app/modules/policies/login-policy/login-policy.component.ts @@ -88,10 +88,12 @@ export class LoginPolicyComponent implements OnInit { mgmtreq.setForceMfa(this.loginData.forceMfa); mgmtreq.setPasswordlessType(this.loginData.passwordlessType); mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset); + mgmtreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames); + mgmtreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri); + // if(this.loginData.passwordCheckLifetime) { // mgmtreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime); // } - if ((this.loginData as LoginPolicy.AsObject).isDefault) { return (this.service as ManagementService).addCustomLoginPolicy(mgmtreq); } else { @@ -105,6 +107,8 @@ export class LoginPolicyComponent implements OnInit { adminreq.setForceMfa(this.loginData.forceMfa); adminreq.setPasswordlessType(this.loginData.passwordlessType); adminreq.setHidePasswordReset(this.loginData.hidePasswordReset); + adminreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames); + adminreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri); // adminreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime); return (this.service as AdminService).updateLoginPolicy(adminreq); diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 7051bcc34..d39209b4a 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1092,6 +1092,10 @@ "HIDEPASSWORDRESET": "Passwort vergessen ausblenden", "HIDEPASSWORDRESET_DESC": "Ist die Option gewählt, ist es nicht möglich im Login das Passwort zurück zusetzen via Passwort vergessen Link.", "HIDELOGINNAMESUFFIX": "Loginname Suffix ausblenden", + "IGNOREUNKNOWNUSERNAMES": "Unbekannte Usernamen ignorieren", + "IGNOREUNKNOWNUSERNAMES_DESC": "Ist die Option gewählt, wird der Passwort Schritt im Login auch angezeigt wenn der User nicht gefunden wurde. Dem Benutzer wird auf bei der Passwortprüfung nicht angezeigt ob der Username oder das Passwort falsch war.", + "DEFAULTREDIRECTURI": "Default Redirect URI", + "DEFAULTREDIRECTURI_DESC": "Definiert, wohin der Benutzer umgeleitet wird, wenn die Anmeldung ohne App-Kontext gestartet wurde (z. B. von Mail)", "ERRORMSGPOPUP": "Fehler als Dialog Fenster", "DISABLEWATERMARK": "Wasserzeichen ausblenden" }, diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index a2caa916e..bdb32920f 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1092,6 +1092,10 @@ "HIDEPASSWORDRESET": "Hide Password reset", "HIDEPASSWORDRESET_DESC": "If the option is selected, the user can't reset his password in the login process.", "HIDELOGINNAMESUFFIX": "Hide Loginname suffix", + "IGNOREUNKNOWNUSERNAMES": "Ignore unknown usernames", + "IGNOREUNKNOWNUSERNAMES_DESC": "If the option is selected, the password screen will be displayed in the login process even if the user was not found. The error on the password check will not disclose if the username or password was wrong.", + "DEFAULTREDIRECTURI": "Default Redirect URI", + "DEFAULTREDIRECTURI_DESC": "Defines where the user will be redirected to if the login has started without an app context (e.g. from mail)", "ERRORMSGPOPUP": "Show Error in Dialog", "DISABLEWATERMARK": "Hide Watermark" }, diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index e85bc5d1a..855cb34c6 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1092,6 +1092,10 @@ "HIDEPASSWORDRESET": "Nascondi ripristino della password", "HIDEPASSWORDRESET_DESC": "Se l'opzione \u00e8 selezionata, l'utente non pu\u00f2 resettare la sua password nel interfaccia login.", "HIDELOGINNAMESUFFIX": "Nascondi il suffisso del nome utente", + "IGNOREUNKNOWNUSERNAMES": "Ignora un nome utente sconosciuto", + "IGNOREUNKNOWNUSERNAMES_DESC": "Se l'opzione \u00e8 selezionata, l'inserimento della password viene mostrato anche se nessun utente è stato trovato. Nota che dopo il controllo della password, non viene mostrato se il nome utente o la password erano errati.", + "DEFAULTREDIRECTURI": "Default Redirect URI", + "DEFAULTREDIRECTURI_DESC": "Definisce dove verrà reindirizzato l'utente se l'accesso è stato avviato senza un contesto dell'app (ad es. dall' email)", "ERRORMSGPOPUP": "Mostra l'errore nella finestra di dialogo", "DISABLEWATERMARK": "Nascondi la filigrana" }, diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index b8910f9ca..6aac51ca7 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -3700,6 +3700,8 @@ This is an empty request | force_mfa | bool | - | | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true
| | hide_password_reset | bool | - | | +| ignore_unknown_usernames | bool | - | | +| default_redirect_uri | string | - | | | password_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | | diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index f9cd9b7b0..2492057ed 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -3039,6 +3039,8 @@ This is an empty request | force_mfa | bool | - | | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true
| | hide_password_reset | bool | - | | +| ignore_unknown_usernames | bool | - | | +| default_redirect_uri | string | - | | | password_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | | @@ -7760,6 +7762,8 @@ This is an empty request | force_mfa | bool | - | | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true
| | hide_password_reset | bool | - | | +| ignore_unknown_usernames | bool | - | | +| default_redirect_uri | string | - | | | password_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | | diff --git a/docs/docs/apis/proto/policy.md b/docs/docs/apis/proto/policy.md index 5fc1712a4..3f49b6f1c 100644 --- a/docs/docs/apis/proto/policy.md +++ b/docs/docs/apis/proto/policy.md @@ -77,6 +77,8 @@ title: zitadel/policy.proto | passwordless_type | PasswordlessType | - | | | is_default | bool | - | | | hide_password_reset | bool | - | | +| ignore_unknown_usernames | bool | - | | +| default_redirect_uri | string | - | | | password_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | | diff --git a/internal/api/grpc/admin/login_policy_converter.go b/internal/api/grpc/admin/login_policy_converter.go index 202e1743d..84e23d1fe 100644 --- a/internal/api/grpc/admin/login_policy_converter.go +++ b/internal/api/grpc/admin/login_policy_converter.go @@ -16,6 +16,8 @@ func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.Log ForceMFA: p.ForceMfa, PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), HidePasswordReset: p.HidePasswordReset, + IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, + DefaultRedirectURI: p.DefaultRedirectUri, PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(), diff --git a/internal/api/grpc/management/policy_login_converter.go b/internal/api/grpc/management/policy_login_converter.go index e6f63f5fb..f4f98d33f 100644 --- a/internal/api/grpc/management/policy_login_converter.go +++ b/internal/api/grpc/management/policy_login_converter.go @@ -16,6 +16,8 @@ func addLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.Logi ForceMFA: p.ForceMfa, PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), HidePasswordReset: p.HidePasswordReset, + IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, + DefaultRedirectURI: p.DefaultRedirectUri, PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(), @@ -32,6 +34,8 @@ func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domai ForceMFA: p.ForceMfa, PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), HidePasswordReset: p.HidePasswordReset, + IgnoreUnknownUsernames: p.IgnoreUnknownUsernames, + DefaultRedirectURI: p.DefaultRedirectUri, PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(), diff --git a/internal/api/grpc/policy/login_policy.go b/internal/api/grpc/policy/login_policy.go index e9eada18f..4b8d1c037 100644 --- a/internal/api/grpc/policy/login_policy.go +++ b/internal/api/grpc/policy/login_policy.go @@ -1,12 +1,13 @@ package policy import ( + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/object" policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy" - "google.golang.org/protobuf/types/known/durationpb" - timestamp_pb "google.golang.org/protobuf/types/known/timestamppb" ) func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy { @@ -18,6 +19,8 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy { ForceMfa: policy.ForceMFA, PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType), HidePasswordReset: policy.HidePasswordReset, + IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames, + DefaultRedirectUri: policy.DefaultRedirectURI, PasswordCheckLifetime: durationpb.New(policy.PasswordCheckLifetime), ExternalLoginCheckLifetime: durationpb.New(policy.ExternalLoginCheckLifetime), MfaInitSkipLifetime: durationpb.New(policy.MFAInitSkipLifetime), @@ -25,8 +28,8 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy { MultiFactorCheckLifetime: durationpb.New(policy.MultiFactorCheckLifetime), Details: &object.ObjectDetails{ Sequence: policy.Sequence, - CreationDate: timestamp_pb.New(policy.CreationDate), - ChangeDate: timestamp_pb.New(policy.ChangeDate), + CreationDate: timestamppb.New(policy.CreationDate), + ChangeDate: timestamppb.New(policy.ChangeDate), ResourceOwner: policy.OrgID, }, } diff --git a/internal/api/ui/login/external_login_handler.go b/internal/api/ui/login/external_login_handler.go index af4fc4226..6186ccf47 100644 --- a/internal/api/ui/login/external_login_handler.go +++ b/internal/api/ui/login/external_login_handler.go @@ -75,7 +75,7 @@ func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) { return } if authReq == nil { - http.Redirect(w, r, l.consolePath, http.StatusFound) + l.defaultRedirect(w, r) return } l.handleIDP(w, r, authReq, data.IDPConfigID) diff --git a/internal/api/ui/login/external_register_handler.go b/internal/api/ui/login/external_register_handler.go index ca7adebe1..762100400 100644 --- a/internal/api/ui/login/external_register_handler.go +++ b/internal/api/ui/login/external_register_handler.go @@ -58,7 +58,7 @@ func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) { return } if authReq == nil { - http.Redirect(w, r, l.consolePath, http.StatusFound) + l.defaultRedirect(w, r) return } idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID) diff --git a/internal/api/ui/login/init_password_handler.go b/internal/api/ui/login/init_password_handler.go index f8887f79c..b2fc4ee6c 100644 --- a/internal/api/ui/login/init_password_handler.go +++ b/internal/api/ui/login/init_password_handler.go @@ -39,8 +39,8 @@ type initPasswordData struct { HasSymbol string } -func InitPasswordLink(origin, userID, code string) string { - return fmt.Sprintf("%s%s?userID=%s&code=%s", externalLink(origin), EndpointInitPassword, userID, code) +func InitPasswordLink(origin, userID, code, orgID string) string { + return fmt.Sprintf("%s%s?userID=%s&code=%s&orgID=%s", externalLink(origin), EndpointInitPassword, userID, code, orgID) } func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/init_user_handler.go b/internal/api/ui/login/init_user_handler.go index eceb7e43e..b29ac612d 100644 --- a/internal/api/ui/login/init_user_handler.go +++ b/internal/api/ui/login/init_user_handler.go @@ -42,8 +42,8 @@ type initUserData struct { HasSymbol string } -func InitUserLink(origin, userID, code string, passwordSet bool) string { - return fmt.Sprintf("%s%s?userID=%s&code=%s&passwordset=%t", externalLink(origin), EndpointInitUser, userID, code, passwordSet) +func InitUserLink(origin, userID, code, orgID string, passwordSet bool) string { + return fmt.Sprintf("%s%s?userID=%s&code=%s&passwordset=%t&orgID=%s", externalLink(origin), EndpointInitUser, userID, code, passwordSet, orgID) } func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/jwt_handler.go b/internal/api/ui/login/jwt_handler.go index e343d62ea..cebebe5c3 100644 --- a/internal/api/ui/login/jwt_handler.go +++ b/internal/api/ui/login/jwt_handler.go @@ -120,7 +120,7 @@ func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request l.renderError(w, r, authReq, err) return } - resourceOwner := l.getOrgID(authReq) + resourceOwner := l.getOrgID(r, authReq) orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) if err != nil { l.renderError(w, r, authReq, err) diff --git a/internal/api/ui/login/login_handler.go b/internal/api/ui/login/login_handler.go index 495109566..26cfc77e4 100644 --- a/internal/api/ui/login/login_handler.go +++ b/internal/api/ui/login/login_handler.go @@ -3,13 +3,16 @@ package login import ( "net/http" + "github.com/zitadel/logging" + http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" ) const ( - tmplLogin = "login" + tmplLogin = "login" + queryOrgID = "orgID" ) type loginData struct { @@ -17,8 +20,8 @@ type loginData struct { Register bool `schema:"register"` } -func LoginLink(origin string) string { - return externalLink(origin) + EndpointLogin +func LoginLink(origin, orgID string) string { + return externalLink(origin) + EndpointLogin + "?orgID=" + orgID } func externalLink(origin string) string { @@ -32,12 +35,23 @@ func (l *Login) handleLogin(w http.ResponseWriter, r *http.Request) { return } if authReq == nil { - http.Redirect(w, r, l.consolePath, http.StatusFound) + l.defaultRedirect(w, r) return } l.renderNextStep(w, r, authReq) } +func (l *Login) defaultRedirect(w http.ResponseWriter, r *http.Request) { + orgID := r.FormValue(queryOrgID) + policy, err := l.getLoginPolicy(r, orgID) + logging.OnError(err).WithField("orgID", orgID).Error("error loading login policy") + redirect := l.consolePath + if policy != nil && policy.DefaultRedirectURI != "" { + redirect = policy.DefaultRedirectURI + } + http.Redirect(w, r, redirect, http.StatusFound) +} + func (l *Login) handleLoginName(w http.ResponseWriter, r *http.Request) { authReq, err := l.getAuthRequest(r) if err != nil { diff --git a/internal/api/ui/login/mail_verify_handler.go b/internal/api/ui/login/mail_verify_handler.go index ff694d8df..741d8ec28 100644 --- a/internal/api/ui/login/mail_verify_handler.go +++ b/internal/api/ui/login/mail_verify_handler.go @@ -27,8 +27,8 @@ type mailVerificationData struct { UserID string } -func MailVerificationLink(origin, userID, code string) string { - return fmt.Sprintf("%s%s?userID=%s&code=%s", externalLink(origin), EndpointMailVerification, userID, code) +func MailVerificationLink(origin, userID, code, orgID string) string { + return fmt.Sprintf("%s%s?userID=%s&code=%s&orgID=%s", externalLink(origin), EndpointMailVerification, userID, code, orgID) } func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/ui/login/password_handler.go b/internal/api/ui/login/password_handler.go index ccf5f80b2..b9b43d9ee 100644 --- a/internal/api/ui/login/password_handler.go +++ b/internal/api/ui/login/password_handler.go @@ -40,6 +40,10 @@ func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) { } err = l.authRepo.VerifyPassword(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Password, authReq.AgentID, domain.BrowserInfoFromRequest(r)) if err != nil { + if authReq.LoginPolicy.IgnoreUnknownUsernames { + l.renderLogin(w, r, authReq, err) + return + } l.renderPassword(w, r, authReq, err) return } diff --git a/internal/api/ui/login/password_reset_handler.go b/internal/api/ui/login/password_reset_handler.go index 2c0b4a674..d022db316 100644 --- a/internal/api/ui/login/password_reset_handler.go +++ b/internal/api/ui/login/password_reset_handler.go @@ -1,10 +1,11 @@ package login import ( - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/query" - "net/http" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query" ) const ( @@ -29,6 +30,9 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) { } passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg) if err != nil { + if authReq.LoginPolicy.IgnoreUnknownUsernames && errors.IsNotFound(err) { + err = nil + } l.renderPasswordResetDone(w, r, authReq, err) return } diff --git a/internal/api/ui/login/policy_handler.go b/internal/api/ui/login/policy_handler.go index 54b751d3a..dac3a694a 100644 --- a/internal/api/ui/login/policy_handler.go +++ b/internal/api/ui/login/policy_handler.go @@ -21,3 +21,10 @@ func (l *Login) getOrgDomainPolicy(r *http.Request, orgID string) (*query.Domain func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) { return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID) } + +func (l *Login) getLoginPolicy(r *http.Request, orgID string) (*query.LoginPolicy, error) { + if orgID == "" { + return l.query.DefaultLoginPolicy(r.Context()) + } + return l.query.LoginPolicyByID(r.Context(), orgID) +} diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go index c456f98c5..38cb8ba4b 100644 --- a/internal/api/ui/login/register_handler.go +++ b/internal/api/ui/login/register_handler.go @@ -90,7 +90,7 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) { return } if authRequest == nil { - http.Redirect(w, r, l.consolePath, http.StatusFound) + l.defaultRedirect(w, r) return } userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index 20625995d..e25b38ce0 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -73,7 +73,7 @@ func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) { return } if authRequest == nil { - http.Redirect(w, r, l.consolePath, http.StatusFound) + l.defaultRedirect(w, r) return } l.renderNextStep(w, r, authRequest) diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index 6f35cccd3..c9db22815 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -342,8 +342,8 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title Theme: l.getTheme(r), ThemeMode: l.getThemeMode(r), DarkMode: l.isDarkMode(r), - PrivateLabelingOrgID: l.getPrivateLabelingID(authz.GetInstance(r.Context()).InstanceID(), authReq), - OrgID: l.getOrgID(authReq), + PrivateLabelingOrgID: l.getPrivateLabelingID(r, authReq), + OrgID: l.getOrgID(r, authReq), OrgName: l.getOrgName(authReq), PrimaryDomain: l.getOrgPrimaryDomain(authReq), DisplayLoginNameSuffix: l.isDisplayLoginNameSuffix(authReq), @@ -361,6 +361,10 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title } privacyPolicy = authReq.PrivacyPolicy } else { + labelPolicy, _ := l.query.ActiveLabelPolicyByOrg(r.Context(), baseData.PrivateLabelingOrgID) + if labelPolicy != nil { + baseData.LabelPolicy = labelPolicy.ToDomain() + } policy, err := l.query.DefaultPrivacyPolicy(r.Context()) if err != nil { return baseData @@ -446,9 +450,9 @@ func (l *Login) isDarkMode(r *http.Request) bool { return strings.HasSuffix(cookie.Value, "dark") } -func (l *Login) getOrgID(authReq *domain.AuthRequest) string { +func (l *Login) getOrgID(r *http.Request, authReq *domain.AuthRequest) string { if authReq == nil { - return "" + return r.FormValue(queryOrgID) } if authReq.RequestedOrgID != "" { return authReq.RequestedOrgID @@ -456,9 +460,12 @@ func (l *Login) getOrgID(authReq *domain.AuthRequest) string { return authReq.UserOrgID } -func (l *Login) getPrivateLabelingID(instanceID string, authReq *domain.AuthRequest) string { - privateLabelingOrgID := instanceID +func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string { + privateLabelingOrgID := authz.GetInstance(r.Context()).InstanceID() if authReq == nil { + if id := r.FormValue(queryOrgID); id != "" { + return id + } return privateLabelingOrgID } if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified { diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index c4758fb66..3616d09a2 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -322,6 +322,8 @@ Errors: Empty: Passwort ist leer Invalid: Passwort ungültig InvalidAndLocked: Password ist undgültig und Benutzer wurde gesperrt, melden Sie sich bei ihrem Administrator. + UsernameOrPassword: + Invalid: Username oder Passwort ist ungültig PasswordComplexityPolicy: NotFound: Passwort Policy konnte nicht gefunden werden MinLength: Passwort ist zu kurz diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index 8fb2f6678..1a9e2f95e 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -323,6 +323,8 @@ Errors: Empty: Password is empty Invalid: Password is invalid InvalidAndLocked: Password is invalid and user is locked, contact your administrator. + UsernameOrPassword: + Invalid: Username or Password is invalid PasswordComplexityPolicy: NotFound: Password policy not found MinLength: Password is to short diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index ec6b7254c..d38a80414 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -323,6 +323,8 @@ Errors: Empty: La password è vuota Invalid: La password non è valida InvalidAndLocked: La password non è valida e l'utente è bloccato, contatta il tuo amministratore. + UsernameOrPassword: + Invalid: Il nome utente o la password non sono validi PasswordComplexityPolicy: NotFound: Impostazioni della password non trovate MinLength: La password è troppo corta diff --git a/internal/api/ui/login/static/templates/init_password.html b/internal/api/ui/login/static/templates/init_password.html index f214b4f48..613ce3d09 100644 --- a/internal/api/ui/login/static/templates/init_password.html +++ b/internal/api/ui/login/static/templates/init_password.html @@ -13,6 +13,7 @@ +
@@ -56,4 +57,4 @@ -{{template "main-bottom" .}} \ No newline at end of file +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/init_password_done.html b/internal/api/ui/login/static/templates/init_password_done.html index b6075a376..c46b67179 100644 --- a/internal/api/ui/login/static/templates/init_password_done.html +++ b/internal/api/ui/login/static/templates/init_password_done.html @@ -12,15 +12,13 @@ {{ .CSRF }} + -
- - {{t "InitPasswordDone.CancelButtonText"}} - +
-{{template "main-bottom" .}} \ No newline at end of file +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/init_user.html b/internal/api/ui/login/static/templates/init_user.html index 71d36aee9..075d143cb 100644 --- a/internal/api/ui/login/static/templates/init_user.html +++ b/internal/api/ui/login/static/templates/init_user.html @@ -15,6 +15,7 @@ +
@@ -63,4 +64,4 @@ {{ end }} -{{template "main-bottom" .}} \ No newline at end of file +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/init_user_done.html b/internal/api/ui/login/static/templates/init_user_done.html index 28c83c52b..bfcd59a5d 100644 --- a/internal/api/ui/login/static/templates/init_user_done.html +++ b/internal/api/ui/login/static/templates/init_user_done.html @@ -13,6 +13,7 @@ {{ .CSRF }} +
@@ -24,4 +25,4 @@ -{{template "main-bottom" .}} \ No newline at end of file +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/mail_verification.html b/internal/api/ui/login/static/templates/mail_verification.html index 69ef9d8bf..c79011060 100644 --- a/internal/api/ui/login/static/templates/mail_verification.html +++ b/internal/api/ui/login/static/templates/mail_verification.html @@ -13,6 +13,7 @@ +
@@ -41,4 +42,4 @@ -{{template "main-bottom" .}} \ No newline at end of file +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/mail_verified.html b/internal/api/ui/login/static/templates/mail_verified.html index 97546cbc6..878a3cfe2 100644 --- a/internal/api/ui/login/static/templates/mail_verified.html +++ b/internal/api/ui/login/static/templates/mail_verified.html @@ -12,6 +12,7 @@ {{ .CSRF }} +
@@ -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; }