feat: add default redirect uri and handling of unknown usernames (#3616)

* feat: add possibility to ignore username errors on first login screen

* console changes

* fix: handling of unknown usernames (#3445)

* fix: handling of unknown usernames

* fix: handle HideLoginNameSuffix on unknown users

* feat: add default redirect uri on login policy (#3607)

* feat: add default redirect uri on login policy

* fix tests

* feat: Console login policy default redirect (#3613)

* console default redirect

* placeholder

* validate default redirect uri

* allow empty default redirect uri

Co-authored-by: Max Peintner <max@caos.ch>

* remove wonrgly cherry picked migration

Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
Livio Amstutz 2022-05-16 15:39:09 +02:00 committed by GitHub
parent f1fa74a2c0
commit 411d7c6c5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 655 additions and 107 deletions

View File

@ -247,7 +247,9 @@ DefaultInstance:
AllowExternalIDP: true AllowExternalIDP: true
ForceMFA: false ForceMFA: false
HidePasswordReset: false HidePasswordReset: false
IgnoreUnknownUsernames: false
PasswordlessType: 1 #1: allowed 0: not allowed PasswordlessType: 1 #1: allowed 0: not allowed
DefaultRedirectURI: #empty because we use the Console UI
PasswordCheckLifetime: 240h #10d PasswordCheckLifetime: 240h #10d
ExternalLoginCheckLifetime: 240h #10d ExternalLoginCheckLifetime: 240h #10d
MfaInitSkipLifetime: 720h #30d MfaInitSkipLifetime: 720h #30d

View File

@ -129,6 +129,19 @@
</cnsl-info-section> </cnsl-info-section>
</ng-template> --> </ng-template> -->
</div> </div>
<div class="login-policy-row">
<mat-slide-toggle class="login-policy-toggle" color="primary" ngDefaultControl [(ngModel)]="loginData.ignoreUnknownUsernames">
{{ 'POLICY.DATA.IGNOREUNKNOWNUSERNAMES' | translate }}
</mat-slide-toggle>
</div>
<div class="login-policy-row">
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>{{ 'POLICY.DATA.DEFAULTREDIRECTURI' | translate }}</cnsl-label>
<input cnslInput placeholder="https://" [(ngModel)]="loginData.defaultRedirectUri" />
</cnsl-form-field>
</div>
</cnsl-card> </cnsl-card>
<div class="login-policy-btn-container"> <div class="login-policy-btn-container">

View File

@ -88,10 +88,12 @@ export class LoginPolicyComponent implements OnInit {
mgmtreq.setForceMfa(this.loginData.forceMfa); mgmtreq.setForceMfa(this.loginData.forceMfa);
mgmtreq.setPasswordlessType(this.loginData.passwordlessType); mgmtreq.setPasswordlessType(this.loginData.passwordlessType);
mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset); mgmtreq.setHidePasswordReset(this.loginData.hidePasswordReset);
mgmtreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
mgmtreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
// if(this.loginData.passwordCheckLifetime) { // if(this.loginData.passwordCheckLifetime) {
// mgmtreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime); // mgmtreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime);
// } // }
if ((this.loginData as LoginPolicy.AsObject).isDefault) { if ((this.loginData as LoginPolicy.AsObject).isDefault) {
return (this.service as ManagementService).addCustomLoginPolicy(mgmtreq); return (this.service as ManagementService).addCustomLoginPolicy(mgmtreq);
} else { } else {
@ -105,6 +107,8 @@ export class LoginPolicyComponent implements OnInit {
adminreq.setForceMfa(this.loginData.forceMfa); adminreq.setForceMfa(this.loginData.forceMfa);
adminreq.setPasswordlessType(this.loginData.passwordlessType); adminreq.setPasswordlessType(this.loginData.passwordlessType);
adminreq.setHidePasswordReset(this.loginData.hidePasswordReset); adminreq.setHidePasswordReset(this.loginData.hidePasswordReset);
adminreq.setIgnoreUnknownUsernames(this.loginData.ignoreUnknownUsernames);
adminreq.setDefaultRedirectUri(this.loginData.defaultRedirectUri);
// adminreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime); // adminreq.setPasswordCheckLifetime(this.loginData.passwordCheckLifetime);
return (this.service as AdminService).updateLoginPolicy(adminreq); return (this.service as AdminService).updateLoginPolicy(adminreq);

View File

@ -1092,6 +1092,10 @@
"HIDEPASSWORDRESET": "Passwort vergessen ausblenden", "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.", "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", "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", "ERRORMSGPOPUP": "Fehler als Dialog Fenster",
"DISABLEWATERMARK": "Wasserzeichen ausblenden" "DISABLEWATERMARK": "Wasserzeichen ausblenden"
}, },

View File

@ -1092,6 +1092,10 @@
"HIDEPASSWORDRESET": "Hide Password reset", "HIDEPASSWORDRESET": "Hide Password reset",
"HIDEPASSWORDRESET_DESC": "If the option is selected, the user can't reset his password in the login process.", "HIDEPASSWORDRESET_DESC": "If the option is selected, the user can't reset his password in the login process.",
"HIDELOGINNAMESUFFIX": "Hide Loginname suffix", "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", "ERRORMSGPOPUP": "Show Error in Dialog",
"DISABLEWATERMARK": "Hide Watermark" "DISABLEWATERMARK": "Hide Watermark"
}, },

View File

@ -1092,6 +1092,10 @@
"HIDEPASSWORDRESET": "Nascondi ripristino della password", "HIDEPASSWORDRESET": "Nascondi ripristino della password",
"HIDEPASSWORDRESET_DESC": "Se l'opzione \u00e8 selezionata, l'utente non pu\u00f2 resettare la sua password nel interfaccia login.", "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", "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", "ERRORMSGPOPUP": "Mostra l'errore nella finestra di dialogo",
"DISABLEWATERMARK": "Nascondi la filigrana" "DISABLEWATERMARK": "Nascondi la filigrana"
}, },

View File

@ -3700,6 +3700,8 @@ This is an empty request
| force_mfa | bool | - | | | force_mfa | bool | - | |
| passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> |
| hide_password_reset | bool | - | | | hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |
| default_redirect_uri | string | - | |
| password_check_lifetime | google.protobuf.Duration | - | | | password_check_lifetime | google.protobuf.Duration | - | |
| external_login_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | |
| mfa_init_skip_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | |

View File

@ -3039,6 +3039,8 @@ This is an empty request
| force_mfa | bool | - | | | force_mfa | bool | - | |
| passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> |
| hide_password_reset | bool | - | | | hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |
| default_redirect_uri | string | - | |
| password_check_lifetime | google.protobuf.Duration | - | | | password_check_lifetime | google.protobuf.Duration | - | |
| external_login_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | |
| mfa_init_skip_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | |
@ -7760,6 +7762,8 @@ This is an empty request
| force_mfa | bool | - | | | force_mfa | bool | - | |
| passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> | | passwordless_type | zitadel.policy.v1.PasswordlessType | - | enum.defined_only: true<br /> |
| hide_password_reset | bool | - | | | hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |
| default_redirect_uri | string | - | |
| password_check_lifetime | google.protobuf.Duration | - | | | password_check_lifetime | google.protobuf.Duration | - | |
| external_login_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | |
| mfa_init_skip_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | |

View File

@ -77,6 +77,8 @@ title: zitadel/policy.proto
| passwordless_type | PasswordlessType | - | | | passwordless_type | PasswordlessType | - | |
| is_default | bool | - | | | is_default | bool | - | |
| hide_password_reset | bool | - | | | hide_password_reset | bool | - | |
| ignore_unknown_usernames | bool | - | |
| default_redirect_uri | string | - | |
| password_check_lifetime | google.protobuf.Duration | - | | | password_check_lifetime | google.protobuf.Duration | - | |
| external_login_check_lifetime | google.protobuf.Duration | - | | | external_login_check_lifetime | google.protobuf.Duration | - | |
| mfa_init_skip_lifetime | google.protobuf.Duration | - | | | mfa_init_skip_lifetime | google.protobuf.Duration | - | |

View File

@ -16,6 +16,8 @@ func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.Log
ForceMFA: p.ForceMfa, ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset, HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
DefaultRedirectURI: p.DefaultRedirectUri,
PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(),
ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(),
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(), MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),

View File

@ -16,6 +16,8 @@ func addLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.Logi
ForceMFA: p.ForceMfa, ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset, HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
DefaultRedirectURI: p.DefaultRedirectUri,
PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(),
ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(),
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(), MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),
@ -32,6 +34,8 @@ func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domai
ForceMFA: p.ForceMfa, ForceMFA: p.ForceMfa,
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType), PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
HidePasswordReset: p.HidePasswordReset, HidePasswordReset: p.HidePasswordReset,
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
DefaultRedirectURI: p.DefaultRedirectUri,
PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(), PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(),
ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(), ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(),
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(), MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),

View File

@ -1,12 +1,13 @@
package policy package policy
import ( 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/domain"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/object" "github.com/zitadel/zitadel/pkg/grpc/object"
policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy" 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 { func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
@ -18,6 +19,8 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
ForceMfa: policy.ForceMFA, ForceMfa: policy.ForceMFA,
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType), PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
HidePasswordReset: policy.HidePasswordReset, HidePasswordReset: policy.HidePasswordReset,
IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,
DefaultRedirectUri: policy.DefaultRedirectURI,
PasswordCheckLifetime: durationpb.New(policy.PasswordCheckLifetime), PasswordCheckLifetime: durationpb.New(policy.PasswordCheckLifetime),
ExternalLoginCheckLifetime: durationpb.New(policy.ExternalLoginCheckLifetime), ExternalLoginCheckLifetime: durationpb.New(policy.ExternalLoginCheckLifetime),
MfaInitSkipLifetime: durationpb.New(policy.MFAInitSkipLifetime), MfaInitSkipLifetime: durationpb.New(policy.MFAInitSkipLifetime),
@ -25,8 +28,8 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
MultiFactorCheckLifetime: durationpb.New(policy.MultiFactorCheckLifetime), MultiFactorCheckLifetime: durationpb.New(policy.MultiFactorCheckLifetime),
Details: &object.ObjectDetails{ Details: &object.ObjectDetails{
Sequence: policy.Sequence, Sequence: policy.Sequence,
CreationDate: timestamp_pb.New(policy.CreationDate), CreationDate: timestamppb.New(policy.CreationDate),
ChangeDate: timestamp_pb.New(policy.ChangeDate), ChangeDate: timestamppb.New(policy.ChangeDate),
ResourceOwner: policy.OrgID, ResourceOwner: policy.OrgID,
}, },
} }

View File

@ -75,7 +75,7 @@ func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
if authReq == nil { if authReq == nil {
http.Redirect(w, r, l.consolePath, http.StatusFound) l.defaultRedirect(w, r)
return return
} }
l.handleIDP(w, r, authReq, data.IDPConfigID) l.handleIDP(w, r, authReq, data.IDPConfigID)

View File

@ -58,7 +58,7 @@ func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
return return
} }
if authReq == nil { if authReq == nil {
http.Redirect(w, r, l.consolePath, http.StatusFound) l.defaultRedirect(w, r)
return return
} }
idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID) idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID)

View File

@ -39,8 +39,8 @@ type initPasswordData struct {
HasSymbol string HasSymbol string
} }
func InitPasswordLink(origin, userID, code string) string { func InitPasswordLink(origin, userID, code, orgID string) string {
return fmt.Sprintf("%s%s?userID=%s&code=%s", externalLink(origin), EndpointInitPassword, userID, code) 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) { func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) {

View File

@ -42,8 +42,8 @@ type initUserData struct {
HasSymbol string HasSymbol string
} }
func InitUserLink(origin, userID, code string, passwordSet bool) string { func InitUserLink(origin, userID, code, orgID string, passwordSet bool) string {
return fmt.Sprintf("%s%s?userID=%s&code=%s&passwordset=%t", externalLink(origin), EndpointInitUser, userID, code, passwordSet) 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) { func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) {

View File

@ -120,7 +120,7 @@ func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
} }
resourceOwner := l.getOrgID(authReq) resourceOwner := l.getOrgID(r, authReq)
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
if err != nil { if err != nil {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)

View File

@ -3,6 +3,8 @@ package login
import ( import (
"net/http" "net/http"
"github.com/zitadel/logging"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
@ -10,6 +12,7 @@ import (
const ( const (
tmplLogin = "login" tmplLogin = "login"
queryOrgID = "orgID"
) )
type loginData struct { type loginData struct {
@ -17,8 +20,8 @@ type loginData struct {
Register bool `schema:"register"` Register bool `schema:"register"`
} }
func LoginLink(origin string) string { func LoginLink(origin, orgID string) string {
return externalLink(origin) + EndpointLogin return externalLink(origin) + EndpointLogin + "?orgID=" + orgID
} }
func externalLink(origin string) string { func externalLink(origin string) string {
@ -32,12 +35,23 @@ func (l *Login) handleLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
if authReq == nil { if authReq == nil {
http.Redirect(w, r, l.consolePath, http.StatusFound) l.defaultRedirect(w, r)
return return
} }
l.renderNextStep(w, r, authReq) 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) { func (l *Login) handleLoginName(w http.ResponseWriter, r *http.Request) {
authReq, err := l.getAuthRequest(r) authReq, err := l.getAuthRequest(r)
if err != nil { if err != nil {

View File

@ -27,8 +27,8 @@ type mailVerificationData struct {
UserID string UserID string
} }
func MailVerificationLink(origin, userID, code string) string { func MailVerificationLink(origin, userID, code, orgID string) string {
return fmt.Sprintf("%s%s?userID=%s&code=%s", externalLink(origin), EndpointMailVerification, userID, code) 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) { func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) {

View File

@ -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)) 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 err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames {
l.renderLogin(w, r, authReq, err)
return
}
l.renderPassword(w, r, authReq, err) l.renderPassword(w, r, authReq, err)
return return
} }

View File

@ -1,10 +1,11 @@
package login package login
import ( import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"net/http" "net/http"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
) )
const ( 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) passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg)
if err != nil { if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames && errors.IsNotFound(err) {
err = nil
}
l.renderPasswordResetDone(w, r, authReq, err) l.renderPasswordResetDone(w, r, authReq, err)
return return
} }

View File

@ -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) { func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) {
return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID) 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)
}

View File

@ -90,7 +90,7 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
return return
} }
if authRequest == nil { if authRequest == nil {
http.Redirect(w, r, l.consolePath, http.StatusFound) l.defaultRedirect(w, r)
return return
} }
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())

View File

@ -73,7 +73,7 @@ func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
return return
} }
if authRequest == nil { if authRequest == nil {
http.Redirect(w, r, l.consolePath, http.StatusFound) l.defaultRedirect(w, r)
return return
} }
l.renderNextStep(w, r, authRequest) l.renderNextStep(w, r, authRequest)

View File

@ -342,8 +342,8 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title
Theme: l.getTheme(r), Theme: l.getTheme(r),
ThemeMode: l.getThemeMode(r), ThemeMode: l.getThemeMode(r),
DarkMode: l.isDarkMode(r), DarkMode: l.isDarkMode(r),
PrivateLabelingOrgID: l.getPrivateLabelingID(authz.GetInstance(r.Context()).InstanceID(), authReq), PrivateLabelingOrgID: l.getPrivateLabelingID(r, authReq),
OrgID: l.getOrgID(authReq), OrgID: l.getOrgID(r, authReq),
OrgName: l.getOrgName(authReq), OrgName: l.getOrgName(authReq),
PrimaryDomain: l.getOrgPrimaryDomain(authReq), PrimaryDomain: l.getOrgPrimaryDomain(authReq),
DisplayLoginNameSuffix: l.isDisplayLoginNameSuffix(authReq), DisplayLoginNameSuffix: l.isDisplayLoginNameSuffix(authReq),
@ -361,6 +361,10 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title
} }
privacyPolicy = authReq.PrivacyPolicy privacyPolicy = authReq.PrivacyPolicy
} else { } else {
labelPolicy, _ := l.query.ActiveLabelPolicyByOrg(r.Context(), baseData.PrivateLabelingOrgID)
if labelPolicy != nil {
baseData.LabelPolicy = labelPolicy.ToDomain()
}
policy, err := l.query.DefaultPrivacyPolicy(r.Context()) policy, err := l.query.DefaultPrivacyPolicy(r.Context())
if err != nil { if err != nil {
return baseData return baseData
@ -446,9 +450,9 @@ func (l *Login) isDarkMode(r *http.Request) bool {
return strings.HasSuffix(cookie.Value, "dark") 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 { if authReq == nil {
return "" return r.FormValue(queryOrgID)
} }
if authReq.RequestedOrgID != "" { if authReq.RequestedOrgID != "" {
return authReq.RequestedOrgID return authReq.RequestedOrgID
@ -456,9 +460,12 @@ func (l *Login) getOrgID(authReq *domain.AuthRequest) string {
return authReq.UserOrgID return authReq.UserOrgID
} }
func (l *Login) getPrivateLabelingID(instanceID string, authReq *domain.AuthRequest) string { func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string {
privateLabelingOrgID := instanceID privateLabelingOrgID := authz.GetInstance(r.Context()).InstanceID()
if authReq == nil { if authReq == nil {
if id := r.FormValue(queryOrgID); id != "" {
return id
}
return privateLabelingOrgID return privateLabelingOrgID
} }
if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified { if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified {

View File

@ -322,6 +322,8 @@ Errors:
Empty: Passwort ist leer Empty: Passwort ist leer
Invalid: Passwort ungültig Invalid: Passwort ungültig
InvalidAndLocked: Password ist undgültig und Benutzer wurde gesperrt, melden Sie sich bei ihrem Administrator. InvalidAndLocked: Password ist undgültig und Benutzer wurde gesperrt, melden Sie sich bei ihrem Administrator.
UsernameOrPassword:
Invalid: Username oder Passwort ist ungültig
PasswordComplexityPolicy: PasswordComplexityPolicy:
NotFound: Passwort Policy konnte nicht gefunden werden NotFound: Passwort Policy konnte nicht gefunden werden
MinLength: Passwort ist zu kurz MinLength: Passwort ist zu kurz

View File

@ -323,6 +323,8 @@ Errors:
Empty: Password is empty Empty: Password is empty
Invalid: Password is invalid Invalid: Password is invalid
InvalidAndLocked: Password is invalid and user is locked, contact your administrator. InvalidAndLocked: Password is invalid and user is locked, contact your administrator.
UsernameOrPassword:
Invalid: Username or Password is invalid
PasswordComplexityPolicy: PasswordComplexityPolicy:
NotFound: Password policy not found NotFound: Password policy not found
MinLength: Password is to short MinLength: Password is to short

View File

@ -323,6 +323,8 @@ Errors:
Empty: La password è vuota Empty: La password è vuota
Invalid: La password non è valida Invalid: La password non è valida
InvalidAndLocked: La password non è valida e l'utente è bloccato, contatta il tuo amministratore. 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: PasswordComplexityPolicy:
NotFound: Impostazioni della password non trovate NotFound: Impostazioni della password non trovate
MinLength: La password è troppo corta MinLength: La password è troppo corta

View File

@ -13,6 +13,7 @@
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" /> <input type="hidden" name="userID" value="{{ .UserID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">

View File

@ -12,11 +12,9 @@
{{ .CSRF }} {{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<div class="lgnactions"> <div class="lgn-actions">
<a class="lgn-stroked-button lgn-primary" href="{{ loginUrl }}">
{{t "InitPasswordDone.CancelButtonText"}}
</a>
<span class="fill-space"></span> <span class="fill-space"></span>
<button class="lgn-raised-button lgn-primary" type="submit">{{t "InitPasswordDone.NextButtonText"}}</button> <button class="lgn-raised-button lgn-primary" type="submit">{{t "InitPasswordDone.NextButtonText"}}</button>
</div> </div>

View File

@ -15,6 +15,7 @@
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" /> <input type="hidden" name="userID" value="{{ .UserID }}" />
<input type="hidden" name="passwordSet" value="{{ .PasswordSet }}" /> <input type="hidden" name="passwordSet" value="{{ .PasswordSet }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">

View File

@ -13,6 +13,7 @@
{{ .CSRF }} {{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<div class="lgn-actions lgn-reverse-order"> <div class="lgn-actions lgn-reverse-order">
<button class="lgn-raised-button lgn-primary" type="submit">{{t "InitUserDone.NextButtonText"}}</button> <button class="lgn-raised-button lgn-primary" type="submit">{{t "InitUserDone.NextButtonText"}}</button>

View File

@ -13,6 +13,7 @@
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" /> <input type="hidden" name="userID" value="{{ .UserID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<div class="fields"> <div class="fields">
<label class="lgn-label" for="code">{{t "EmailVerification.CodeLabel"}}</label> <label class="lgn-label" for="code">{{t "EmailVerification.CodeLabel"}}</label>

View File

@ -12,6 +12,7 @@
{{ .CSRF }} {{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<div class="lgn-actions"> <div class="lgn-actions">
<a class="lgn-stroked-button lgn-primary" href="{{ loginUrl }}"> <a class="lgn-stroked-button lgn-primary" href="{{ loginUrl }}">

View File

@ -16,6 +16,7 @@
{{ .CSRF }} {{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" /> <input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<div class="lgn-actions"> <div class="lgn-actions">
{{if not .HideNextButton }} {{if not .HideNextButton }}

View File

@ -27,6 +27,8 @@ import (
user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
) )
const unknownUserID = "UNKNOWN"
type AuthRequestRepo struct { type AuthRequestRepo struct {
Command *command.Commands Command *command.Commands
Query *query.Queries Query *query.Queries
@ -284,7 +286,7 @@ func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID, userAge
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -304,13 +306,28 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, authReqID, user
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
request, err := repo.getAuthRequestEnsureUser(ctx, authReqID, userAgentID, userID) request, err := repo.getAuthRequestEnsureUser(ctx, authReqID, userAgentID, userID)
if err != nil { if err != nil {
if isIgnoreUserNotFoundError(err, request) {
return errors.ThrowInvalidArgument(nil, "EVENT-SDe2f", "Errors.User.UsernameOrPassword.Invalid")
}
return err return err
} }
policy, err := repo.getLockoutPolicy(ctx, resourceOwner) policy, err := repo.getLockoutPolicy(ctx, resourceOwner)
if err != nil { if err != nil {
return err 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 { func lockoutPolicyToDomain(policy *query.LockoutPolicy) *domain.LockoutPolicy {
@ -499,9 +516,9 @@ func (repo *AuthRequestRepo) getAuthRequestEnsureUser(ctx context.Context, authR
if request.UserID != userID { if request.UserID != userID {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-GBH32", "Errors.User.NotMatchingUserID") 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 { if err != nil {
return nil, err return request, err
} }
return request, nil 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) { func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain.AuthRequest, loginName string) (err error) {
user := new(user_view_model.UserView) user := new(user_view_model.UserView)
if request.RequestedOrgID != "" {
preferredLoginName := loginName preferredLoginName := loginName
if request.RequestedOrgID != "" {
if request.RequestedOrgID != "" { if request.RequestedOrgID != "" {
preferredLoginName += "@" + request.RequestedPrimaryDomain 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 { if err != nil {
return err return err
} }
@ -675,6 +701,7 @@ func queryLoginPolicyToDomain(policy *query.LoginPolicy) *domain.LoginPolicy {
MultiFactors: policy.MultiFactors, MultiFactors: policy.MultiFactors,
PasswordlessType: policy.PasswordlessType, PasswordlessType: policy.PasswordlessType,
HidePasswordReset: policy.HidePasswordReset, HidePasswordReset: policy.HidePasswordReset,
IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,
PasswordCheckLifetime: policy.PasswordCheckLifetime, PasswordCheckLifetime: policy.PasswordCheckLifetime,
ExternalLoginCheckLifetime: policy.ExternalLoginCheckLifetime, ExternalLoginCheckLifetime: policy.ExternalLoginCheckLifetime,
MFAInitSkipLifetime: policy.MFAInitSkipLifetime, MFAInitSkipLifetime: policy.MFAInitSkipLifetime,
@ -703,7 +730,7 @@ func (repo *AuthRequestRepo) checkExternalUserLogin(ctx context.Context, request
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -749,11 +776,13 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
} }
return steps, nil 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 { if err != nil {
return nil, err return nil, err
} }
if user.PreferredLoginName != "" {
request.LoginName = user.PreferredLoginName request.LoginName = user.PreferredLoginName
}
userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user) userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1129,10 +1158,16 @@ func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eve
return user_view_model.UserSessionToModel(&sessionCopy, provider.PrefixAvatarURL()), nil 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 // PLANNED: Check LockoutPolicy
user, err := userByID(ctx, userViewProvider, userEventProvider, userID) user, err = userByID(ctx, userViewProvider, userEventProvider, userID)
if err != nil { if err != nil {
if ignoreUnknownUsernames && errors.IsNotFound(err) {
return &user_model.UserView{
ID: userID,
HumanView: &user_model.HumanView{},
}, nil
}
return nil, err return nil, err
} }

View File

@ -421,7 +421,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewNoUser{}, userViewProvider: &mockViewNoUser{},
userEventProvider: &mockEventUser{}, userEventProvider: &mockEventUser{},
}, },
args{&domain.AuthRequest{UserID: "UserID"}, false}, args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
nil, nil,
errors.IsNotFound, 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, nil,
errors.IsPreconditionFailed, 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, nil,
errors.IsPreconditionFailed, 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, nil,
errors.IsInternal, 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, nil,
errors.IsPreconditionFailed, 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, nil,
errors.IsInternal, 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{ []domain.NextStep{&domain.InitUserStep{
PasswordSet: true, PasswordSet: true,
}}, }},

View File

@ -68,7 +68,9 @@ type InstanceSetup struct {
AllowExternalIDP bool AllowExternalIDP bool
ForceMFA bool ForceMFA bool
HidePasswordReset bool HidePasswordReset bool
IgnoreUnknownUsername bool
PasswordlessType domain.PasswordlessType PasswordlessType domain.PasswordlessType
DefaultRedirectURI string
PasswordCheckLifetime time.Duration PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration
MfaInitSkipLifetime time.Duration MfaInitSkipLifetime time.Duration
@ -205,7 +207,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
setup.LoginPolicy.AllowExternalIDP, setup.LoginPolicy.AllowExternalIDP,
setup.LoginPolicy.ForceMFA, setup.LoginPolicy.ForceMFA,
setup.LoginPolicy.HidePasswordReset, setup.LoginPolicy.HidePasswordReset,
setup.LoginPolicy.IgnoreUnknownUsername,
setup.LoginPolicy.PasswordlessType, setup.LoginPolicy.PasswordlessType,
setup.LoginPolicy.DefaultRedirectURI,
setup.LoginPolicy.PasswordCheckLifetime, setup.LoginPolicy.PasswordCheckLifetime,
setup.LoginPolicy.ExternalLoginCheckLifetime, setup.LoginPolicy.ExternalLoginCheckLifetime,
setup.LoginPolicy.MfaInitSkipLifetime, setup.LoginPolicy.MfaInitSkipLifetime,

View File

@ -31,8 +31,10 @@ func writeModelToLoginPolicy(wm *LoginPolicyWriteModel) *domain.LoginPolicy {
AllowRegister: wm.AllowRegister, AllowRegister: wm.AllowRegister,
AllowExternalIDP: wm.AllowExternalIDP, AllowExternalIDP: wm.AllowExternalIDP,
HidePasswordReset: wm.HidePasswordReset, HidePasswordReset: wm.HidePasswordReset,
IgnoreUnknownUsernames: wm.IgnoreUnknownUsernames,
ForceMFA: wm.ForceMFA, ForceMFA: wm.ForceMFA,
PasswordlessType: wm.PasswordlessType, PasswordlessType: wm.PasswordlessType,
DefaultRedirectURI: wm.DefaultRedirectURI,
PasswordCheckLifetime: wm.PasswordCheckLifetime, PasswordCheckLifetime: wm.PasswordCheckLifetime,
ExternalLoginCheckLifetime: wm.ExternalLoginCheckLifetime, ExternalLoginCheckLifetime: wm.ExternalLoginCheckLifetime,
MFAInitSkipLifetime: wm.MFAInitSkipLifetime, MFAInitSkipLifetime: wm.MFAInitSkipLifetime,

View File

@ -17,7 +17,9 @@ func AddDefaultLoginPolicy(
allowExternalIDP bool, allowExternalIDP bool,
forceMFA bool, forceMFA bool,
hidePasswordReset bool, hidePasswordReset bool,
ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType, passwordlessType domain.PasswordlessType,
defaultRedirectURI string,
passwordCheckLifetime time.Duration, passwordCheckLifetime time.Duration,
externalLoginCheckLifetime time.Duration, externalLoginCheckLifetime time.Duration,
mfaInitSkipLifetime time.Duration, mfaInitSkipLifetime time.Duration,
@ -34,7 +36,9 @@ func AddDefaultLoginPolicy(
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset, hidePasswordReset,
ignoreUnknownUsernames,
passwordlessType, passwordlessType,
defaultRedirectURI,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors" caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
@ -56,7 +57,9 @@ func (c *Commands) addDefaultLoginPolicy(ctx context.Context, instanceAgg *event
policy.AllowExternalIDP, policy.AllowExternalIDP,
policy.ForceMFA, policy.ForceMFA,
policy.HidePasswordReset, policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.PasswordlessType, policy.PasswordlessType,
policy.DefaultRedirectURI,
policy.PasswordCheckLifetime, policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime, policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime, 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) { 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) err := c.defaultLoginPolicyWriteModelByID(ctx, existingPolicy)
if err != nil { if err != nil {
return nil, err return nil, err
@ -97,12 +103,15 @@ func (c *Commands) changeDefaultLoginPolicy(ctx context.Context, instanceAgg *ev
policy.AllowExternalIDP, policy.AllowExternalIDP,
policy.ForceMFA, policy.ForceMFA,
policy.HidePasswordReset, policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.PasswordlessType, policy.PasswordlessType,
policy.DefaultRedirectURI,
policy.PasswordCheckLifetime, policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime, policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime, policy.MFAInitSkipLifetime,
policy.SecondFactorCheckLifetime, policy.SecondFactorCheckLifetime,
policy.MultiFactorCheckLifetime) policy.MultiFactorCheckLifetime,
)
if !hasChanged { if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-5M9vdd", "Errors.IAM.LoginPolicy.NotChanged") return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-5M9vdd", "Errors.IAM.LoginPolicy.NotChanged")
} }

View File

@ -65,8 +65,10 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
allowRegister, allowRegister,
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset bool, hidePasswordReset,
ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType, passwordlessType domain.PasswordlessType,
defaultRedirectURI string,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,
@ -93,6 +95,12 @@ func (wm *InstanceLoginPolicyWriteModel) NewChangedEvent(
if wm.HidePasswordReset != hidePasswordReset { if wm.HidePasswordReset != hidePasswordReset {
changes = append(changes, policy.ChangeHidePasswordReset(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 { if wm.PasswordCheckLifetime != passwordCheckLifetime {
changes = append(changes, policy.ChangePasswordCheckLifetime(passwordCheckLifetime)) changes = append(changes, policy.ChangePasswordCheckLifetime(passwordCheckLifetime))
} }

View File

@ -50,7 +50,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*1, time.Hour*1,
time.Hour*1, time.Hour*1,
@ -90,7 +92,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -110,7 +114,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
HidePasswordReset: true, HidePasswordReset: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1, PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3, MFAInitSkipLifetime: time.Hour * 3,
@ -130,7 +136,9 @@ func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
HidePasswordReset: true, HidePasswordReset: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1, PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3, MFAInitSkipLifetime: time.Hour * 3,
@ -210,7 +218,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -229,7 +239,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
HidePasswordReset: true, HidePasswordReset: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1, PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3, MFAInitSkipLifetime: time.Hour * 3,
@ -256,7 +268,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -275,7 +289,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*10, time.Hour*10,
time.Hour*20, time.Hour*20,
time.Hour*30, time.Hour*30,
@ -294,7 +310,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: false, AllowExternalIDP: false,
ForceMFA: false, ForceMFA: false,
HidePasswordReset: false, HidePasswordReset: false,
IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed, PasswordlessType: domain.PasswordlessTypeNotAllowed,
DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10, PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20, ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30, MFAInitSkipLifetime: time.Hour * 30,
@ -314,7 +332,9 @@ func TestCommandSide_ChangeDefaultLoginPolicy(t *testing.T) {
AllowExternalIDP: false, AllowExternalIDP: false,
ForceMFA: false, ForceMFA: false,
HidePasswordReset: false, HidePasswordReset: false,
IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed, PasswordlessType: domain.PasswordlessTypeNotAllowed,
DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10, PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20, ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30, MFAInitSkipLifetime: time.Hour * 30,
@ -408,7 +428,9 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -444,7 +466,9 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -500,7 +524,9 @@ func TestCommandSide_AddIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -638,7 +664,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -674,7 +702,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -723,7 +753,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -777,7 +809,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -839,7 +873,9 @@ func TestCommandSide_RemoveIDPProviderDefaultLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, 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, passwordlessType domain.PasswordlessType,
redirectURI string,
passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime time.Duration) *instance.LoginPolicyChangedEvent { passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime time.Duration) *instance.LoginPolicyChangedEvent {
event, _ := instance.NewLoginPolicyChangedEvent(ctx, event, _ := instance.NewLoginPolicyChangedEvent(ctx,
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
@ -1403,7 +1440,9 @@ func newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allow
policy.ChangeForceMFA(forceMFA), policy.ChangeForceMFA(forceMFA),
policy.ChangeAllowUserNamePassword(allowUsernamePassword), policy.ChangeAllowUserNamePassword(allowUsernamePassword),
policy.ChangeHidePasswordReset(hidePasswordReset), policy.ChangeHidePasswordReset(hidePasswordReset),
policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames),
policy.ChangePasswordlessType(passwordlessType), policy.ChangePasswordlessType(passwordlessType),
policy.ChangeDefaultRedirectURI(redirectURI),
policy.ChangePasswordCheckLifetime(passwordLifetime), policy.ChangePasswordCheckLifetime(passwordLifetime),
policy.ChangeExternalLoginCheckLifetime(externalLoginLifetime), policy.ChangeExternalLoginCheckLifetime(externalLoginLifetime),
policy.ChangeMFAInitSkipLifetime(mfaInitSkipLifetime), policy.ChangeMFAInitSkipLifetime(mfaInitSkipLifetime),

View File

@ -16,6 +16,9 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
if resourceOwner == "" { if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Fn8ds", "Errors.ResourceOwnerMissing") 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) addedPolicy := NewOrgLoginPolicyWriteModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy)
if err != nil { if err != nil {
@ -36,7 +39,9 @@ func (c *Commands) AddLoginPolicy(ctx context.Context, resourceOwner string, pol
policy.AllowExternalIDP, policy.AllowExternalIDP,
policy.ForceMFA, policy.ForceMFA,
policy.HidePasswordReset, policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.PasswordlessType, policy.PasswordlessType,
policy.DefaultRedirectURI,
policy.PasswordCheckLifetime, policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime, policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime, policy.MFAInitSkipLifetime,
@ -76,6 +81,9 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
if resourceOwner == "" { if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-Mf9sf", "Errors.ResourceOwnerMissing") 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) existingPolicy := NewOrgLoginPolicyWriteModel(resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy) err := c.eventstore.FilterToQueryReducer(ctx, existingPolicy)
if err != nil { if err != nil {
@ -94,7 +102,9 @@ func (c *Commands) ChangeLoginPolicy(ctx context.Context, resourceOwner string,
policy.AllowExternalIDP, policy.AllowExternalIDP,
policy.ForceMFA, policy.ForceMFA,
policy.HidePasswordReset, policy.HidePasswordReset,
policy.IgnoreUnknownUsernames,
policy.PasswordlessType, policy.PasswordlessType,
policy.DefaultRedirectURI,
policy.PasswordCheckLifetime, policy.PasswordCheckLifetime,
policy.ExternalLoginCheckLifetime, policy.ExternalLoginCheckLifetime,
policy.MFAInitSkipLifetime, policy.MFAInitSkipLifetime,

View File

@ -67,8 +67,10 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent(
allowRegister, allowRegister,
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset bool, hidePasswordReset,
ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType, passwordlessType domain.PasswordlessType,
defaultRedirectURI string,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,
@ -92,6 +94,9 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent(
if wm.HidePasswordReset != hidePasswordReset { if wm.HidePasswordReset != hidePasswordReset {
changes = append(changes, policy.ChangeHidePasswordReset(hidePasswordReset)) changes = append(changes, policy.ChangeHidePasswordReset(hidePasswordReset))
} }
if wm.IgnoreUnknownUsernames != ignoreUnknownUsernames {
changes = append(changes, policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames))
}
if wm.PasswordCheckLifetime != passwordCheckLifetime { if wm.PasswordCheckLifetime != passwordCheckLifetime {
changes = append(changes, policy.ChangePasswordCheckLifetime(passwordCheckLifetime)) changes = append(changes, policy.ChangePasswordCheckLifetime(passwordCheckLifetime))
} }
@ -110,6 +115,9 @@ func (wm *OrgLoginPolicyWriteModel) NewChangedEvent(
if passwordlessType.Valid() && wm.PasswordlessType != passwordlessType { if passwordlessType.Valid() && wm.PasswordlessType != passwordlessType {
changes = append(changes, policy.ChangePasswordlessType(passwordlessType)) changes = append(changes, policy.ChangePasswordlessType(passwordlessType))
} }
if wm.DefaultRedirectURI != defaultRedirectURI {
changes = append(changes, policy.ChangeDefaultRedirectURI(defaultRedirectURI))
}
if len(changes) == 0 { if len(changes) == 0 {
return nil, false return nil, false
} }

View File

@ -77,7 +77,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -96,7 +98,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowUsernamePassword: true, AllowUsernamePassword: true,
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1, PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3, MFAInitSkipLifetime: time.Hour * 3,
@ -124,7 +128,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -145,7 +151,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
HidePasswordReset: true, HidePasswordReset: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1, PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3, MFAInitSkipLifetime: time.Hour * 3,
@ -164,7 +172,9 @@ func TestCommandSide_AddLoginPolicy(t *testing.T) {
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
HidePasswordReset: true, HidePasswordReset: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1, PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3, MFAInitSkipLifetime: time.Hour * 3,
@ -226,7 +236,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: true, AllowUsernamePassword: true,
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
}, },
}, },
res: res{ res: res{
@ -249,7 +261,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: true, AllowUsernamePassword: true,
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
}, },
}, },
res: res{ res: res{
@ -270,7 +284,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -290,7 +306,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowExternalIDP: true, AllowExternalIDP: true,
ForceMFA: true, ForceMFA: true,
HidePasswordReset: true, HidePasswordReset: true,
IgnoreUnknownUsernames: true,
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 1, PasswordCheckLifetime: time.Hour * 1,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: time.Hour * 3, MFAInitSkipLifetime: time.Hour * 3,
@ -316,7 +334,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"https://example.com/redirect",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -335,7 +355,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
&duration10, &duration10,
&duration20, &duration20,
&duration30, &duration30,
@ -355,7 +377,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowUsernamePassword: false, AllowUsernamePassword: false,
AllowExternalIDP: false, AllowExternalIDP: false,
ForceMFA: false, ForceMFA: false,
IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed, PasswordlessType: domain.PasswordlessTypeNotAllowed,
DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10, PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20, ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30, MFAInitSkipLifetime: time.Hour * 30,
@ -374,7 +398,9 @@ func TestCommandSide_ChangeLoginPolicy(t *testing.T) {
AllowExternalIDP: false, AllowExternalIDP: false,
ForceMFA: false, ForceMFA: false,
HidePasswordReset: false, HidePasswordReset: false,
IgnoreUnknownUsernames: false,
PasswordlessType: domain.PasswordlessTypeNotAllowed, PasswordlessType: domain.PasswordlessTypeNotAllowed,
DefaultRedirectURI: "",
PasswordCheckLifetime: time.Hour * 10, PasswordCheckLifetime: time.Hour * 10,
ExternalLoginCheckLifetime: time.Hour * 20, ExternalLoginCheckLifetime: time.Hour * 20,
MFAInitSkipLifetime: time.Hour * 30, MFAInitSkipLifetime: time.Hour * 30,
@ -465,7 +491,9 @@ func TestCommandSide_RemoveLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -603,7 +631,9 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -642,7 +672,9 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -701,7 +733,9 @@ func TestCommandSide_AddIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -863,7 +897,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -902,7 +938,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -953,7 +991,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1011,7 +1051,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1077,7 +1119,9 @@ func TestCommandSide_RemoveIDPProviderLoginPolicy(t *testing.T) {
true, true,
true, true,
true, true,
true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, 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, passwordlessType domain.PasswordlessType,
redirectURI string,
passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime *time.Duration) *org.LoginPolicyChangedEvent { passwordLifetime, externalLoginLifetime, mfaInitSkipLifetime, secondFactorLifetime, multiFactorLifetime *time.Duration) *org.LoginPolicyChangedEvent {
changes := []policy.LoginPolicyChanges{ changes := []policy.LoginPolicyChanges{
policy.ChangeAllowUserNamePassword(usernamePassword), policy.ChangeAllowUserNamePassword(usernamePassword),
@ -1703,7 +1748,9 @@ func newLoginPolicyChangedEvent(ctx context.Context, orgID string, usernamePassw
policy.ChangeAllowExternalIDP(externalIDP), policy.ChangeAllowExternalIDP(externalIDP),
policy.ChangeForceMFA(mfa), policy.ChangeForceMFA(mfa),
policy.ChangeHidePasswordReset(passwordReset), policy.ChangeHidePasswordReset(passwordReset),
policy.ChangeIgnoreUnknownUsernames(ignoreUnknownUsernames),
policy.ChangePasswordlessType(passwordlessType), policy.ChangePasswordlessType(passwordlessType),
policy.ChangeDefaultRedirectURI(redirectURI),
} }
if passwordLifetime != nil { if passwordLifetime != nil {
changes = append(changes, policy.ChangePasswordCheckLifetime(*passwordLifetime)) changes = append(changes, policy.ChangePasswordCheckLifetime(*passwordLifetime))

View File

@ -16,7 +16,9 @@ type LoginPolicyWriteModel struct {
AllowExternalIDP bool AllowExternalIDP bool
ForceMFA bool ForceMFA bool
HidePasswordReset bool HidePasswordReset bool
IgnoreUnknownUsernames bool
PasswordlessType domain.PasswordlessType PasswordlessType domain.PasswordlessType
DefaultRedirectURI string
PasswordCheckLifetime time.Duration PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration
MFAInitSkipLifetime time.Duration MFAInitSkipLifetime time.Duration
@ -35,6 +37,8 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
wm.ForceMFA = e.ForceMFA wm.ForceMFA = e.ForceMFA
wm.PasswordlessType = e.PasswordlessType wm.PasswordlessType = e.PasswordlessType
wm.HidePasswordReset = e.HidePasswordReset wm.HidePasswordReset = e.HidePasswordReset
wm.IgnoreUnknownUsernames = e.IgnoreUnknownUsernames
wm.DefaultRedirectURI = e.DefaultRedirectURI
wm.PasswordCheckLifetime = e.PasswordCheckLifetime wm.PasswordCheckLifetime = e.PasswordCheckLifetime
wm.ExternalLoginCheckLifetime = e.ExternalLoginCheckLifetime wm.ExternalLoginCheckLifetime = e.ExternalLoginCheckLifetime
wm.MFAInitSkipLifetime = e.MFAInitSkipLifetime wm.MFAInitSkipLifetime = e.MFAInitSkipLifetime
@ -57,9 +61,15 @@ func (wm *LoginPolicyWriteModel) Reduce() error {
if e.HidePasswordReset != nil { if e.HidePasswordReset != nil {
wm.HidePasswordReset = *e.HidePasswordReset wm.HidePasswordReset = *e.HidePasswordReset
} }
if e.IgnoreUnknownUsernames != nil {
wm.IgnoreUnknownUsernames = *e.IgnoreUnknownUsernames
}
if e.PasswordlessType != nil { if e.PasswordlessType != nil {
wm.PasswordlessType = *e.PasswordlessType wm.PasswordlessType = *e.PasswordlessType
} }
if e.DefaultRedirectURI != nil {
wm.DefaultRedirectURI = *e.DefaultRedirectURI
}
if e.PasswordCheckLifetime != nil { if e.PasswordCheckLifetime != nil {
wm.PasswordCheckLifetime = *e.PasswordCheckLifetime wm.PasswordCheckLifetime = *e.PasswordCheckLifetime
} }

View File

@ -1156,7 +1156,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1191,7 +1193,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1227,7 +1231,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1279,7 +1285,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1365,7 +1373,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1458,7 +1468,9 @@ func TestCommandSide_CheckPassword(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,

View File

@ -1687,7 +1687,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1750,7 +1752,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1813,7 +1817,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -1893,7 +1899,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -2031,7 +2039,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -2137,7 +2147,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -2237,7 +2249,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,
@ -2359,7 +2373,9 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
false, false,
false, false,
false, false,
false,
domain.PasswordlessTypeNotAllowed, domain.PasswordlessTypeNotAllowed,
"",
time.Hour*1, time.Hour*1,
time.Hour*2, time.Hour*2,
time.Hour*3, time.Hour*3,

View File

@ -1,6 +1,7 @@
package domain package domain
import ( import (
"net/url"
"time" "time"
"github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/eventstore/v1/models"
@ -19,6 +20,8 @@ type LoginPolicy struct {
MultiFactors []MultiFactorType MultiFactors []MultiFactorType
PasswordlessType PasswordlessType PasswordlessType PasswordlessType
HidePasswordReset bool HidePasswordReset bool
IgnoreUnknownUsernames bool
DefaultRedirectURI string
PasswordCheckLifetime time.Duration PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration
MFAInitSkipLifetime time.Duration MFAInitSkipLifetime time.Duration
@ -26,6 +29,24 @@ type LoginPolicy struct {
MultiFactorCheckLifetime time.Duration 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 { type IDPProvider struct {
models.ObjectRoot models.ObjectRoot
Type IdentityProviderType Type IdentityProviderType

View File

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

View File

@ -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 { 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) var args = mapNotifyUserToArgs(user)
args["TempUsername"] = username args["TempUsername"] = username
args["Domain"] = strings.Split(user.LastEmail, "@")[1] args["Domain"] = strings.Split(user.LastEmail, "@")[1]

View File

@ -26,7 +26,7 @@ func SendEmailVerificationCode(ctx context.Context, mailhtml string, translator
if err != nil { if err != nil {
return err return err
} }
url := login.MailVerificationLink(origin, user.ID, codeString) url := login.MailVerificationLink(origin, user.ID, codeString, user.ResourceOwner)
var args = mapNotifyUserToArgs(user) var args = mapNotifyUserToArgs(user)
args["Code"] = codeString args["Code"] = codeString

View File

@ -25,6 +25,7 @@ type UrlData struct {
UserID string UserID string
Code string Code string
PasswordSet bool 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 { 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 { if err != nil {
return err 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) var args = mapNotifyUserToArgs(user)
args["Code"] = codeString args["Code"] = codeString

View File

@ -29,7 +29,7 @@ func SendPasswordCode(ctx context.Context, mailhtml string, translator *i18n.Tra
if err != nil { if err != nil {
return err return err
} }
url := login.InitPasswordLink(origin, user.ID, codeString) url := login.InitPasswordLink(origin, user.ID, codeString, user.ResourceOwner)
var args = mapNotifyUserToArgs(user) var args = mapNotifyUserToArgs(user)
args["Code"] = codeString args["Code"] = codeString

View File

@ -307,3 +307,25 @@ func prepareLabelPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LabelPolicy,
return policy, nil 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,
}
}

View File

@ -10,7 +10,6 @@ import (
"github.com/lib/pq" "github.com/lib/pq"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/query/projection"
@ -30,6 +29,8 @@ type LoginPolicy struct {
PasswordlessType domain.PasswordlessType PasswordlessType domain.PasswordlessType
IsDefault bool IsDefault bool
HidePasswordReset bool HidePasswordReset bool
IgnoreUnknownUsernames bool
DefaultRedirectURI string
PasswordCheckLifetime time.Duration PasswordCheckLifetime time.Duration
ExternalLoginCheckLifetime time.Duration ExternalLoginCheckLifetime time.Duration
MFAInitSkipLifetime time.Duration MFAInitSkipLifetime time.Duration
@ -107,6 +108,14 @@ var (
name: projection.LoginPolicyHidePWResetCol, name: projection.LoginPolicyHidePWResetCol,
table: loginPolicyTable, table: loginPolicyTable,
} }
LoginPolicyColumnIgnoreUnknownUsernames = Column{
name: projection.IgnoreUnknownUsernames,
table: loginPolicyTable,
}
LoginPolicyColumnDefaultRedirectURI = Column{
name: projection.DefaultRedirectURI,
table: loginPolicyTable,
}
LoginPolicyColumnPasswordCheckLifetime = Column{ LoginPolicyColumnPasswordCheckLifetime = Column{
name: projection.PasswordCheckLifetimeCol, name: projection.PasswordCheckLifetimeCol,
table: loginPolicyTable, table: loginPolicyTable,
@ -284,6 +293,8 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
LoginPolicyColumnPasswordlessType.identifier(), LoginPolicyColumnPasswordlessType.identifier(),
LoginPolicyColumnIsDefault.identifier(), LoginPolicyColumnIsDefault.identifier(),
LoginPolicyColumnHidePasswordReset.identifier(), LoginPolicyColumnHidePasswordReset.identifier(),
LoginPolicyColumnIgnoreUnknownUsernames.identifier(),
LoginPolicyColumnDefaultRedirectURI.identifier(),
LoginPolicyColumnPasswordCheckLifetime.identifier(), LoginPolicyColumnPasswordCheckLifetime.identifier(),
LoginPolicyColumnExternalLoginCheckLifetime.identifier(), LoginPolicyColumnExternalLoginCheckLifetime.identifier(),
LoginPolicyColumnMFAInitSkipLifetime.identifier(), LoginPolicyColumnMFAInitSkipLifetime.identifier(),
@ -294,6 +305,7 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
p := new(LoginPolicy) p := new(LoginPolicy)
secondFactors := pq.Int32Array{} secondFactors := pq.Int32Array{}
multiFactors := pq.Int32Array{} multiFactors := pq.Int32Array{}
defaultRedirectURI := sql.NullString{}
err := row.Scan( err := row.Scan(
&p.OrgID, &p.OrgID,
&p.CreationDate, &p.CreationDate,
@ -308,6 +320,8 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
&p.PasswordlessType, &p.PasswordlessType,
&p.IsDefault, &p.IsDefault,
&p.HidePasswordReset, &p.HidePasswordReset,
&p.IgnoreUnknownUsernames,
&defaultRedirectURI,
&p.PasswordCheckLifetime, &p.PasswordCheckLifetime,
&p.ExternalLoginCheckLifetime, &p.ExternalLoginCheckLifetime,
&p.MFAInitSkipLifetime, &p.MFAInitSkipLifetime,
@ -320,7 +334,7 @@ func prepareLoginPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*LoginPolicy,
} }
return nil, errors.ThrowInternal(err, "QUERY-YcC53", "Errors.Internal") return nil, errors.ThrowInternal(err, "QUERY-YcC53", "Errors.Internal")
} }
p.DefaultRedirectURI = defaultRedirectURI.String
p.MultiFactors = make([]domain.MultiFactorType, len(multiFactors)) p.MultiFactors = make([]domain.MultiFactorType, len(multiFactors))
for i, mfa := range multiFactors { for i, mfa := range multiFactors {
p.MultiFactors[i] = domain.MultiFactorType(mfa) p.MultiFactors[i] = domain.MultiFactorType(mfa)

View File

@ -44,6 +44,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
` projections.login_policies.passwordless_type,`+ ` projections.login_policies.passwordless_type,`+
` projections.login_policies.is_default,`+ ` projections.login_policies.is_default,`+
` projections.login_policies.hide_password_reset,`+ ` 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.password_check_lifetime,`+
` projections.login_policies.external_login_check_lifetime,`+ ` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_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.passwordless_type,`+
` projections.login_policies.is_default,`+ ` projections.login_policies.is_default,`+
` projections.login_policies.hide_password_reset,`+ ` 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.password_check_lifetime,`+
` projections.login_policies.external_login_check_lifetime,`+ ` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+ ` projections.login_policies.mfa_init_skip_lifetime,`+
@ -100,6 +104,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
"passwordless_type", "passwordless_type",
"is_default", "is_default",
"hide_password_reset", "hide_password_reset",
"ignore_unknown_usernames",
"default_redirect_uri",
"password_check_lifetime", "password_check_lifetime",
"external_login_check_lifetime", "external_login_check_lifetime",
"mfa_init_skip_lifetime", "mfa_init_skip_lifetime",
@ -120,6 +126,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
true, true,
true, true,
true,
"https://example.com/redirect",
time.Hour * 2, time.Hour * 2,
time.Hour * 2, time.Hour * 2,
time.Hour * 2, time.Hour * 2,
@ -142,6 +150,8 @@ func Test_LoginPolicyPrepares(t *testing.T) {
PasswordlessType: domain.PasswordlessTypeAllowed, PasswordlessType: domain.PasswordlessTypeAllowed,
IsDefault: true, IsDefault: true,
HidePasswordReset: true, HidePasswordReset: true,
IgnoreUnknownUsernames: true,
DefaultRedirectURI: "https://example.com/redirect",
PasswordCheckLifetime: time.Hour * 2, PasswordCheckLifetime: time.Hour * 2,
ExternalLoginCheckLifetime: time.Hour * 2, ExternalLoginCheckLifetime: time.Hour * 2,
MFAInitSkipLifetime: 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.passwordless_type,`+
` projections.login_policies.is_default,`+ ` projections.login_policies.is_default,`+
` projections.login_policies.hide_password_reset,`+ ` 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.password_check_lifetime,`+
` projections.login_policies.external_login_check_lifetime,`+ ` projections.login_policies.external_login_check_lifetime,`+
` projections.login_policies.mfa_init_skip_lifetime,`+ ` projections.login_policies.mfa_init_skip_lifetime,`+

View File

@ -29,6 +29,8 @@ const (
LoginPolicyMFAsCol = "multi_factors" LoginPolicyMFAsCol = "multi_factors"
LoginPolicyPasswordlessTypeCol = "passwordless_type" LoginPolicyPasswordlessTypeCol = "passwordless_type"
LoginPolicyHidePWResetCol = "hide_password_reset" LoginPolicyHidePWResetCol = "hide_password_reset"
IgnoreUnknownUsernames = "ignore_unknown_usernames"
DefaultRedirectURI = "default_redirect_uri"
PasswordCheckLifetimeCol = "password_check_lifetime" PasswordCheckLifetimeCol = "password_check_lifetime"
ExternalLoginCheckLifetimeCol = "external_login_check_lifetime" ExternalLoginCheckLifetimeCol = "external_login_check_lifetime"
MFAInitSkipLifetimeCol = "mfa_init_skip_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(LoginPolicyMFAsCol, crdb.ColumnTypeEnumArray, crdb.Nullable()),
crdb.NewColumn(LoginPolicyPasswordlessTypeCol, crdb.ColumnTypeEnum), crdb.NewColumn(LoginPolicyPasswordlessTypeCol, crdb.ColumnTypeEnum),
crdb.NewColumn(LoginPolicyHidePWResetCol, crdb.ColumnTypeBool), crdb.NewColumn(LoginPolicyHidePWResetCol, crdb.ColumnTypeBool),
crdb.NewColumn(IgnoreUnknownUsernames, crdb.ColumnTypeBool),
crdb.NewColumn(DefaultRedirectURI, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(PasswordCheckLifetimeCol, crdb.ColumnTypeInt64), crdb.NewColumn(PasswordCheckLifetimeCol, crdb.ColumnTypeInt64),
crdb.NewColumn(ExternalLoginCheckLifetimeCol, crdb.ColumnTypeInt64), crdb.NewColumn(ExternalLoginCheckLifetimeCol, crdb.ColumnTypeInt64),
crdb.NewColumn(MFAInitSkipLifetimeCol, 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(LoginPolicyPasswordlessTypeCol, policyEvent.PasswordlessType),
handler.NewCol(LoginPolicyIsDefaultCol, isDefault), handler.NewCol(LoginPolicyIsDefaultCol, isDefault),
handler.NewCol(LoginPolicyHidePWResetCol, policyEvent.HidePasswordReset), handler.NewCol(LoginPolicyHidePWResetCol, policyEvent.HidePasswordReset),
handler.NewCol(IgnoreUnknownUsernames, policyEvent.IgnoreUnknownUsernames),
handler.NewCol(DefaultRedirectURI, policyEvent.DefaultRedirectURI),
handler.NewCol(PasswordCheckLifetimeCol, policyEvent.PasswordCheckLifetime), handler.NewCol(PasswordCheckLifetimeCol, policyEvent.PasswordCheckLifetime),
handler.NewCol(ExternalLoginCheckLifetimeCol, policyEvent.ExternalLoginCheckLifetime), handler.NewCol(ExternalLoginCheckLifetimeCol, policyEvent.ExternalLoginCheckLifetime),
handler.NewCol(MFAInitSkipLifetimeCol, policyEvent.MFAInitSkipLifetime), handler.NewCol(MFAInitSkipLifetimeCol, policyEvent.MFAInitSkipLifetime),
@ -208,6 +214,12 @@ func (p *LoginPolicyProjection) reduceLoginPolicyChanged(event eventstore.Event)
if policyEvent.HidePasswordReset != nil { if policyEvent.HidePasswordReset != nil {
cols = append(cols, handler.NewCol(LoginPolicyHidePWResetCol, *policyEvent.HidePasswordReset)) 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 { if policyEvent.PasswordCheckLifetime != nil {
cols = append(cols, handler.NewCol(PasswordCheckLifetimeCol, *policyEvent.PasswordCheckLifetime)) cols = append(cols, handler.NewCol(PasswordCheckLifetimeCol, *policyEvent.PasswordCheckLifetime))
} }

View File

@ -35,7 +35,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": false, "allowExternalIdp": false,
"forceMFA": false, "forceMFA": false,
"hidePasswordReset": true, "hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"passwordlessType": 1, "passwordlessType": 1,
"defaultRedirectURI": "https://example.com/redirect",
"passwordCheckLifetime": 10000000, "passwordCheckLifetime": 10000000,
"externalLoginCheckLifetime": 10000000, "externalLoginCheckLifetime": 10000000,
"mfaInitSkipLifetime": 10000000, "mfaInitSkipLifetime": 10000000,
@ -53,7 +55,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -67,6 +69,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
false, false,
true, true,
true,
"https://example.com/redirect",
time.Millisecond * 10, time.Millisecond * 10,
time.Millisecond * 10, time.Millisecond * 10,
time.Millisecond * 10, time.Millisecond * 10,
@ -91,7 +95,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": true, "allowExternalIdp": true,
"forceMFA": true, "forceMFA": true,
"hidePasswordReset": true, "hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"passwordlessType": 1, "passwordlessType": 1,
"defaultRedirectURI": "https://example.com/redirect",
"passwordCheckLifetime": 10000000, "passwordCheckLifetime": 10000000,
"externalLoginCheckLifetime": 10000000, "externalLoginCheckLifetime": 10000000,
"mfaInitSkipLifetime": 10000000, "mfaInitSkipLifetime": 10000000,
@ -108,7 +114,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
uint64(15), uint64(15),
@ -118,6 +124,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true, true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
true, true,
true,
"https://example.com/redirect",
time.Millisecond * 10, time.Millisecond * 10,
time.Millisecond * 10, time.Millisecond * 10,
time.Millisecond * 10, time.Millisecond * 10,
@ -298,7 +306,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": false, "allowExternalIdp": false,
"forceMFA": false, "forceMFA": false,
"hidePasswordReset": true, "hidePasswordReset": true,
"ignoreUnknownUsernames": true,
"passwordlessType": 1, "passwordlessType": 1,
"defaultRedirectURI": "https://example.com/redirect",
"passwordCheckLifetime": 10000000, "passwordCheckLifetime": 10000000,
"externalLoginCheckLifetime": 10000000, "externalLoginCheckLifetime": 10000000,
"mfaInitSkipLifetime": 10000000, "mfaInitSkipLifetime": 10000000,
@ -315,7 +325,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -329,6 +339,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
true, true,
true, true,
true,
"https://example.com/redirect",
time.Millisecond * 10, time.Millisecond * 10,
time.Millisecond * 10, time.Millisecond * 10,
time.Millisecond * 10, time.Millisecond * 10,
@ -353,7 +365,9 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
"allowExternalIdp": true, "allowExternalIdp": true,
"forceMFA": true, "forceMFA": true,
"hidePasswordReset": true, "hidePasswordReset": true,
"passwordlessType": 1 "ignoreUnknownUsernames": true,
"passwordlessType": 1,
"defaultRedirectURI": "https://example.com/redirect"
}`), }`),
), instance.LoginPolicyChangedEventMapper), ), instance.LoginPolicyChangedEventMapper),
}, },
@ -365,7 +379,7 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
uint64(15), uint64(15),
@ -375,6 +389,8 @@ func TestLoginPolicyProjection_reduces(t *testing.T) {
true, true,
domain.PasswordlessTypeAllowed, domain.PasswordlessTypeAllowed,
true, true,
true,
"https://example.com/redirect",
"agg-id", "agg-id",
}, },
}, },

View File

@ -27,8 +27,10 @@ func NewLoginPolicyAddedEvent(
allowRegister, allowRegister,
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset bool, hidePasswordReset,
ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType, passwordlessType domain.PasswordlessType,
defaultRedirectURI string,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,
@ -46,7 +48,9 @@ func NewLoginPolicyAddedEvent(
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset, hidePasswordReset,
ignoreUnknownUsernames,
passwordlessType, passwordlessType,
defaultRedirectURI,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,

View File

@ -28,8 +28,10 @@ func NewLoginPolicyAddedEvent(
allowRegister, allowRegister,
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset bool, hidePasswordReset,
ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType, passwordlessType domain.PasswordlessType,
defaultRedirectURI string,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,
@ -47,7 +49,9 @@ func NewLoginPolicyAddedEvent(
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset, hidePasswordReset,
ignoreUnknownUsernames,
passwordlessType, passwordlessType,
defaultRedirectURI,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,

View File

@ -25,7 +25,9 @@ type LoginPolicyAddedEvent struct {
AllowExternalIDP bool `json:"allowExternalIdp,omitempty"` AllowExternalIDP bool `json:"allowExternalIdp,omitempty"`
ForceMFA bool `json:"forceMFA,omitempty"` ForceMFA bool `json:"forceMFA,omitempty"`
HidePasswordReset bool `json:"hidePasswordReset,omitempty"` HidePasswordReset bool `json:"hidePasswordReset,omitempty"`
IgnoreUnknownUsernames bool `json:"ignoreUnknownUsernames,omitempty"`
PasswordlessType domain.PasswordlessType `json:"passwordlessType,omitempty"` PasswordlessType domain.PasswordlessType `json:"passwordlessType,omitempty"`
DefaultRedirectURI string `json:"defaultRedirectURI,omitempty"`
PasswordCheckLifetime time.Duration `json:"passwordCheckLifetime,omitempty"` PasswordCheckLifetime time.Duration `json:"passwordCheckLifetime,omitempty"`
ExternalLoginCheckLifetime time.Duration `json:"externalLoginCheckLifetime,omitempty"` ExternalLoginCheckLifetime time.Duration `json:"externalLoginCheckLifetime,omitempty"`
MFAInitSkipLifetime time.Duration `json:"mfaInitSkipLifetime,omitempty"` MFAInitSkipLifetime time.Duration `json:"mfaInitSkipLifetime,omitempty"`
@ -47,8 +49,10 @@ func NewLoginPolicyAddedEvent(
allowRegister, allowRegister,
allowExternalIDP, allowExternalIDP,
forceMFA, forceMFA,
hidePasswordReset bool, hidePasswordReset,
ignoreUnknownUsernames bool,
passwordlessType domain.PasswordlessType, passwordlessType domain.PasswordlessType,
defaultRedirectURI string,
passwordCheckLifetime, passwordCheckLifetime,
externalLoginCheckLifetime, externalLoginCheckLifetime,
mfaInitSkipLifetime, mfaInitSkipLifetime,
@ -63,6 +67,8 @@ func NewLoginPolicyAddedEvent(
ForceMFA: forceMFA, ForceMFA: forceMFA,
PasswordlessType: passwordlessType, PasswordlessType: passwordlessType,
HidePasswordReset: hidePasswordReset, HidePasswordReset: hidePasswordReset,
IgnoreUnknownUsernames: ignoreUnknownUsernames,
DefaultRedirectURI: defaultRedirectURI,
PasswordCheckLifetime: passwordCheckLifetime, PasswordCheckLifetime: passwordCheckLifetime,
ExternalLoginCheckLifetime: externalLoginCheckLifetime, ExternalLoginCheckLifetime: externalLoginCheckLifetime,
MFAInitSkipLifetime: mfaInitSkipLifetime, MFAInitSkipLifetime: mfaInitSkipLifetime,
@ -92,7 +98,9 @@ type LoginPolicyChangedEvent struct {
AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"` AllowExternalIDP *bool `json:"allowExternalIdp,omitempty"`
ForceMFA *bool `json:"forceMFA,omitempty"` ForceMFA *bool `json:"forceMFA,omitempty"`
HidePasswordReset *bool `json:"hidePasswordReset,omitempty"` HidePasswordReset *bool `json:"hidePasswordReset,omitempty"`
IgnoreUnknownUsernames *bool `json:"ignoreUnknownUsernames,omitempty"`
PasswordlessType *domain.PasswordlessType `json:"passwordlessType,omitempty"` PasswordlessType *domain.PasswordlessType `json:"passwordlessType,omitempty"`
DefaultRedirectURI *string `json:"defaultRedirectURI,omitempty"`
PasswordCheckLifetime *time.Duration `json:"passwordCheckLifetime,omitempty"` PasswordCheckLifetime *time.Duration `json:"passwordCheckLifetime,omitempty"`
ExternalLoginCheckLifetime *time.Duration `json:"externalLoginCheckLifetime,omitempty"` ExternalLoginCheckLifetime *time.Duration `json:"externalLoginCheckLifetime,omitempty"`
MFAInitSkipLifetime *time.Duration `json:"mfaInitSkipLifetime,omitempty"` MFAInitSkipLifetime *time.Duration `json:"mfaInitSkipLifetime,omitempty"`
@ -167,26 +175,43 @@ func ChangePasswordCheckLifetime(passwordCheckLifetime time.Duration) func(*Logi
e.PasswordCheckLifetime = &passwordCheckLifetime e.PasswordCheckLifetime = &passwordCheckLifetime
} }
} }
func ChangeExternalLoginCheckLifetime(externalLoginCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) { func ChangeExternalLoginCheckLifetime(externalLoginCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) { return func(e *LoginPolicyChangedEvent) {
e.ExternalLoginCheckLifetime = &externalLoginCheckLifetime e.ExternalLoginCheckLifetime = &externalLoginCheckLifetime
} }
} }
func ChangeMFAInitSkipLifetime(mfaInitSkipLifetime time.Duration) func(*LoginPolicyChangedEvent) { func ChangeMFAInitSkipLifetime(mfaInitSkipLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) { return func(e *LoginPolicyChangedEvent) {
e.MFAInitSkipLifetime = &mfaInitSkipLifetime e.MFAInitSkipLifetime = &mfaInitSkipLifetime
} }
} }
func ChangeSecondFactorCheckLifetime(secondFactorCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) { func ChangeSecondFactorCheckLifetime(secondFactorCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) { return func(e *LoginPolicyChangedEvent) {
e.SecondFactorCheckLifetime = &secondFactorCheckLifetime e.SecondFactorCheckLifetime = &secondFactorCheckLifetime
} }
} }
func ChangeMultiFactorCheckLifetime(multiFactorCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) { func ChangeMultiFactorCheckLifetime(multiFactorCheckLifetime time.Duration) func(*LoginPolicyChangedEvent) {
return func(e *LoginPolicyChangedEvent) { return func(e *LoginPolicyChangedEvent) {
e.MultiFactorCheckLifetime = &multiFactorCheckLifetime 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) { func LoginPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &LoginPolicyChangedEvent{ e := &LoginPolicyChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event), BaseEvent: *eventstore.BaseEventFromRepo(event),

View File

@ -176,6 +176,7 @@ Errors:
LoginPolicy: LoginPolicy:
NotFound: Login Policy konnte nicht gefunden werden NotFound: Login Policy konnte nicht gefunden werden
Invalid: Login Policy ist ungültig Invalid: Login Policy ist ungültig
RedirectURIInvalid: Default Redirect URI ist ungültig
NotExisting: Login Policy existiert nicht auf dieser Organisation NotExisting: Login Policy existiert nicht auf dieser Organisation
AlreadyExists: Login Policy existiert bereits AlreadyExists: Login Policy existiert bereits
IdpProviderAlreadyExisting: Idp Provider existiert bereits IdpProviderAlreadyExisting: Idp Provider existiert bereits
@ -285,6 +286,7 @@ Errors:
NotChanged: Default Login Policy wurde nicht verändert NotChanged: Default Login Policy wurde nicht verändert
NotExisting: Default Login Policy existiert nicht NotExisting: Default Login Policy existiert nicht
AlreadyExists: Default Login Policy existiert bereits AlreadyExists: Default Login Policy existiert bereits
RedirectURIInvalid: Default Redirect URI ist ungültig
MFA: MFA:
AlreadyExists: Multifaktor existiert bereits AlreadyExists: Multifaktor existiert bereits
NotExisting: Multifaktor existiert nicht NotExisting: Multifaktor existiert nicht

View File

@ -176,7 +176,8 @@ Errors:
LoginPolicy: LoginPolicy:
NotFound: Login Policy not found NotFound: Login Policy not found
Invalid: Login Policy is invalid 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 AlreadyExists: Login Policy already exists
IdpProviderAlreadyExisting: Idp Provider already existing IdpProviderAlreadyExisting: Idp Provider already existing
IdpProviderNotExisting: Idp Provider not existing IdpProviderNotExisting: Idp Provider not existing
@ -285,6 +286,7 @@ Errors:
NotChanged: Default Login Policy has not been changed NotChanged: Default Login Policy has not been changed
NotExisting: Default Login Policy not existig NotExisting: Default Login Policy not existig
AlreadyExists: Default Login Policy already exists AlreadyExists: Default Login Policy already exists
RedirectURIInvalid: Default Redirect URI is invalid
MFA: MFA:
AlreadyExists: Multifactor already exists AlreadyExists: Multifactor already exists
NotExisting: Multifactor not existing NotExisting: Multifactor not existing

View File

@ -176,6 +176,7 @@ Errors:
LoginPolicy: LoginPolicy:
NotFound: Impostazioni di accesso non trovati NotFound: Impostazioni di accesso non trovati
Invalid: Impostazioni di accesso non sono validi Invalid: Impostazioni di accesso non sono validi
RedirectURIInvalid: Default Redirect URI non valido
NotExisting: Impostazioni di accesso non esistenti NotExisting: Impostazioni di accesso non esistenti
AlreadyExists: Impostazioni di accesso già esistenti AlreadyExists: Impostazioni di accesso già esistenti
IdpProviderAlreadyExisting: IDP già esistente IdpProviderAlreadyExisting: IDP già esistente
@ -283,6 +284,7 @@ Errors:
NotChanged: Le impostazioni di accesso predefinite non sono state cambiate NotChanged: Le impostazioni di accesso predefinite non sono state cambiate
NotExisting: Impostazioni di accesso predefinite non esistenti NotExisting: Impostazioni di accesso predefinite non esistenti
AlreadyExists: Impostazioni di accesso predefinite già esistenti AlreadyExists: Impostazioni di accesso predefinite già esistenti
RedirectURIInvalid: Default Redirect URI non valido
MFA: MFA:
AlreadyExists: Multifattore già esistente AlreadyExists: Multifattore già esistente
NotExisting: Multifattore non esistente NotExisting: Multifattore non esistente

View File

@ -3677,11 +3677,21 @@ message UpdateLoginPolicyRequest {
description: "defines if password reset link should be shown in the login screen" description: "defines if password reset link should be shown in the login screen"
} }
]; ];
google.protobuf.Duration password_check_lifetime = 7; bool ignore_unknown_usernames = 7 [
google.protobuf.Duration external_login_check_lifetime = 8; (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
google.protobuf.Duration mfa_init_skip_lifetime = 9; description: "defines if unknown username on login screen directly return an error or always display the password screen"
google.protobuf.Duration second_factor_check_lifetime = 10; }
google.protobuf.Duration multi_factor_check_lifetime = 11; ];
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 { message UpdateLoginPolicyResponse {

View File

@ -4344,11 +4344,21 @@ message AddCustomLoginPolicyRequest {
bool force_mfa = 4; bool force_mfa = 4;
zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}]; zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}];
bool hide_password_reset = 6; bool hide_password_reset = 6;
google.protobuf.Duration password_check_lifetime = 7; bool ignore_unknown_usernames = 7 [
google.protobuf.Duration external_login_check_lifetime = 8; (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
google.protobuf.Duration mfa_init_skip_lifetime = 9; description: "defines if unknown username on login screen directly return an error or always display the password screen"
google.protobuf.Duration second_factor_check_lifetime = 10; }
google.protobuf.Duration multi_factor_check_lifetime = 11; ];
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 { message AddCustomLoginPolicyResponse {
@ -4362,11 +4372,21 @@ message UpdateCustomLoginPolicyRequest {
bool force_mfa = 4; bool force_mfa = 4;
zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}]; zitadel.policy.v1.PasswordlessType passwordless_type = 5 [(validate.rules).enum = {defined_only: true}];
bool hide_password_reset = 6; bool hide_password_reset = 6;
google.protobuf.Duration password_check_lifetime = 7; bool ignore_unknown_usernames = 7 [
google.protobuf.Duration external_login_check_lifetime = 8; (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
google.protobuf.Duration mfa_init_skip_lifetime = 9; description: "defines if unknown username on login screen directly return an error or always display the password screen"
google.protobuf.Duration second_factor_check_lifetime = 10; }
google.protobuf.Duration multi_factor_check_lifetime = 11; ];
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 { message UpdateCustomLoginPolicyResponse {

View File

@ -149,11 +149,21 @@ message LoginPolicy {
description: "defines if password reset link should be shown in the login screen" description: "defines if password reset link should be shown in the login screen"
} }
]; ];
google.protobuf.Duration password_check_lifetime = 9; bool ignore_unknown_usernames = 9 [
google.protobuf.Duration external_login_check_lifetime = 10; (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
google.protobuf.Duration mfa_init_skip_lifetime = 11; description: "defines if unknown username on login screen directly return an error or always display the password screen"
google.protobuf.Duration second_factor_check_lifetime = 12; }
google.protobuf.Duration multi_factor_check_lifetime = 13; ];
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;
} }