feat: provide option to limit (T)OTP checks (#7693)

* feat: provide option to limit (T)OTP checks

* fix requests in console

* update errors pkg

* cleanup

* cleanup

* improve naming of existing config
This commit is contained in:
Livio Spring 2024-04-10 11:14:55 +02:00 committed by GitHub
parent e3f10f7e23
commit 153df2e12f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 752 additions and 755 deletions

File diff suppressed because one or more lines are too long

View File

@ -17,17 +17,49 @@
<div class="lockout-content"> <div class="lockout-content">
<div class="row"> <div class="row">
<div class="length-wrapper"> <div class="length-wrapper">
<button [disabled]="(['policy.write'] | hasRole | async) === false" mat-icon-button (click)="decrementMaxAttempts()"> <button
[disabled]="(['policy.write'] | hasRole | async) === false"
mat-icon-button
(click)="decrementPasswordMaxAttempts()"
>
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
</button> </button>
<span>{{ lockoutData.maxPasswordAttempts }}</span> <span>{{ lockoutData.maxPasswordAttempts }}</span>
<button [disabled]="(['policy.write'] | hasRole | async) === false" mat-icon-button (click)="incrementMaxAttempts()"> <button
[disabled]="(['policy.write'] | hasRole | async) === false"
mat-icon-button
(click)="incrementPasswordMaxAttempts()"
>
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</div> </div>
<div class="number-toggle-row"> <div class="number-toggle-row">
<span class="left-desc">{{ 'POLICY.DATA.MAXATTEMPTS' | translate }}</span> <span class="left-desc">{{ 'POLICY.DATA.MAXPASSWORDATTEMPTS' | translate }}</span>
<span class="fill-space"></span>
</div>
</div>
<div class="row">
<div class="length-wrapper">
<button
[disabled]="(['policy.write'] | hasRole | async) === false"
mat-icon-button
(click)="decrementOTPMaxAttempts()"
>
<mat-icon>remove</mat-icon>
</button>
<span>{{ lockoutData.maxOtpAttempts }}</span>
<button
[disabled]="(['policy.write'] | hasRole | async) === false"
mat-icon-button
(click)="incrementOTPMaxAttempts()"
>
<mat-icon>add</mat-icon>
</button>
</div>
<div class="number-toggle-row">
<span class="left-desc">{{ 'POLICY.DATA.MAXOTPATTEMPTS' | translate }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
</div> </div>

View File

@ -91,24 +91,36 @@ export class PasswordLockoutPolicyComponent implements OnInit {
} }
} }
public incrementMaxAttempts(): void { public incrementPasswordMaxAttempts(): void {
if (this.lockoutData?.maxPasswordAttempts !== undefined) { if (this.lockoutData?.maxPasswordAttempts !== undefined) {
this.lockoutData.maxPasswordAttempts++; this.lockoutData.maxPasswordAttempts++;
} }
} }
public decrementMaxAttempts(): void { public decrementPasswordMaxAttempts(): void {
if (this.lockoutData?.maxPasswordAttempts && this.lockoutData?.maxPasswordAttempts > 0) { if (this.lockoutData?.maxPasswordAttempts && this.lockoutData?.maxPasswordAttempts > 0) {
this.lockoutData.maxPasswordAttempts--; this.lockoutData.maxPasswordAttempts--;
} }
} }
public incrementOTPMaxAttempts(): void {
if (this.lockoutData?.maxOtpAttempts !== undefined) {
this.lockoutData.maxOtpAttempts++;
}
}
public decrementOTPMaxAttempts(): void {
if (this.lockoutData?.maxOtpAttempts && this.lockoutData?.maxOtpAttempts > 0) {
this.lockoutData.maxOtpAttempts--;
}
}
public savePolicy(): void { public savePolicy(): void {
let promise: Promise<any>; let promise: Promise<any>;
if (this.lockoutData) { if (this.lockoutData) {
if (this.service instanceof AdminService) { if (this.service instanceof AdminService) {
promise = this.service promise = this.service
.updateLockoutPolicy(this.lockoutData.maxPasswordAttempts) .updateLockoutPolicy(this.lockoutData.maxPasswordAttempts, this.lockoutData.maxOtpAttempts)
.then(() => { .then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData(); this.fetchData();
@ -119,7 +131,7 @@ export class PasswordLockoutPolicyComponent implements OnInit {
} else { } else {
if ((this.lockoutData as LockoutPolicy.AsObject).isDefault) { if ((this.lockoutData as LockoutPolicy.AsObject).isDefault) {
promise = (this.service as ManagementService) promise = (this.service as ManagementService)
.addCustomLockoutPolicy(this.lockoutData.maxPasswordAttempts) .addCustomLockoutPolicy(this.lockoutData.maxPasswordAttempts, this.lockoutData.maxOtpAttempts)
.then(() => { .then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData(); this.fetchData();
@ -129,7 +141,7 @@ export class PasswordLockoutPolicyComponent implements OnInit {
}); });
} else { } else {
promise = (this.service as ManagementService) promise = (this.service as ManagementService)
.updateCustomLockoutPolicy(this.lockoutData.maxPasswordAttempts) .updateCustomLockoutPolicy(this.lockoutData.maxPasswordAttempts, this.lockoutData.maxOtpAttempts)
.then(() => { .then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true); this.toast.showInfo('POLICY.TOAST.SET', true);
this.fetchData(); this.fetchData();

View File

@ -957,9 +957,13 @@ export class AdminService {
return this.grpcService.admin.getLockoutPolicy(req, null).then((resp) => resp.toObject()); return this.grpcService.admin.getLockoutPolicy(req, null).then((resp) => resp.toObject());
} }
public updateLockoutPolicy(maxAttempts: number): Promise<UpdateLockoutPolicyResponse.AsObject> { public updateLockoutPolicy(
maxPasswordAttempts: number,
maxOTPAttempts: number,
): Promise<UpdateLockoutPolicyResponse.AsObject> {
const req = new UpdateLockoutPolicyRequest(); const req = new UpdateLockoutPolicyRequest();
req.setMaxPasswordAttempts(maxAttempts); req.setMaxPasswordAttempts(maxPasswordAttempts);
req.setMaxOtpAttempts(maxOTPAttempts);
return this.grpcService.admin.updateLockoutPolicy(req, null).then((resp) => resp.toObject()); return this.grpcService.admin.updateLockoutPolicy(req, null).then((resp) => resp.toObject());
} }

View File

@ -1587,9 +1587,13 @@ export class ManagementService {
return this.grpcService.mgmt.getLockoutPolicy(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.getLockoutPolicy(req, null).then((resp) => resp.toObject());
} }
public addCustomLockoutPolicy(maxAttempts: number): Promise<AddCustomLockoutPolicyResponse.AsObject> { public addCustomLockoutPolicy(
maxPasswordAttempts: number,
maxOTPAttempts: number,
): Promise<AddCustomLockoutPolicyResponse.AsObject> {
const req = new AddCustomLockoutPolicyRequest(); const req = new AddCustomLockoutPolicyRequest();
req.setMaxPasswordAttempts(maxAttempts); req.setMaxPasswordAttempts(maxPasswordAttempts);
req.setMaxOtpAttempts(maxOTPAttempts);
return this.grpcService.mgmt.addCustomLockoutPolicy(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.addCustomLockoutPolicy(req, null).then((resp) => resp.toObject());
} }
@ -1599,9 +1603,13 @@ export class ManagementService {
return this.grpcService.mgmt.resetLockoutPolicyToDefault(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.resetLockoutPolicyToDefault(req, null).then((resp) => resp.toObject());
} }
public updateCustomLockoutPolicy(maxAttempts: number): Promise<UpdateCustomLockoutPolicyResponse.AsObject> { public updateCustomLockoutPolicy(
maxPasswordAttempts: number,
maxOTPAttempts: number,
): Promise<UpdateCustomLockoutPolicyResponse.AsObject> {
const req = new UpdateCustomLockoutPolicyRequest(); const req = new UpdateCustomLockoutPolicyRequest();
req.setMaxPasswordAttempts(maxAttempts); req.setMaxPasswordAttempts(maxPasswordAttempts);
req.setMaxOtpAttempts(maxOTPAttempts);
return this.grpcService.mgmt.updateCustomLockoutPolicy(req, null).then((resp) => resp.toObject()); return this.grpcService.mgmt.updateCustomLockoutPolicy(req, null).then((resp) => resp.toObject());
} }

View File

@ -1653,7 +1653,8 @@
"HASLOWERCASE": "има малки букви", "HASLOWERCASE": "има малки букви",
"HASUPPERCASE": "има главни букви", "HASUPPERCASE": "има главни букви",
"SHOWLOCKOUTFAILURES": "показва грешки при блокиране", "SHOWLOCKOUTFAILURES": "показва грешки при блокиране",
"MAXATTEMPTS": "Максимален брой опити за парола", "MAXPASSWORDATTEMPTS": "Максимален брой опити за парола",
"MAXOTPATTEMPTS": "Максимален брой опити за OTP",
"EXPIREWARNDAYS": "Предупреждение за изтичане след ден", "EXPIREWARNDAYS": "Предупреждение за изтичане след ден",
"MAXAGEDAYS": "Максимална възраст в дни", "MAXAGEDAYS": "Максимална възраст в дни",
"USERLOGINMUSTBEDOMAIN": "Добавяне на домейн на организация като суфикс към имената за вход", "USERLOGINMUSTBEDOMAIN": "Добавяне на домейн на организация като суфикс към имената за вход",

View File

@ -1660,7 +1660,8 @@
"HASLOWERCASE": "obsahuje malá písmena", "HASLOWERCASE": "obsahuje malá písmena",
"HASUPPERCASE": "obsahuje velká písmena", "HASUPPERCASE": "obsahuje velká písmena",
"SHOWLOCKOUTFAILURES": "zobrazit neúspěšné pokusy o uzamčení", "SHOWLOCKOUTFAILURES": "zobrazit neúspěšné pokusy o uzamčení",
"MAXATTEMPTS": "Maximální počet pokusů o heslo", "MAXPASSWORDATTEMPTS": "Maximální počet pokusů o heslo",
"MAXOTPATTEMPTS": "Maximální počet pokusů o OTP",
"EXPIREWARNDAYS": "Upozornění na expiraci po dni", "EXPIREWARNDAYS": "Upozornění na expiraci po dni",
"MAXAGEDAYS": "Maximální stáří v dnech", "MAXAGEDAYS": "Maximální stáří v dnech",
"USERLOGINMUSTBEDOMAIN": "Přidat doménu organizace jako příponu k přihlašovacím jménům", "USERLOGINMUSTBEDOMAIN": "Přidat doménu organizace jako příponu k přihlašovacím jménům",

View File

@ -1659,7 +1659,8 @@
"HASLOWERCASE": "erfordert Kleinbuchstaben", "HASLOWERCASE": "erfordert Kleinbuchstaben",
"HASUPPERCASE": "erfordert Grossbuchstaben", "HASUPPERCASE": "erfordert Grossbuchstaben",
"SHOWLOCKOUTFAILURES": "Zeige Anzahl Anmeldeversuche", "SHOWLOCKOUTFAILURES": "Zeige Anzahl Anmeldeversuche",
"MAXATTEMPTS": "Maximale Anzahl an Versuchen", "MAXPASSWORDATTEMPTS": "Maximale Anzahl an Passwort Versuchen",
"MAXOTPATTEMPTS": "Maximale Anzahl an OTP Versuchen",
"EXPIREWARNDAYS": "Ablauf Warnung nach Tagen", "EXPIREWARNDAYS": "Ablauf Warnung nach Tagen",
"MAXAGEDAYS": "Maximale Gültigkeit in Tagen", "MAXAGEDAYS": "Maximale Gültigkeit in Tagen",
"USERLOGINMUSTBEDOMAIN": "Organisationsdomain dem Loginname hinzufügen", "USERLOGINMUSTBEDOMAIN": "Organisationsdomain dem Loginname hinzufügen",

View File

@ -1660,7 +1660,8 @@
"HASLOWERCASE": "must include a lowercase letter", "HASLOWERCASE": "must include a lowercase letter",
"HASUPPERCASE": "must include an uppercase letter", "HASUPPERCASE": "must include an uppercase letter",
"SHOWLOCKOUTFAILURES": "show lockout failures", "SHOWLOCKOUTFAILURES": "show lockout failures",
"MAXATTEMPTS": "Password maximum Attempts", "MAXPASSWORDATTEMPTS": "Password maximum attempts",
"MAXOTPATTEMPTS": "OTP maximum attempts",
"EXPIREWARNDAYS": "Expiration Warning after day", "EXPIREWARNDAYS": "Expiration Warning after day",
"MAXAGEDAYS": "Max Age in days", "MAXAGEDAYS": "Max Age in days",
"USERLOGINMUSTBEDOMAIN": "Add organization domain as suffix to loginnames", "USERLOGINMUSTBEDOMAIN": "Add organization domain as suffix to loginnames",

View File

@ -1661,7 +1661,8 @@
"HASLOWERCASE": "tiene minúsculas", "HASLOWERCASE": "tiene minúsculas",
"HASUPPERCASE": "tiene mayúsculas", "HASUPPERCASE": "tiene mayúsculas",
"SHOWLOCKOUTFAILURES": "mostrar fallos de bloqueo", "SHOWLOCKOUTFAILURES": "mostrar fallos de bloqueo",
"MAXATTEMPTS": "Intentos máximos", "MAXPASSWORDATTEMPTS": "Intentos máximos de contraseña",
"MAXOTPATTEMPTS": "Intentos máximos de OTP",
"EXPIREWARNDAYS": "Aviso de expiración después de estos días: ", "EXPIREWARNDAYS": "Aviso de expiración después de estos días: ",
"MAXAGEDAYS": "Antigüedad máxima en días", "MAXAGEDAYS": "Antigüedad máxima en días",
"USERLOGINMUSTBEDOMAIN": "Añadir el dominio de la organización como sufijo de los nombres de inicio de sesión", "USERLOGINMUSTBEDOMAIN": "Añadir el dominio de la organización como sufijo de los nombres de inicio de sesión",

View File

@ -1659,7 +1659,8 @@
"HASLOWERCASE": "a minuscule", "HASLOWERCASE": "a minuscule",
"HASUPPERCASE": "a majuscule", "HASUPPERCASE": "a majuscule",
"SHOWLOCKOUTFAILURES": "montrer les échecs de verrouillage", "SHOWLOCKOUTFAILURES": "montrer les échecs de verrouillage",
"MAXATTEMPTS": "Mot de passe maximum Tentatives", "MAXPASSWORDATTEMPTS": "Mot de passe maximum tentatives",
"MAXOTPATTEMPTS": "Maximal de tentatives OTP",
"EXPIREWARNDAYS": "Expiration Avertissement après le jour", "EXPIREWARNDAYS": "Expiration Avertissement après le jour",
"MAXAGEDAYS": "Âge maximum en jours", "MAXAGEDAYS": "Âge maximum en jours",
"USERLOGINMUSTBEDOMAIN": "Le nom de connexion de l'utilisateur doit contenir le nom de domaine de l'organisation", "USERLOGINMUSTBEDOMAIN": "Le nom de connexion de l'utilisateur doit contenir le nom de domaine de l'organisation",

View File

@ -1659,7 +1659,8 @@
"HASLOWERCASE": "ha la minuscola", "HASLOWERCASE": "ha la minuscola",
"HASUPPERCASE": "ha la maiuscola", "HASUPPERCASE": "ha la maiuscola",
"SHOWLOCKOUTFAILURES": "mostra i fallimenti del blocco", "SHOWLOCKOUTFAILURES": "mostra i fallimenti del blocco",
"MAXATTEMPTS": "Massimo numero di tentativi di password", "MAXPASSWORDATTEMPTS": "Massimo numero di tentativi di password",
"MAXOTPATTEMPTS": "Massimo numero di tentativi di OTP",
"EXPIREWARNDAYS": "Avviso scadenza dopo il giorno", "EXPIREWARNDAYS": "Avviso scadenza dopo il giorno",
"MAXAGEDAYS": "Lunghezza massima in giorni", "MAXAGEDAYS": "Lunghezza massima in giorni",
"USERLOGINMUSTBEDOMAIN": "Nome utente deve contenere il dominio dell' organizzazione", "USERLOGINMUSTBEDOMAIN": "Nome utente deve contenere il dominio dell' organizzazione",

View File

@ -1656,7 +1656,8 @@
"HASLOWERCASE": "小文字を含める", "HASLOWERCASE": "小文字を含める",
"HASUPPERCASE": "大文字を含める", "HASUPPERCASE": "大文字を含める",
"SHOWLOCKOUTFAILURES": "ロックアウトの失敗を表示する", "SHOWLOCKOUTFAILURES": "ロックアウトの失敗を表示する",
"MAXATTEMPTS": "パスワードの最大試行", "MAXPASSWORDATTEMPTS": "パスワードの最大試行",
"MAXOTPATTEMPTS": "最大OTP試行回数",
"EXPIREWARNDAYS": "有効期限の翌日以降の警告", "EXPIREWARNDAYS": "有効期限の翌日以降の警告",
"MAXAGEDAYS": "最大有効期限", "MAXAGEDAYS": "最大有効期限",
"USERLOGINMUSTBEDOMAIN": "ログイン名の接尾辞として組織ドメインを追加する", "USERLOGINMUSTBEDOMAIN": "ログイン名の接尾辞として組織ドメインを追加する",

View File

@ -1661,7 +1661,8 @@
"HASLOWERCASE": "има мали букви", "HASLOWERCASE": "има мали букви",
"HASUPPERCASE": "има големи букви", "HASUPPERCASE": "има големи букви",
"SHOWLOCKOUTFAILURES": "прикажи неуспешни заклучувања", "SHOWLOCKOUTFAILURES": "прикажи неуспешни заклучувања",
"MAXATTEMPTS": "Максимален број на обиди за лозинка", "MAXPASSWORDATTEMPTS": "Максимален број на обиди за лозинка",
"MAXOTPATTEMPTS": "Максимални обиди за OTP",
"EXPIREWARNDAYS": "Предупредување за истекување по ден", "EXPIREWARNDAYS": "Предупредување за истекување по ден",
"MAXAGEDAYS": "Максимална возраст во денови", "MAXAGEDAYS": "Максимална возраст во денови",
"USERLOGINMUSTBEDOMAIN": "Додади организациски домен како суфикс на корисничките имиња", "USERLOGINMUSTBEDOMAIN": "Додади организациски домен како суфикс на корисничките имиња",

View File

@ -1660,7 +1660,8 @@
"HASLOWERCASE": "heeft kleine letters", "HASLOWERCASE": "heeft kleine letters",
"HASUPPERCASE": "heeft hoofdletters", "HASUPPERCASE": "heeft hoofdletters",
"SHOWLOCKOUTFAILURES": "toon lockout mislukkingen", "SHOWLOCKOUTFAILURES": "toon lockout mislukkingen",
"MAXATTEMPTS": "Maximum pogingen voor wachtwoord", "MAXPASSWORDATTEMPTS": "Maximum pogingen voor wachtwoord",
"MAXOTPATTEMPTS": "Maximale OTP-pogingen",
"EXPIREWARNDAYS": "Vervaldatum Waarschuwing na dag", "EXPIREWARNDAYS": "Vervaldatum Waarschuwing na dag",
"MAXAGEDAYS": "Maximale Leeftijd in dagen", "MAXAGEDAYS": "Maximale Leeftijd in dagen",
"USERLOGINMUSTBEDOMAIN": "Voeg organisatie domein toe als achtervoegsel aan inlognamen", "USERLOGINMUSTBEDOMAIN": "Voeg organisatie domein toe als achtervoegsel aan inlognamen",

View File

@ -1659,7 +1659,8 @@
"HASLOWERCASE": "zawiera małe litery", "HASLOWERCASE": "zawiera małe litery",
"HASUPPERCASE": "zawiera duże litery", "HASUPPERCASE": "zawiera duże litery",
"SHOWLOCKOUTFAILURES": "pokaż blokady nieudanych prób", "SHOWLOCKOUTFAILURES": "pokaż blokady nieudanych prób",
"MAXATTEMPTS": "Maksymalna liczba prób wprowadzenia hasła", "MAXPASSWORDATTEMPTS": "Maksymalna liczba prób wprowadzenia hasła",
"MAXOTPATTEMPTS": "Maksymalna liczba prób OTP",
"EXPIREWARNDAYS": "Ostrzeżenie o wygaśnięciu po dniu", "EXPIREWARNDAYS": "Ostrzeżenie o wygaśnięciu po dniu",
"MAXAGEDAYS": "Maksymalny wiek w dniach", "MAXAGEDAYS": "Maksymalny wiek w dniach",
"USERLOGINMUSTBEDOMAIN": "Dodaj domenę organizacji jako przyrostek do nazw logowania", "USERLOGINMUSTBEDOMAIN": "Dodaj domenę organizacji jako przyrostek do nazw logowania",

View File

@ -1661,7 +1661,8 @@
"HASLOWERCASE": "tem letra minúscula", "HASLOWERCASE": "tem letra minúscula",
"HASUPPERCASE": "tem letra maiúscula", "HASUPPERCASE": "tem letra maiúscula",
"SHOWLOCKOUTFAILURES": "mostrar falhas de bloqueio", "SHOWLOCKOUTFAILURES": "mostrar falhas de bloqueio",
"MAXATTEMPTS": "Máximo de tentativas de senha", "MAXPASSWORDATTEMPTS": "Máximo de tentativas de senha",
"MAXOTPATTEMPTS": "Máximo de tentativas de OTP",
"EXPIREWARNDAYS": "Aviso de expiração após dias", "EXPIREWARNDAYS": "Aviso de expiração após dias",
"MAXAGEDAYS": "Idade máxima em dias", "MAXAGEDAYS": "Idade máxima em dias",
"USERLOGINMUSTBEDOMAIN": "Adicionar domínio da organização como sufixo aos nomes de login", "USERLOGINMUSTBEDOMAIN": "Adicionar domínio da organização como sufixo aos nomes de login",

View File

@ -1729,7 +1729,8 @@
"HASLOWERCASE": "Содержит нижний регистр", "HASLOWERCASE": "Содержит нижний регистр",
"HASUPPERCASE": "Содержит верхний регистр", "HASUPPERCASE": "Содержит верхний регистр",
"SHOWLOCKOUTFAILURES": "Показать ошибки блокировки", "SHOWLOCKOUTFAILURES": "Показать ошибки блокировки",
"MAXATTEMPTS": "Максимальное количество попыток пароля", "MAXPASSWORDATTEMPTS": "Максимальное количество попыток пароля",
"MAXOTPATTEMPTS": "Максимальное количество попыток OTP",
"EXPIREWARNDAYS": "Предупреждение об истечении срока действия после дня", "EXPIREWARNDAYS": "Предупреждение об истечении срока действия после дня",
"MAXAGEDAYS": "Максимальный возраст в днях", "MAXAGEDAYS": "Максимальный возраст в днях",
"USERLOGINMUSTBEDOMAIN": "Добавить домен организации в качестве суффикса к именам логина", "USERLOGINMUSTBEDOMAIN": "Добавить домен организации в качестве суффикса к именам логина",

View File

@ -1658,7 +1658,8 @@
"HASLOWERCASE": "包含小写字母", "HASLOWERCASE": "包含小写字母",
"HASUPPERCASE": "包含大写字母", "HASUPPERCASE": "包含大写字母",
"SHOWLOCKOUTFAILURES": "显示锁定失败", "SHOWLOCKOUTFAILURES": "显示锁定失败",
"MAXATTEMPTS": "密码最大尝试次数", "MAXPASSWORDATTEMPTS": "密码最大尝试次数",
"MAXOTPATTEMPTS": "最多尝试 OTP 次数",
"EXPIREWARNDAYS": "密码过期警告", "EXPIREWARNDAYS": "密码过期警告",
"MAXAGEDAYS": "Max Age in days", "MAXAGEDAYS": "Max Age in days",
"USERLOGINMUSTBEDOMAIN": "用户名必须包含组织域名", "USERLOGINMUSTBEDOMAIN": "用户名必须包含组织域名",

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,7 @@ When you configure your default settings, you can set the following:
- [**Login Behavior and Access**](#login-behavior-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface. - [**Login Behavior and Access**](#login-behavior-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface.
- [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations - [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations
- [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more. - [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more.
- [**Lockout**](#lockout): Set the maximum attempts a user can try to enter the password. When the number is exceeded, the user gets locked out and has to be unlocked. - [**Lockout**](#lockout): Set the maximum attempts a user can try to enter the password or any (T)OTP method. When the number is exceeded, the user gets locked out and has to be unlocked.
- [**Domain settings**](#domain-settings): Whether users use their email or the generated username to login. Other Validation, SMTP settings - [**Domain settings**](#domain-settings): Whether users use their email or the generated username to login. Other Validation, SMTP settings
- [**Branding**](#branding): Appearance of the login interface. - [**Branding**](#branding): Appearance of the login interface.
- [**Message Texts**](#message-texts): Text and internationalization for emails - [**Message Texts**](#message-texts): Text and internationalization for emails
@ -189,6 +189,7 @@ Define when an account should be locked.
The following settings are available: The following settings are available:
- Maximum Password Attempts: When the user has reached the maximum password attempts the account will be locked, If this is set to 0 the lockout will not trigger. - Maximum Password Attempts: When the user has reached the maximum password attempts the account will be locked, If this is set to 0 the lockout will not trigger.
- Maximum OTP Attempts: When the user has reached the maximum (T)OTP attempts the account will be locked, If this is set to 0 the lockout will not trigger.
If an account is locked, the administrator has to unlock it in the ZITADEL console If an account is locked, the administrator has to unlock it in the ZITADEL console

View File

@ -108,7 +108,7 @@ Those settings are the same as on your instance.
- [**Login Behavior and Access**](./default-settings#login-behaviour-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface. - [**Login Behavior and Access**](./default-settings#login-behaviour-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface.
- [**Identity Providers**](./default-settings#identity-providers): Define IDPs which are available for all organizations - [**Identity Providers**](./default-settings#identity-providers): Define IDPs which are available for all organizations
- [**Password Complexity**](./default-settings#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more. - [**Password Complexity**](./default-settings#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more.
- [**Lockout**](./default-settings#lockout): Set the maximum attempts a user can try to enter the password. When the number is exceeded, the user gets locked out and has to be unlocked. - [**Lockout**](./default-settings#lockout): Set the maximum attempts a user can try to enter the password or any (T)OTP method. When the number is exceeded, the user gets locked out and has to be unlocked.
- [**Verified domains**](/docs/guides/manage/console/organizations#verify-your-domain-name): This is where you manage your organization specific domains which can be used to build usernames - [**Verified domains**](/docs/guides/manage/console/organizations#verify-your-domain-name): This is where you manage your organization specific domains which can be used to build usernames
- [**Domain settings**](./default-settings#domain-settings): Whether users use their email or the generated username to login. Other Validation, SMTP settings - [**Domain settings**](./default-settings#domain-settings): Whether users use their email or the generated username to login. Other Validation, SMTP settings
- [**Branding**](./default-settings#branding): Appearance of the login interface. - [**Branding**](./default-settings#branding): Appearance of the login interface.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -491,13 +491,14 @@ func (s *Server) getLockoutPolicy(ctx context.Context, orgID string) (_ *managem
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
queriedLockout, err := s.query.LockoutPolicyByOrg(ctx, false, orgID, false) queriedLockout, err := s.query.LockoutPolicyByOrg(ctx, false, orgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !queriedLockout.IsDefault { if !queriedLockout.IsDefault {
return &management_pb.AddCustomLockoutPolicyRequest{ return &management_pb.AddCustomLockoutPolicyRequest{
MaxPasswordAttempts: uint32(queriedLockout.MaxPasswordAttempts), MaxPasswordAttempts: uint32(queriedLockout.MaxPasswordAttempts),
MaxOtpAttempts: uint32(queriedLockout.MaxOTPAttempts),
}, nil }, nil
} }
return nil, nil return nil, nil

View File

@ -8,5 +8,6 @@ import (
func UpdateLockoutPolicyToDomain(p *admin.UpdateLockoutPolicyRequest) *domain.LockoutPolicy { func UpdateLockoutPolicyToDomain(p *admin.UpdateLockoutPolicyRequest) *domain.LockoutPolicy {
return &domain.LockoutPolicy{ return &domain.LockoutPolicy{
MaxPasswordAttempts: uint64(p.MaxPasswordAttempts), MaxPasswordAttempts: uint64(p.MaxPasswordAttempts),
MaxOTPAttempts: uint64(p.MaxOtpAttempts),
} }
} }

View File

@ -10,7 +10,7 @@ import (
) )
func (s *Server) GetLockoutPolicy(ctx context.Context, req *mgmt_pb.GetLockoutPolicyRequest) (*mgmt_pb.GetLockoutPolicyResponse, error) { func (s *Server) GetLockoutPolicy(ctx context.Context, req *mgmt_pb.GetLockoutPolicyRequest) (*mgmt_pb.GetLockoutPolicyResponse, error) {
policy, err := s.query.LockoutPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false) policy, err := s.query.LockoutPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,11 +8,13 @@ import (
func AddLockoutPolicyToDomain(p *mgmt.AddCustomLockoutPolicyRequest) *domain.LockoutPolicy { func AddLockoutPolicyToDomain(p *mgmt.AddCustomLockoutPolicyRequest) *domain.LockoutPolicy {
return &domain.LockoutPolicy{ return &domain.LockoutPolicy{
MaxPasswordAttempts: uint64(p.MaxPasswordAttempts), MaxPasswordAttempts: uint64(p.MaxPasswordAttempts),
MaxOTPAttempts: uint64(p.MaxOtpAttempts),
} }
} }
func UpdateLockoutPolicyToDomain(p *mgmt.UpdateCustomLockoutPolicyRequest) *domain.LockoutPolicy { func UpdateLockoutPolicyToDomain(p *mgmt.UpdateCustomLockoutPolicyRequest) *domain.LockoutPolicy {
return &domain.LockoutPolicy{ return &domain.LockoutPolicy{
MaxPasswordAttempts: uint64(p.MaxPasswordAttempts), MaxPasswordAttempts: uint64(p.MaxPasswordAttempts),
MaxOTPAttempts: uint64(p.MaxOtpAttempts),
} }
} }

View File

@ -10,6 +10,7 @@ func ModelLockoutPolicyToPb(policy *query.LockoutPolicy) *policy_pb.LockoutPolic
return &policy_pb.LockoutPolicy{ return &policy_pb.LockoutPolicy{
IsDefault: policy.IsDefault, IsDefault: policy.IsDefault,
MaxPasswordAttempts: policy.MaxPasswordAttempts, MaxPasswordAttempts: policy.MaxPasswordAttempts,
MaxOtpAttempts: policy.MaxOTPAttempts,
Details: object.ToViewDetailsPb( Details: object.ToViewDetailsPb(
policy.Sequence, policy.Sequence,
policy.CreationDate, policy.CreationDate,

View File

@ -90,7 +90,7 @@ func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.G
} }
func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) { func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) {
current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -160,6 +160,7 @@ func legalAndSupportSettingsToPb(current *query.PrivacyPolicy) *settings.LegalAn
func lockoutSettingsToPb(current *query.LockoutPolicy) *settings.LockoutSettings { func lockoutSettingsToPb(current *query.LockoutPolicy) *settings.LockoutSettings {
return &settings.LockoutSettings{ return &settings.LockoutSettings{
MaxPasswordAttempts: current.MaxPasswordAttempts, MaxPasswordAttempts: current.MaxPasswordAttempts,
MaxOtpAttempts: current.MaxOTPAttempts,
ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault), ResourceOwnerType: isDefaultToResourceOwnerTypePb(current.IsDefault),
} }
} }

View File

@ -339,10 +339,12 @@ func Test_legalSettingsToPb(t *testing.T) {
func Test_lockoutSettingsToPb(t *testing.T) { func Test_lockoutSettingsToPb(t *testing.T) {
arg := &query.LockoutPolicy{ arg := &query.LockoutPolicy{
MaxPasswordAttempts: 22, MaxPasswordAttempts: 22,
MaxOTPAttempts: 22,
IsDefault: true, IsDefault: true,
} }
want := &settings.LockoutSettings{ want := &settings.LockoutSettings{
MaxPasswordAttempts: 22, MaxPasswordAttempts: 22,
MaxOtpAttempts: 22,
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE, ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
} }
got := lockoutSettingsToPb(arg) got := lockoutSettingsToPb(arg)

View File

@ -75,7 +75,7 @@ type loginPolicyViewProvider interface {
} }
type lockoutPolicyViewProvider interface { type lockoutPolicyViewProvider interface {
LockoutPolicyByOrg(context.Context, bool, string, bool) (*query.LockoutPolicy, error) LockoutPolicyByOrg(context.Context, bool, string) (*query.LockoutPolicy, error)
} }
type idpProviderViewProvider interface { type idpProviderViewProvider interface {
@ -366,6 +366,7 @@ func lockoutPolicyToDomain(policy *query.LockoutPolicy) *domain.LockoutPolicy {
}, },
Default: policy.IsDefault, Default: policy.IsDefault,
MaxPasswordAttempts: policy.MaxPasswordAttempts, MaxPasswordAttempts: policy.MaxPasswordAttempts,
MaxOTPAttempts: policy.MaxOTPAttempts,
ShowLockOutFailures: policy.ShowFailures, ShowLockOutFailures: policy.ShowFailures,
} }
} }
@ -1281,7 +1282,7 @@ func privacyPolicyToDomain(p *query.PrivacyPolicy) *domain.PrivacyPolicy {
} }
func (repo *AuthRequestRepo) getLockoutPolicy(ctx context.Context, orgID string) (*query.LockoutPolicy, error) { func (repo *AuthRequestRepo) getLockoutPolicy(ctx context.Context, orgID string) (*query.LockoutPolicy, error) {
policy, err := repo.LockoutPolicyViewProvider.LockoutPolicyByOrg(ctx, false, orgID, false) policy, err := repo.LockoutPolicyViewProvider.LockoutPolicyByOrg(ctx, false, orgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -184,7 +184,7 @@ type mockLockoutPolicy struct {
policy *query.LockoutPolicy policy *query.LockoutPolicy
} }
func (m *mockLockoutPolicy) LockoutPolicyByOrg(context.Context, bool, string, bool) (*query.LockoutPolicy, error) { func (m *mockLockoutPolicy) LockoutPolicyByOrg(context.Context, bool, string) (*query.LockoutPolicy, error) {
return m.policy, nil return m.policy, nil
} }

View File

@ -101,7 +101,8 @@ type InstanceSetup struct {
ThemeMode domain.LabelPolicyThemeMode ThemeMode domain.LabelPolicyThemeMode
} }
LockoutPolicy struct { LockoutPolicy struct {
MaxAttempts uint64 MaxPasswordAttempts uint64
MaxOTPAttempts uint64
ShouldShowLockoutFailure bool ShouldShowLockoutFailure bool
} }
EmailTemplate []byte EmailTemplate []byte
@ -271,7 +272,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink, setup.PrivacyPolicy.SupportEmail), prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink, setup.PrivacyPolicy.SupportEmail),
prepareAddDefaultNotificationPolicy(instanceAgg, setup.NotificationPolicy.PasswordChange), prepareAddDefaultNotificationPolicy(instanceAgg, setup.NotificationPolicy.PasswordChange),
prepareAddDefaultLockoutPolicy(instanceAgg, setup.LockoutPolicy.MaxAttempts, setup.LockoutPolicy.ShouldShowLockoutFailure), prepareAddDefaultLockoutPolicy(instanceAgg, setup.LockoutPolicy.MaxPasswordAttempts, setup.LockoutPolicy.MaxOTPAttempts, setup.LockoutPolicy.ShouldShowLockoutFailure),
prepareAddDefaultLabelPolicy( prepareAddDefaultLabelPolicy(
instanceAgg, instanceAgg,

View File

@ -109,6 +109,7 @@ func writeModelToLockoutPolicy(wm *LockoutPolicyWriteModel) *domain.LockoutPolic
return &domain.LockoutPolicy{ return &domain.LockoutPolicy{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel), ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
MaxPasswordAttempts: wm.MaxPasswordAttempts, MaxPasswordAttempts: wm.MaxPasswordAttempts,
MaxOTPAttempts: wm.MaxOTPAttempts,
ShowLockOutFailures: wm.ShowLockOutFailures, ShowLockOutFailures: wm.ShowLockOutFailures,
} }
} }

View File

@ -12,9 +12,15 @@ import (
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
) )
func (c *Commands) AddDefaultLockoutPolicy(ctx context.Context, maxAttempts uint64, showLockoutFailure bool) (*domain.ObjectDetails, error) { func (c *Commands) AddDefaultLockoutPolicy(ctx context.Context, maxPasswordAttempts, maxOTPAttempts uint64, showLockoutFailure bool) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultLockoutPolicy(instanceAgg, maxAttempts, showLockoutFailure)) //nolint:staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultLockoutPolicy(
instanceAgg,
maxPasswordAttempts,
maxOTPAttempts,
showLockoutFailure,
))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -35,7 +41,13 @@ func (c *Commands) ChangeDefaultLockoutPolicy(ctx context.Context, policy *domai
} }
instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LockoutPolicyWriteModel.WriteModel) instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LockoutPolicyWriteModel.WriteModel)
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, instanceAgg, policy.MaxPasswordAttempts, policy.ShowLockOutFailures) changedEvent, hasChanged := existingPolicy.NewChangedEvent(
ctx,
instanceAgg,
policy.MaxPasswordAttempts,
policy.MaxOTPAttempts,
policy.ShowLockOutFailures,
)
if !hasChanged { if !hasChanged {
return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-0psjF", "Errors.IAM.LockoutPolicy.NotChanged") return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-0psjF", "Errors.IAM.LockoutPolicy.NotChanged")
} }
@ -65,7 +77,8 @@ func (c *Commands) defaultLockoutPolicyWriteModelByID(ctx context.Context) (poli
func prepareAddDefaultLockoutPolicy( func prepareAddDefaultLockoutPolicy(
a *instance.Aggregate, a *instance.Aggregate,
maxAttempts uint64, maxPasswordAttempts,
maxOTPAttempts uint64,
showLockoutFailure bool, showLockoutFailure bool,
) preparation.Validation { ) preparation.Validation {
return func() (preparation.CreateCommands, error) { return func() (preparation.CreateCommands, error) {
@ -83,7 +96,7 @@ func prepareAddDefaultLockoutPolicy(
return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-0olDf", "Errors.Instance.LockoutPolicy.AlreadyExists") return nil, zerrors.ThrowAlreadyExists(nil, "INSTANCE-0olDf", "Errors.Instance.LockoutPolicy.AlreadyExists")
} }
return []eventstore.Command{ return []eventstore.Command{
instance.NewLockoutPolicyAddedEvent(ctx, &a.Aggregate, maxAttempts, showLockoutFailure), instance.NewLockoutPolicyAddedEvent(ctx, &a.Aggregate, maxPasswordAttempts, maxOTPAttempts, showLockoutFailure),
}, nil }, nil
}, nil }, nil
} }

View File

@ -54,11 +54,15 @@ func (wm *InstanceLockoutPolicyWriteModel) Query() *eventstore.SearchQueryBuilde
func (wm *InstanceLockoutPolicyWriteModel) NewChangedEvent( func (wm *InstanceLockoutPolicyWriteModel) NewChangedEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
maxAttempts uint64, maxPasswordAttempts,
maxOTPAttempts uint64,
showLockoutFailure bool) (*instance.LockoutPolicyChangedEvent, bool) { showLockoutFailure bool) (*instance.LockoutPolicyChangedEvent, bool) {
changes := make([]policy.LockoutPolicyChanges, 0) changes := make([]policy.LockoutPolicyChanges, 0)
if wm.MaxPasswordAttempts != maxAttempts { if wm.MaxPasswordAttempts != maxPasswordAttempts {
changes = append(changes, policy.ChangeMaxAttempts(maxAttempts)) changes = append(changes, policy.ChangeMaxPasswordAttempts(maxPasswordAttempts))
}
if wm.MaxOTPAttempts != maxOTPAttempts {
changes = append(changes, policy.ChangeMaxOTPAttempts(maxOTPAttempts))
} }
if wm.ShowLockOutFailures != showLockoutFailure { if wm.ShowLockOutFailures != showLockoutFailure {
changes = append(changes, policy.ChangeShowLockOutFailures(showLockoutFailure)) changes = append(changes, policy.ChangeShowLockOutFailures(showLockoutFailure))

View File

@ -22,6 +22,7 @@ func TestCommandSide_AddDefaultLockoutPolicy(t *testing.T) {
type args struct { type args struct {
ctx context.Context ctx context.Context
maxPasswordAttempts uint64 maxPasswordAttempts uint64
maxOTPAttempts uint64
showLockOutFailures bool showLockOutFailures bool
} }
type res struct { type res struct {
@ -44,6 +45,7 @@ func TestCommandSide_AddDefaultLockoutPolicy(t *testing.T) {
instance.NewLockoutPolicyAddedEvent(context.Background(), instance.NewLockoutPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
@ -69,6 +71,7 @@ func TestCommandSide_AddDefaultLockoutPolicy(t *testing.T) {
instance.NewLockoutPolicyAddedEvent(context.Background(), instance.NewLockoutPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
@ -77,6 +80,7 @@ func TestCommandSide_AddDefaultLockoutPolicy(t *testing.T) {
args: args{ args: args{
ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), ctx: authz.WithInstanceID(context.Background(), "INSTANCE"),
maxPasswordAttempts: 10, maxPasswordAttempts: 10,
maxOTPAttempts: 10,
showLockOutFailures: true, showLockOutFailures: true,
}, },
res: res{ res: res{
@ -91,7 +95,7 @@ func TestCommandSide_AddDefaultLockoutPolicy(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, eventstore: tt.fields.eventstore,
} }
got, err := r.AddDefaultLockoutPolicy(tt.args.ctx, tt.args.maxPasswordAttempts, tt.args.showLockOutFailures) got, err := r.AddDefaultLockoutPolicy(tt.args.ctx, tt.args.maxPasswordAttempts, tt.args.maxOTPAttempts, tt.args.showLockOutFailures)
if tt.res.err == nil { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -135,6 +139,7 @@ func TestCommandSide_ChangeDefaultLockoutPolicy(t *testing.T) {
ctx: context.Background(), ctx: context.Background(),
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -152,6 +157,7 @@ func TestCommandSide_ChangeDefaultLockoutPolicy(t *testing.T) {
instance.NewLockoutPolicyAddedEvent(context.Background(), instance.NewLockoutPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
@ -162,6 +168,7 @@ func TestCommandSide_ChangeDefaultLockoutPolicy(t *testing.T) {
ctx: context.Background(), ctx: context.Background(),
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -179,12 +186,13 @@ func TestCommandSide_ChangeDefaultLockoutPolicy(t *testing.T) {
instance.NewLockoutPolicyAddedEvent(context.Background(), instance.NewLockoutPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
), ),
expectPush( expectPush(
newDefaultLockoutPolicyChangedEvent(context.Background(), 20, false), newDefaultLockoutPolicyChangedEvent(context.Background(), 20, 20, false),
), ),
), ),
}, },
@ -192,6 +200,7 @@ func TestCommandSide_ChangeDefaultLockoutPolicy(t *testing.T) {
ctx: context.Background(), ctx: context.Background(),
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 20, MaxPasswordAttempts: 20,
MaxOTPAttempts: 20,
ShowLockOutFailures: false, ShowLockOutFailures: false,
}, },
}, },
@ -203,6 +212,7 @@ func TestCommandSide_ChangeDefaultLockoutPolicy(t *testing.T) {
InstanceID: "INSTANCE", InstanceID: "INSTANCE",
}, },
MaxPasswordAttempts: 20, MaxPasswordAttempts: 20,
MaxOTPAttempts: 20,
ShowLockOutFailures: false, ShowLockOutFailures: false,
}, },
}, },
@ -227,11 +237,12 @@ func TestCommandSide_ChangeDefaultLockoutPolicy(t *testing.T) {
} }
} }
func newDefaultLockoutPolicyChangedEvent(ctx context.Context, maxAttempts uint64, showLockoutFailure bool) *instance.LockoutPolicyChangedEvent { func newDefaultLockoutPolicyChangedEvent(ctx context.Context, maxPasswordAttempts, maxOTPAttempts uint64, showLockoutFailure bool) *instance.LockoutPolicyChangedEvent {
event, _ := instance.NewLockoutPolicyChangedEvent(ctx, event, _ := instance.NewLockoutPolicyChangedEvent(ctx,
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
[]policy.LockoutPolicyChanges{ []policy.LockoutPolicyChanges{
policy.ChangeMaxAttempts(maxAttempts), policy.ChangeMaxPasswordAttempts(maxPasswordAttempts),
policy.ChangeMaxOTPAttempts(maxOTPAttempts),
policy.ChangeShowLockOutFailures(showLockoutFailure), policy.ChangeShowLockOutFailures(showLockoutFailure),
}, },
) )

View File

@ -21,7 +21,13 @@ func (c *Commands) AddLockoutPolicy(ctx context.Context, resourceOwner string, p
} }
orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel) orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewLockoutPolicyAddedEvent(ctx, orgAgg, policy.MaxPasswordAttempts, policy.ShowLockOutFailures)) pushedEvents, err := c.eventstore.Push(ctx, org.NewLockoutPolicyAddedEvent(
ctx,
orgAgg,
policy.MaxPasswordAttempts,
policy.MaxOTPAttempts,
policy.ShowLockOutFailures,
))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -45,7 +51,7 @@ func (c *Commands) ChangeLockoutPolicy(ctx context.Context, resourceOwner string
} }
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LockoutPolicyWriteModel.WriteModel) orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LockoutPolicyWriteModel.WriteModel)
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.MaxPasswordAttempts, policy.ShowLockOutFailures) changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, orgAgg, policy.MaxPasswordAttempts, policy.MaxOTPAttempts, policy.ShowLockOutFailures)
if !hasChanged { if !hasChanged {
return nil, zerrors.ThrowPreconditionFailed(nil, "ORG-0JFSr", "Errors.Org.LockoutPolicy.NotChanged") return nil, zerrors.ThrowPreconditionFailed(nil, "ORG-0JFSr", "Errors.Org.LockoutPolicy.NotChanged")
} }
@ -106,3 +112,20 @@ func (c *Commands) orgLockoutPolicyWriteModelByID(ctx context.Context, orgID str
} }
return policy, nil return policy, nil
} }
func (c *Commands) getLockoutPolicy(ctx context.Context, orgID string) (*domain.LockoutPolicy, error) {
orgWm, err := c.orgLockoutPolicyWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
}
if orgWm.State == domain.PolicyStateActive {
return writeModelToLockoutPolicy(&orgWm.LockoutPolicyWriteModel), nil
}
instanceWm, err := c.defaultLockoutPolicyWriteModelByID(ctx)
if err != nil {
return nil, err
}
policy := writeModelToLockoutPolicy(&instanceWm.LockoutPolicyWriteModel)
policy.Default = true
return policy, nil
}

View File

@ -55,11 +55,15 @@ func (wm *OrgLockoutPolicyWriteModel) Query() *eventstore.SearchQueryBuilder {
func (wm *OrgLockoutPolicyWriteModel) NewChangedEvent( func (wm *OrgLockoutPolicyWriteModel) NewChangedEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
maxAttempts uint64, maxPasswordAttempts,
maxOTPAttempts uint64,
showLockoutFailure bool) (*org.LockoutPolicyChangedEvent, bool) { showLockoutFailure bool) (*org.LockoutPolicyChangedEvent, bool) {
changes := make([]policy.LockoutPolicyChanges, 0) changes := make([]policy.LockoutPolicyChanges, 0)
if wm.MaxPasswordAttempts != maxAttempts { if wm.MaxPasswordAttempts != maxPasswordAttempts {
changes = append(changes, policy.ChangeMaxAttempts(maxAttempts)) changes = append(changes, policy.ChangeMaxPasswordAttempts(maxPasswordAttempts))
}
if wm.MaxOTPAttempts != maxOTPAttempts {
changes = append(changes, policy.ChangeMaxOTPAttempts(maxOTPAttempts))
} }
if wm.ShowLockOutFailures != showLockoutFailure { if wm.ShowLockOutFailures != showLockoutFailure {
changes = append(changes, policy.ChangeShowLockOutFailures(showLockoutFailure)) changes = append(changes, policy.ChangeShowLockOutFailures(showLockoutFailure))

View File

@ -44,6 +44,7 @@ func TestCommandSide_AddPasswordLockoutPolicy(t *testing.T) {
ctx: context.Background(), ctx: context.Background(),
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -61,6 +62,7 @@ func TestCommandSide_AddPasswordLockoutPolicy(t *testing.T) {
org.NewLockoutPolicyAddedEvent(context.Background(), org.NewLockoutPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate, &org.NewAggregate("org1").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
@ -72,6 +74,7 @@ func TestCommandSide_AddPasswordLockoutPolicy(t *testing.T) {
orgID: "org1", orgID: "org1",
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -89,6 +92,7 @@ func TestCommandSide_AddPasswordLockoutPolicy(t *testing.T) {
org.NewLockoutPolicyAddedEvent(context.Background(), org.NewLockoutPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate, &org.NewAggregate("org1").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
@ -99,6 +103,7 @@ func TestCommandSide_AddPasswordLockoutPolicy(t *testing.T) {
orgID: "org1", orgID: "org1",
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -109,6 +114,7 @@ func TestCommandSide_AddPasswordLockoutPolicy(t *testing.T) {
ResourceOwner: "org1", ResourceOwner: "org1",
}, },
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -163,6 +169,7 @@ func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) {
ctx: context.Background(), ctx: context.Background(),
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -183,6 +190,7 @@ func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) {
orgID: "org1", orgID: "org1",
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -200,6 +208,7 @@ func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) {
org.NewLockoutPolicyAddedEvent(context.Background(), org.NewLockoutPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate, &org.NewAggregate("org1").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
@ -211,6 +220,7 @@ func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) {
orgID: "org1", orgID: "org1",
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 10, MaxPasswordAttempts: 10,
MaxOTPAttempts: 10,
ShowLockOutFailures: true, ShowLockOutFailures: true,
}, },
}, },
@ -228,12 +238,13 @@ func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) {
org.NewLockoutPolicyAddedEvent(context.Background(), org.NewLockoutPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate, &org.NewAggregate("org1").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
), ),
expectPush( expectPush(
newPasswordLockoutPolicyChangedEvent(context.Background(), "org1", 5, false), newPasswordLockoutPolicyChangedEvent(context.Background(), "org1", 5, 5, false),
), ),
), ),
}, },
@ -242,6 +253,7 @@ func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) {
orgID: "org1", orgID: "org1",
policy: &domain.LockoutPolicy{ policy: &domain.LockoutPolicy{
MaxPasswordAttempts: 5, MaxPasswordAttempts: 5,
MaxOTPAttempts: 5,
ShowLockOutFailures: false, ShowLockOutFailures: false,
}, },
}, },
@ -252,6 +264,7 @@ func TestCommandSide_ChangePasswordLockoutPolicy(t *testing.T) {
ResourceOwner: "org1", ResourceOwner: "org1",
}, },
MaxPasswordAttempts: 5, MaxPasswordAttempts: 5,
MaxOTPAttempts: 5,
ShowLockOutFailures: false, ShowLockOutFailures: false,
}, },
}, },
@ -334,6 +347,7 @@ func TestCommandSide_RemovePasswordLockoutPolicy(t *testing.T) {
org.NewLockoutPolicyAddedEvent(context.Background(), org.NewLockoutPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate, &org.NewAggregate("org1").Aggregate,
10, 10,
10,
true, true,
), ),
), ),
@ -371,11 +385,12 @@ func TestCommandSide_RemovePasswordLockoutPolicy(t *testing.T) {
} }
} }
func newPasswordLockoutPolicyChangedEvent(ctx context.Context, orgID string, maxAttempts uint64, showLockoutFailure bool) *org.LockoutPolicyChangedEvent { func newPasswordLockoutPolicyChangedEvent(ctx context.Context, orgID string, maxPasswordAttempts, maxOTPAttempts uint64, showLockoutFailure bool) *org.LockoutPolicyChangedEvent {
event, _ := org.NewLockoutPolicyChangedEvent(ctx, event, _ := org.NewLockoutPolicyChangedEvent(ctx,
&org.NewAggregate(orgID).Aggregate, &org.NewAggregate(orgID).Aggregate,
[]policy.LockoutPolicyChanges{ []policy.LockoutPolicyChanges{
policy.ChangeMaxAttempts(maxAttempts), policy.ChangeMaxPasswordAttempts(maxPasswordAttempts),
policy.ChangeMaxOTPAttempts(maxOTPAttempts),
policy.ChangeShowLockOutFailures(showLockoutFailure), policy.ChangeShowLockOutFailures(showLockoutFailure),
}, },
) )

View File

@ -10,6 +10,7 @@ type LockoutPolicyWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
MaxPasswordAttempts uint64 MaxPasswordAttempts uint64
MaxOTPAttempts uint64
ShowLockOutFailures bool ShowLockOutFailures bool
State domain.PolicyState State domain.PolicyState
} }
@ -19,12 +20,16 @@ func (wm *LockoutPolicyWriteModel) Reduce() error {
switch e := event.(type) { switch e := event.(type) {
case *policy.LockoutPolicyAddedEvent: case *policy.LockoutPolicyAddedEvent:
wm.MaxPasswordAttempts = e.MaxPasswordAttempts wm.MaxPasswordAttempts = e.MaxPasswordAttempts
wm.MaxOTPAttempts = e.MaxOTPAttempts
wm.ShowLockOutFailures = e.ShowLockOutFailures wm.ShowLockOutFailures = e.ShowLockOutFailures
wm.State = domain.PolicyStateActive wm.State = domain.PolicyStateActive
case *policy.LockoutPolicyChangedEvent: case *policy.LockoutPolicyChangedEvent:
if e.MaxPasswordAttempts != nil { if e.MaxPasswordAttempts != nil {
wm.MaxPasswordAttempts = *e.MaxPasswordAttempts wm.MaxPasswordAttempts = *e.MaxPasswordAttempts
} }
if e.MaxOTPAttempts != nil {
wm.MaxOTPAttempts = *e.MaxOTPAttempts
}
if e.ShowLockOutFailures != nil { if e.ShowLockOutFailures != nil {
wm.ShowLockOutFailures = *e.ShowLockOutFailures wm.ShowLockOutFailures = *e.ShowLockOutFailures
} }

View File

@ -158,14 +158,37 @@ func (c *Commands) HumanCheckMFATOTP(ctx context.Context, userID, code, resource
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady") return zerrors.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady")
} }
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel) userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
err = domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA) verifyErr := domain.VerifyTOTP(code, existingOTP.Secret, c.multifactors.OTP.CryptoMFA)
if err == nil {
// recheck for additional events (failed OTP checks or locks)
recheckErr := c.eventstore.FilterToQueryReducer(ctx, existingOTP)
if recheckErr != nil {
return recheckErr
}
if existingOTP.UserLocked {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SF3fg", "Errors.User.Locked")
}
// the OTP check succeeded and the user was not locked in the meantime
if verifyErr == nil {
_, err = c.eventstore.Push(ctx, user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest))) _, err = c.eventstore.Push(ctx, user.NewHumanOTPCheckSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
return err return err
} }
_, pushErr := c.eventstore.Push(ctx, user.NewHumanOTPCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
// the OTP check failed, therefore check if the limit was reached and the user must additionally be locked
commands := make([]eventstore.Command, 0, 2)
commands = append(commands, user.NewHumanOTPCheckFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
lockoutPolicy, err := c.getLockoutPolicy(ctx, resourceOwner)
if err != nil {
return err
}
if lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount+1 >= lockoutPolicy.MaxOTPAttempts {
commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
}
_, pushErr := c.eventstore.Push(ctx, commands...)
logging.OnError(pushErr).Error("error create password check failed event") logging.OnError(pushErr).Error("error create password check failed event")
return err return verifyErr
} }
func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) { func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner string) (*domain.ObjectDetails, error) {
@ -515,14 +538,37 @@ func (c *Commands) humanCheckOTP(
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound") return zerrors.ThrowPreconditionFailed(nil, "COMMAND-S34gh", "Errors.User.Code.NotFound")
} }
userAgg := &user.NewAggregate(userID, existingOTP.ResourceOwner()).Aggregate userAgg := &user.NewAggregate(userID, existingOTP.ResourceOwner()).Aggregate
err = crypto.VerifyCode(existingOTP.CodeCreationDate(), existingOTP.CodeExpiry(), existingOTP.Code(), code, c.userEncryption) verifyErr := crypto.VerifyCode(existingOTP.CodeCreationDate(), existingOTP.CodeExpiry(), existingOTP.Code(), code, c.userEncryption)
if err == nil {
// recheck for additional events (failed OTP checks or locks)
recheckErr := c.eventstore.FilterToQueryReducer(ctx, existingOTP)
if recheckErr != nil {
return recheckErr
}
if existingOTP.UserLocked() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked")
}
// the OTP check succeeded and the user was not locked in the meantime
if verifyErr == nil {
_, err = c.eventstore.Push(ctx, checkSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest))) _, err = c.eventstore.Push(ctx, checkSucceededEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
return err return err
} }
_, pushErr := c.eventstore.Push(ctx, checkFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
// the OTP check failed, therefore check if the limit was reached and the user must additionally be locked
commands := make([]eventstore.Command, 0, 2)
commands = append(commands, checkFailedEvent(ctx, userAgg, authRequestDomainToAuthRequestInfo(authRequest)))
lockoutPolicy, err := c.getLockoutPolicy(ctx, resourceOwner)
if err != nil {
return err
}
if lockoutPolicy.MaxOTPAttempts > 0 && existingOTP.CheckFailedCount()+1 >= lockoutPolicy.MaxOTPAttempts {
commands = append(commands, user.NewUserLockedEvent(ctx, userAgg))
}
_, pushErr := c.eventstore.Push(ctx, commands...)
logging.WithFields("userID", userID).OnError(pushErr).Error("otp failure check push failed") logging.WithFields("userID", userID).OnError(pushErr).Error("otp failure check push failed")
return err return verifyErr
} }
func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) { func (c *Commands) totpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanTOTPWriteModel, err error) {

View File

@ -12,8 +12,10 @@ import (
type HumanTOTPWriteModel struct { type HumanTOTPWriteModel struct {
eventstore.WriteModel eventstore.WriteModel
State domain.MFAState State domain.MFAState
Secret *crypto.CryptoValue Secret *crypto.CryptoValue
CheckFailedCount uint64
UserLocked bool
} }
func NewHumanTOTPWriteModel(userID, resourceOwner string) *HumanTOTPWriteModel { func NewHumanTOTPWriteModel(userID, resourceOwner string) *HumanTOTPWriteModel {
@ -33,6 +35,16 @@ func (wm *HumanTOTPWriteModel) Reduce() error {
wm.State = domain.MFAStateNotReady wm.State = domain.MFAStateNotReady
case *user.HumanOTPVerifiedEvent: case *user.HumanOTPVerifiedEvent:
wm.State = domain.MFAStateReady wm.State = domain.MFAStateReady
wm.CheckFailedCount = 0
case *user.HumanOTPCheckSucceededEvent:
wm.CheckFailedCount = 0
case *user.HumanOTPCheckFailedEvent:
wm.CheckFailedCount++
case *user.UserLockedEvent:
wm.UserLocked = true
case *user.UserUnlockedEvent:
wm.CheckFailedCount = 0
wm.UserLocked = false
case *user.HumanOTPRemovedEvent: case *user.HumanOTPRemovedEvent:
wm.State = domain.MFAStateRemoved wm.State = domain.MFAStateRemoved
case *user.UserRemovedEvent: case *user.UserRemovedEvent:
@ -50,6 +62,10 @@ func (wm *HumanTOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
EventTypes(user.HumanMFAOTPAddedType, EventTypes(user.HumanMFAOTPAddedType,
user.HumanMFAOTPVerifiedType, user.HumanMFAOTPVerifiedType,
user.HumanMFAOTPRemovedType, user.HumanMFAOTPRemovedType,
user.HumanMFAOTPCheckSucceededType,
user.HumanMFAOTPCheckFailedType,
user.UserLockedType,
user.UserUnlockedType,
user.UserRemovedType, user.UserRemovedType,
user.UserV1MFAOTPAddedType, user.UserV1MFAOTPAddedType,
user.UserV1MFAOTPVerifiedType, user.UserV1MFAOTPVerifiedType,
@ -72,6 +88,9 @@ type OTPCodeWriteModel interface {
CodeCreationDate() time.Time CodeCreationDate() time.Time
CodeExpiry() time.Duration CodeExpiry() time.Duration
Code() *crypto.CryptoValue Code() *crypto.CryptoValue
CheckFailedCount() uint64
UserLocked() bool
eventstore.QueryReducer
} }
type HumanOTPSMSWriteModel struct { type HumanOTPSMSWriteModel struct {
@ -141,6 +160,9 @@ type HumanOTPSMSCodeWriteModel struct {
code *crypto.CryptoValue code *crypto.CryptoValue
codeCreationDate time.Time codeCreationDate time.Time
codeExpiry time.Duration codeExpiry time.Duration
checkFailedCount uint64
userLocked bool
} }
func (wm *HumanOTPSMSCodeWriteModel) CodeCreationDate() time.Time { func (wm *HumanOTPSMSCodeWriteModel) CodeCreationDate() time.Time {
@ -155,6 +177,14 @@ func (wm *HumanOTPSMSCodeWriteModel) Code() *crypto.CryptoValue {
return wm.code return wm.code
} }
func (wm *HumanOTPSMSCodeWriteModel) CheckFailedCount() uint64 {
return wm.checkFailedCount
}
func (wm *HumanOTPSMSCodeWriteModel) UserLocked() bool {
return wm.userLocked
}
func NewHumanOTPSMSCodeWriteModel(userID, resourceOwner string) *HumanOTPSMSCodeWriteModel { func NewHumanOTPSMSCodeWriteModel(userID, resourceOwner string) *HumanOTPSMSCodeWriteModel {
return &HumanOTPSMSCodeWriteModel{ return &HumanOTPSMSCodeWriteModel{
HumanOTPSMSWriteModel: NewHumanOTPSMSWriteModel(userID, resourceOwner), HumanOTPSMSWriteModel: NewHumanOTPSMSWriteModel(userID, resourceOwner),
@ -163,10 +193,20 @@ func NewHumanOTPSMSCodeWriteModel(userID, resourceOwner string) *HumanOTPSMSCode
func (wm *HumanOTPSMSCodeWriteModel) Reduce() error { func (wm *HumanOTPSMSCodeWriteModel) Reduce() error {
for _, event := range wm.Events { for _, event := range wm.Events {
if e, ok := event.(*user.HumanOTPSMSCodeAddedEvent); ok { switch e := event.(type) {
case *user.HumanOTPSMSCodeAddedEvent:
wm.code = e.Code wm.code = e.Code
wm.codeCreationDate = e.CreationDate() wm.codeCreationDate = e.CreationDate()
wm.codeExpiry = e.Expiry wm.codeExpiry = e.Expiry
case *user.HumanOTPSMSCheckSucceededEvent:
wm.checkFailedCount = 0
case *user.HumanOTPSMSCheckFailedEvent:
wm.checkFailedCount++
case *user.UserLockedEvent:
wm.userLocked = true
case *user.UserUnlockedEvent:
wm.checkFailedCount = 0
wm.userLocked = false
} }
} }
return wm.HumanOTPSMSWriteModel.Reduce() return wm.HumanOTPSMSWriteModel.Reduce()
@ -179,6 +219,10 @@ func (wm *HumanOTPSMSCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateIDs(wm.AggregateID). AggregateIDs(wm.AggregateID).
EventTypes( EventTypes(
user.HumanOTPSMSCodeAddedType, user.HumanOTPSMSCodeAddedType,
user.HumanOTPSMSCheckSucceededType,
user.HumanOTPSMSCheckFailedType,
user.UserLockedType,
user.UserUnlockedType,
user.HumanPhoneVerifiedType, user.HumanPhoneVerifiedType,
user.HumanOTPSMSAddedType, user.HumanOTPSMSAddedType,
user.HumanOTPSMSRemovedType, user.HumanOTPSMSRemovedType,
@ -259,6 +303,9 @@ type HumanOTPEmailCodeWriteModel struct {
code *crypto.CryptoValue code *crypto.CryptoValue
codeCreationDate time.Time codeCreationDate time.Time
codeExpiry time.Duration codeExpiry time.Duration
checkFailedCount uint64
userLocked bool
} }
func (wm *HumanOTPEmailCodeWriteModel) CodeCreationDate() time.Time { func (wm *HumanOTPEmailCodeWriteModel) CodeCreationDate() time.Time {
@ -273,6 +320,14 @@ func (wm *HumanOTPEmailCodeWriteModel) Code() *crypto.CryptoValue {
return wm.code return wm.code
} }
func (wm *HumanOTPEmailCodeWriteModel) CheckFailedCount() uint64 {
return wm.checkFailedCount
}
func (wm *HumanOTPEmailCodeWriteModel) UserLocked() bool {
return wm.userLocked
}
func NewHumanOTPEmailCodeWriteModel(userID, resourceOwner string) *HumanOTPEmailCodeWriteModel { func NewHumanOTPEmailCodeWriteModel(userID, resourceOwner string) *HumanOTPEmailCodeWriteModel {
return &HumanOTPEmailCodeWriteModel{ return &HumanOTPEmailCodeWriteModel{
HumanOTPEmailWriteModel: NewHumanOTPEmailWriteModel(userID, resourceOwner), HumanOTPEmailWriteModel: NewHumanOTPEmailWriteModel(userID, resourceOwner),
@ -281,10 +336,20 @@ func NewHumanOTPEmailCodeWriteModel(userID, resourceOwner string) *HumanOTPEmail
func (wm *HumanOTPEmailCodeWriteModel) Reduce() error { func (wm *HumanOTPEmailCodeWriteModel) Reduce() error {
for _, event := range wm.Events { for _, event := range wm.Events {
if e, ok := event.(*user.HumanOTPEmailCodeAddedEvent); ok { switch e := event.(type) {
case *user.HumanOTPEmailCodeAddedEvent:
wm.code = e.Code wm.code = e.Code
wm.codeCreationDate = e.CreationDate() wm.codeCreationDate = e.CreationDate()
wm.codeExpiry = e.Expiry wm.codeExpiry = e.Expiry
case *user.HumanOTPEmailCheckSucceededEvent:
wm.checkFailedCount = 0
case *user.HumanOTPEmailCheckFailedEvent:
wm.checkFailedCount++
case *user.UserLockedEvent:
wm.userLocked = true
case *user.UserUnlockedEvent:
wm.checkFailedCount = 0
wm.userLocked = false
} }
} }
return wm.HumanOTPEmailWriteModel.Reduce() return wm.HumanOTPEmailWriteModel.Reduce()
@ -297,6 +362,10 @@ func (wm *HumanOTPEmailCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
AggregateIDs(wm.AggregateID). AggregateIDs(wm.AggregateID).
EventTypes( EventTypes(
user.HumanOTPEmailCodeAddedType, user.HumanOTPEmailCodeAddedType,
user.HumanOTPEmailCheckSucceededType,
user.HumanOTPEmailCheckFailedType,
user.UserLockedType,
user.UserUnlockedType,
user.HumanEmailVerifiedType, user.HumanEmailVerifiedType,
user.HumanOTPEmailAddedType, user.HumanOTPEmailAddedType,
user.HumanOTPEmailRemovedType, user.HumanOTPEmailRemovedType,

View File

@ -1671,6 +1671,15 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
), ),
), ),
), ),
expectFilter(), // recheck
expectFilter(
eventFromEventPusher(
org.NewLockoutPolicyAddedEvent(ctx,
&org.NewAggregate("orgID").Aggregate,
3, 3, true,
),
),
),
expectPush( expectPush(
user.NewHumanOTPSMSCheckFailedEvent(ctx, user.NewHumanOTPSMSCheckFailedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate, &user.NewAggregate("user1", "org1").Aggregate,
@ -1707,6 +1716,86 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
err: zerrors.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"), err: zerrors.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"),
}, },
}, },
{
name: "invalid code, max attempts reached, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPSMSAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusherWithCreationDateNow(
user.NewHumanOTPSMSCodeAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("other-code"),
},
time.Hour,
&user.AuthRequestInfo{
ID: "authRequestID",
UserAgentID: "userAgentID",
BrowserInfo: &user.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
),
),
),
expectFilter(), // recheck
expectFilter(
eventFromEventPusher(
org.NewLockoutPolicyAddedEvent(ctx,
&org.NewAggregate("orgID").Aggregate,
1, 1, true,
),
),
),
expectPush(
user.NewHumanOTPSMSCheckFailedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
&user.AuthRequestInfo{
ID: "authRequestID",
UserAgentID: "userAgentID",
BrowserInfo: &user.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
),
user.NewUserLockedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: ctx,
userID: "user1",
code: "code",
resourceOwner: "org1",
authRequest: &domain.AuthRequest{
ID: "authRequestID",
AgentID: "userAgentID",
BrowserInfo: &domain.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
},
res: res{
err: zerrors.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"),
},
},
{ {
name: "code ok", name: "code ok",
fields: fields{ fields: fields{
@ -1739,6 +1828,7 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
), ),
), ),
), ),
expectFilter(), // recheck
expectPush( expectPush(
user.NewHumanOTPSMSCheckSucceededEvent(ctx, user.NewHumanOTPSMSCheckSucceededEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate, &user.NewAggregate("user1", "org1").Aggregate,
@ -1777,6 +1867,65 @@ func TestCommandSide_HumanCheckOTPSMS(t *testing.T) {
}, },
}, },
}, },
{
name: "code ok, locked in the meantime",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPSMSAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusherWithCreationDateNow(
user.NewHumanOTPSMSCodeAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("code"),
},
time.Hour,
&user.AuthRequestInfo{
ID: "authRequestID",
UserAgentID: "userAgentID",
BrowserInfo: &user.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
),
),
),
expectFilter( // recheck
user.NewUserLockedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: ctx,
userID: "user1",
code: "code",
resourceOwner: "org1",
authRequest: &domain.AuthRequest{
ID: "authRequestID",
AgentID: "userAgentID",
BrowserInfo: &domain.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
},
res: res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked"),
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -2616,6 +2765,15 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
), ),
), ),
), ),
expectFilter(), // recheck
expectFilter(
eventFromEventPusher(
org.NewLockoutPolicyAddedEvent(ctx,
&org.NewAggregate("orgID").Aggregate,
3, 3, true,
),
),
),
expectPush( expectPush(
user.NewHumanOTPEmailCheckFailedEvent(ctx, user.NewHumanOTPEmailCheckFailedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate, &user.NewAggregate("user1", "org1").Aggregate,
@ -2652,6 +2810,86 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
err: zerrors.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"), err: zerrors.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"),
}, },
}, },
{
name: "invalid code, max attempts reached, error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPEmailAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusherWithCreationDateNow(
user.NewHumanOTPEmailCodeAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("other-code"),
},
time.Hour,
&user.AuthRequestInfo{
ID: "authRequestID",
UserAgentID: "userAgentID",
BrowserInfo: &user.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
),
),
),
expectFilter(), // recheck
expectFilter(
eventFromEventPusher(
org.NewLockoutPolicyAddedEvent(ctx,
&org.NewAggregate("orgID").Aggregate,
1, 1, true,
),
),
),
expectPush(
user.NewHumanOTPEmailCheckFailedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
&user.AuthRequestInfo{
ID: "authRequestID",
UserAgentID: "userAgentID",
BrowserInfo: &user.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
),
user.NewUserLockedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: ctx,
userID: "user1",
code: "code",
resourceOwner: "org1",
authRequest: &domain.AuthRequest{
ID: "authRequestID",
AgentID: "userAgentID",
BrowserInfo: &domain.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
},
res: res{
err: zerrors.ThrowInvalidArgument(nil, "CODE-woT0xc", "Errors.User.Code.Invalid"),
},
},
{ {
name: "code ok", name: "code ok",
fields: fields{ fields: fields{
@ -2684,6 +2922,7 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
), ),
), ),
), ),
expectFilter(), // recheck
expectPush( expectPush(
user.NewHumanOTPEmailCheckSucceededEvent(ctx, user.NewHumanOTPEmailCheckSucceededEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate, &user.NewAggregate("user1", "org1").Aggregate,
@ -2722,6 +2961,65 @@ func TestCommandSide_HumanCheckOTPEmail(t *testing.T) {
}, },
}, },
}, },
{
name: "code ok, locked in the meantime",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPEmailAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
eventFromEventPusherWithCreationDateNow(
user.NewHumanOTPEmailCodeAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("code"),
},
time.Hour,
&user.AuthRequestInfo{
ID: "authRequestID",
UserAgentID: "userAgentID",
BrowserInfo: &user.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
),
),
),
expectFilter( // recheck
user.NewUserLockedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
),
userEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args: args{
ctx: ctx,
userID: "user1",
code: "code",
resourceOwner: "org1",
authRequest: &domain.AuthRequest{
ID: "authRequestID",
AgentID: "userAgentID",
BrowserInfo: &domain.BrowserInfo{
UserAgent: "user-agent",
AcceptLanguage: "en",
RemoteIP: net.IP{192, 0, 2, 1},
},
},
},
res: res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-S6h4R", "Errors.User.Locked"),
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -1643,6 +1643,7 @@ func TestCommandSide_CheckPassword(t *testing.T) {
}, },
lockoutPolicy: &domain.LockoutPolicy{ lockoutPolicy: &domain.LockoutPolicy{
MaxPasswordAttempts: 1, MaxPasswordAttempts: 1,
MaxOTPAttempts: 1,
}, },
}, },
res: res{ res: res{

View File

@ -9,5 +9,6 @@ type LockoutPolicy struct {
Default bool Default bool
MaxPasswordAttempts uint64 MaxPasswordAttempts uint64
MaxOTPAttempts uint64
ShowLockOutFailures bool ShowLockOutFailures bool
} }

View File

@ -27,6 +27,7 @@ type LockoutPolicy struct {
State domain.PolicyState State domain.PolicyState
MaxPasswordAttempts uint64 MaxPasswordAttempts uint64
MaxOTPAttempts uint64
ShowFailures bool ShowFailures bool
IsDefault bool IsDefault bool
@ -69,6 +70,10 @@ var (
name: projection.LockoutPolicyMaxPasswordAttemptsCol, name: projection.LockoutPolicyMaxPasswordAttemptsCol,
table: lockoutTable, table: lockoutTable,
} }
LockoutColMaxOTPAttempts = Column{
name: projection.LockoutPolicyMaxOTPAttemptsCol,
table: lockoutTable,
}
LockoutColIsDefault = Column{ LockoutColIsDefault = Column{
name: projection.LockoutPolicyIsDefaultCol, name: projection.LockoutPolicyIsDefaultCol,
table: lockoutTable, table: lockoutTable,
@ -77,13 +82,9 @@ var (
name: projection.LockoutPolicyStateCol, name: projection.LockoutPolicyStateCol,
table: lockoutTable, table: lockoutTable,
} }
LockoutPolicyOwnerRemoved = Column{
name: projection.LockoutPolicyOwnerRemovedCol,
table: lockoutTable,
}
) )
func (q *Queries) LockoutPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (policy *LockoutPolicy, err error) { func (q *Queries) LockoutPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string) (policy *LockoutPolicy, err error) {
ctx, span := tracing.NewSpan(ctx) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@ -96,9 +97,6 @@ func (q *Queries) LockoutPolicyByOrg(ctx context.Context, shouldTriggerBulk bool
eq := sq.Eq{ eq := sq.Eq{
LockoutColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), LockoutColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
} }
if !withOwnerRemoved {
eq[LockoutPolicyOwnerRemoved.identifier()] = false
}
stmt, scan := prepareLockoutPolicyQuery(ctx, q.client) stmt, scan := prepareLockoutPolicyQuery(ctx, q.client)
query, args, err := stmt.Where( query, args, err := stmt.Where(
@ -153,6 +151,7 @@ func prepareLockoutPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele
LockoutColResourceOwner.identifier(), LockoutColResourceOwner.identifier(),
LockoutColShowFailures.identifier(), LockoutColShowFailures.identifier(),
LockoutColMaxPasswordAttempts.identifier(), LockoutColMaxPasswordAttempts.identifier(),
LockoutColMaxOTPAttempts.identifier(),
LockoutColIsDefault.identifier(), LockoutColIsDefault.identifier(),
LockoutColState.identifier(), LockoutColState.identifier(),
). ).
@ -168,6 +167,7 @@ func prepareLockoutPolicyQuery(ctx context.Context, db prepareDatabase) (sq.Sele
&policy.ResourceOwner, &policy.ResourceOwner,
&policy.ShowFailures, &policy.ShowFailures,
&policy.MaxPasswordAttempts, &policy.MaxPasswordAttempts,
&policy.MaxOTPAttempts,
&policy.IsDefault, &policy.IsDefault,
&policy.State, &policy.State,
) )

View File

@ -13,16 +13,17 @@ import (
) )
var ( var (
prepareLockoutPolicyStmt = `SELECT projections.lockout_policies2.id,` + prepareLockoutPolicyStmt = `SELECT projections.lockout_policies3.id,` +
` projections.lockout_policies2.sequence,` + ` projections.lockout_policies3.sequence,` +
` projections.lockout_policies2.creation_date,` + ` projections.lockout_policies3.creation_date,` +
` projections.lockout_policies2.change_date,` + ` projections.lockout_policies3.change_date,` +
` projections.lockout_policies2.resource_owner,` + ` projections.lockout_policies3.resource_owner,` +
` projections.lockout_policies2.show_failure,` + ` projections.lockout_policies3.show_failure,` +
` projections.lockout_policies2.max_password_attempts,` + ` projections.lockout_policies3.max_password_attempts,` +
` projections.lockout_policies2.is_default,` + ` projections.lockout_policies3.max_otp_attempts,` +
` projections.lockout_policies2.state` + ` projections.lockout_policies3.is_default,` +
` FROM projections.lockout_policies2` + ` projections.lockout_policies3.state` +
` FROM projections.lockout_policies3` +
` AS OF SYSTEM TIME '-1 ms'` ` AS OF SYSTEM TIME '-1 ms'`
prepareLockoutPolicyCols = []string{ prepareLockoutPolicyCols = []string{
@ -33,6 +34,7 @@ var (
"resource_owner", "resource_owner",
"show_failure", "show_failure",
"max_password_attempts", "max_password_attempts",
"max_otp_attempts",
"is_default", "is_default",
"state", "state",
} }
@ -82,6 +84,7 @@ func Test_LockoutPolicyPrepares(t *testing.T) {
"ro", "ro",
true, true,
20, 20,
20,
true, true,
domain.PolicyStateActive, domain.PolicyStateActive,
}, },
@ -96,6 +99,7 @@ func Test_LockoutPolicyPrepares(t *testing.T) {
State: domain.PolicyStateActive, State: domain.PolicyStateActive,
ShowFailures: true, ShowFailures: true,
MaxPasswordAttempts: 20, MaxPasswordAttempts: 20,
MaxOTPAttempts: 20,
IsDefault: true, IsDefault: true,
}, },
}, },

View File

@ -14,7 +14,7 @@ import (
) )
const ( const (
LockoutPolicyTable = "projections.lockout_policies2" LockoutPolicyTable = "projections.lockout_policies3"
LockoutPolicyIDCol = "id" LockoutPolicyIDCol = "id"
LockoutPolicyCreationDateCol = "creation_date" LockoutPolicyCreationDateCol = "creation_date"
@ -25,8 +25,8 @@ const (
LockoutPolicyResourceOwnerCol = "resource_owner" LockoutPolicyResourceOwnerCol = "resource_owner"
LockoutPolicyInstanceIDCol = "instance_id" LockoutPolicyInstanceIDCol = "instance_id"
LockoutPolicyMaxPasswordAttemptsCol = "max_password_attempts" LockoutPolicyMaxPasswordAttemptsCol = "max_password_attempts"
LockoutPolicyMaxOTPAttemptsCol = "max_otp_attempts"
LockoutPolicyShowLockOutFailuresCol = "show_failure" LockoutPolicyShowLockOutFailuresCol = "show_failure"
LockoutPolicyOwnerRemovedCol = "owner_removed"
) )
type lockoutPolicyProjection struct{} type lockoutPolicyProjection struct{}
@ -51,11 +51,10 @@ func (*lockoutPolicyProjection) Init() *old_handler.Check {
handler.NewColumn(LockoutPolicyResourceOwnerCol, handler.ColumnTypeText), handler.NewColumn(LockoutPolicyResourceOwnerCol, handler.ColumnTypeText),
handler.NewColumn(LockoutPolicyInstanceIDCol, handler.ColumnTypeText), handler.NewColumn(LockoutPolicyInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(LockoutPolicyMaxPasswordAttemptsCol, handler.ColumnTypeInt64), handler.NewColumn(LockoutPolicyMaxPasswordAttemptsCol, handler.ColumnTypeInt64),
handler.NewColumn(LockoutPolicyMaxOTPAttemptsCol, handler.ColumnTypeInt64, handler.Default(0)),
handler.NewColumn(LockoutPolicyShowLockOutFailuresCol, handler.ColumnTypeBool), handler.NewColumn(LockoutPolicyShowLockOutFailuresCol, handler.ColumnTypeBool),
handler.NewColumn(LockoutPolicyOwnerRemovedCol, handler.ColumnTypeBool, handler.Default(false)),
}, },
handler.NewPrimaryKey(LockoutPolicyInstanceIDCol, LockoutPolicyIDCol), handler.NewPrimaryKey(LockoutPolicyInstanceIDCol, LockoutPolicyIDCol),
handler.WithIndex(handler.NewIndex("owner_removed", []string{LockoutPolicyOwnerRemovedCol})),
), ),
) )
} }
@ -125,6 +124,7 @@ func (p *lockoutPolicyProjection) reduceAdded(event eventstore.Event) (*handler.
handler.NewCol(LockoutPolicyIDCol, policyEvent.Aggregate().ID), handler.NewCol(LockoutPolicyIDCol, policyEvent.Aggregate().ID),
handler.NewCol(LockoutPolicyStateCol, domain.PolicyStateActive), handler.NewCol(LockoutPolicyStateCol, domain.PolicyStateActive),
handler.NewCol(LockoutPolicyMaxPasswordAttemptsCol, policyEvent.MaxPasswordAttempts), handler.NewCol(LockoutPolicyMaxPasswordAttemptsCol, policyEvent.MaxPasswordAttempts),
handler.NewCol(LockoutPolicyMaxOTPAttemptsCol, policyEvent.MaxOTPAttempts),
handler.NewCol(LockoutPolicyShowLockOutFailuresCol, policyEvent.ShowLockOutFailures), handler.NewCol(LockoutPolicyShowLockOutFailuresCol, policyEvent.ShowLockOutFailures),
handler.NewCol(LockoutPolicyIsDefaultCol, isDefault), handler.NewCol(LockoutPolicyIsDefaultCol, isDefault),
handler.NewCol(LockoutPolicyResourceOwnerCol, policyEvent.Aggregate().ResourceOwner), handler.NewCol(LockoutPolicyResourceOwnerCol, policyEvent.Aggregate().ResourceOwner),
@ -149,6 +149,9 @@ func (p *lockoutPolicyProjection) reduceChanged(event eventstore.Event) (*handle
if policyEvent.MaxPasswordAttempts != nil { if policyEvent.MaxPasswordAttempts != nil {
cols = append(cols, handler.NewCol(LockoutPolicyMaxPasswordAttemptsCol, *policyEvent.MaxPasswordAttempts)) cols = append(cols, handler.NewCol(LockoutPolicyMaxPasswordAttemptsCol, *policyEvent.MaxPasswordAttempts))
} }
if policyEvent.MaxOTPAttempts != nil {
cols = append(cols, handler.NewCol(LockoutPolicyMaxOTPAttemptsCol, *policyEvent.MaxOTPAttempts))
}
if policyEvent.ShowLockOutFailures != nil { if policyEvent.ShowLockOutFailures != nil {
cols = append(cols, handler.NewCol(LockoutPolicyShowLockOutFailuresCol, *policyEvent.ShowLockOutFailures)) cols = append(cols, handler.NewCol(LockoutPolicyShowLockOutFailuresCol, *policyEvent.ShowLockOutFailures))
} }

View File

@ -30,6 +30,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
org.AggregateType, org.AggregateType,
[]byte(`{ []byte(`{
"maxPasswordAttempts": 10, "maxPasswordAttempts": 10,
"maxOTPAttempts": 10,
"showLockOutFailures": true "showLockOutFailures": true
}`), }`),
), org.LockoutPolicyAddedEventMapper), ), org.LockoutPolicyAddedEventMapper),
@ -41,7 +42,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "INSERT INTO projections.lockout_policies2 (creation_date, change_date, sequence, id, state, max_password_attempts, show_failure, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedStmt: "INSERT INTO projections.lockout_policies3 (creation_date, change_date, sequence, id, state, max_password_attempts, max_otp_attempts, show_failure, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -49,6 +50,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
"agg-id", "agg-id",
domain.PolicyStateActive, domain.PolicyStateActive,
uint64(10), uint64(10),
uint64(10),
true, true,
false, false,
"ro-id", "ro-id",
@ -69,6 +71,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
org.AggregateType, org.AggregateType,
[]byte(`{ []byte(`{
"maxPasswordAttempts": 10, "maxPasswordAttempts": 10,
"maxOTPAttempts": 10,
"showLockOutFailures": true "showLockOutFailures": true
}`), }`),
), org.LockoutPolicyChangedEventMapper), ), org.LockoutPolicyChangedEventMapper),
@ -79,11 +82,12 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.lockout_policies2 SET (change_date, sequence, max_password_attempts, show_failure) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedStmt: "UPDATE projections.lockout_policies3 SET (change_date, sequence, max_password_attempts, max_otp_attempts, show_failure) = ($1, $2, $3, $4, $5) WHERE (id = $6) AND (instance_id = $7)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
uint64(15), uint64(15),
uint64(10), uint64(10),
uint64(10),
true, true,
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -110,7 +114,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "DELETE FROM projections.lockout_policies2 WHERE (id = $1) AND (instance_id = $2)", expectedStmt: "DELETE FROM projections.lockout_policies3 WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -137,7 +141,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "DELETE FROM projections.lockout_policies2 WHERE (instance_id = $1)", expectedStmt: "DELETE FROM projections.lockout_policies3 WHERE (instance_id = $1)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
}, },
@ -156,6 +160,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
instance.AggregateType, instance.AggregateType,
[]byte(`{ []byte(`{
"maxPasswordAttempts": 10, "maxPasswordAttempts": 10,
"maxOTPAttempts": 10,
"showLockOutFailures": true "showLockOutFailures": true
}`), }`),
), instance.LockoutPolicyAddedEventMapper), ), instance.LockoutPolicyAddedEventMapper),
@ -166,7 +171,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "INSERT INTO projections.lockout_policies2 (creation_date, change_date, sequence, id, state, max_password_attempts, show_failure, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", expectedStmt: "INSERT INTO projections.lockout_policies3 (creation_date, change_date, sequence, id, state, max_password_attempts, max_otp_attempts, show_failure, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -174,6 +179,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
"agg-id", "agg-id",
domain.PolicyStateActive, domain.PolicyStateActive,
uint64(10), uint64(10),
uint64(10),
true, true,
true, true,
"ro-id", "ro-id",
@ -194,6 +200,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
instance.AggregateType, instance.AggregateType,
[]byte(`{ []byte(`{
"maxPasswordAttempts": 10, "maxPasswordAttempts": 10,
"maxOTPAttempts": 10,
"showLockOutFailures": true "showLockOutFailures": true
}`), }`),
), instance.LockoutPolicyChangedEventMapper), ), instance.LockoutPolicyChangedEventMapper),
@ -204,11 +211,12 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.lockout_policies2 SET (change_date, sequence, max_password_attempts, show_failure) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedStmt: "UPDATE projections.lockout_policies3 SET (change_date, sequence, max_password_attempts, max_otp_attempts, show_failure) = ($1, $2, $3, $4, $5) WHERE (id = $6) AND (instance_id = $7)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
uint64(15), uint64(15),
uint64(10), uint64(10),
uint64(10),
true, true,
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -235,7 +243,7 @@ func TestLockoutPolicyProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "DELETE FROM projections.lockout_policies2 WHERE (instance_id = $1) AND (resource_owner = $2)", expectedStmt: "DELETE FROM projections.lockout_policies3 WHERE (instance_id = $1) AND (resource_owner = $2)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"instance-id", "instance-id",
"agg-id", "agg-id",

View File

@ -19,7 +19,8 @@ type LockoutPolicyAddedEvent struct {
func NewLockoutPolicyAddedEvent( func NewLockoutPolicyAddedEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
maxAttempts uint64, maxPasswordAttempts,
maxOTPAttempts uint64,
showLockoutFailure bool, showLockoutFailure bool,
) *LockoutPolicyAddedEvent { ) *LockoutPolicyAddedEvent {
return &LockoutPolicyAddedEvent{ return &LockoutPolicyAddedEvent{
@ -28,7 +29,8 @@ func NewLockoutPolicyAddedEvent(
ctx, ctx,
aggregate, aggregate,
LockoutPolicyAddedEventType), LockoutPolicyAddedEventType),
maxAttempts, maxPasswordAttempts,
maxOTPAttempts,
showLockoutFailure), showLockoutFailure),
} }
} }

View File

@ -20,7 +20,8 @@ type LockoutPolicyAddedEvent struct {
func NewLockoutPolicyAddedEvent( func NewLockoutPolicyAddedEvent(
ctx context.Context, ctx context.Context,
aggregate *eventstore.Aggregate, aggregate *eventstore.Aggregate,
maxAttempts uint64, maxPasswordAttempts,
maxOTPAttempts uint64,
showLockoutFailure bool, showLockoutFailure bool,
) *LockoutPolicyAddedEvent { ) *LockoutPolicyAddedEvent {
return &LockoutPolicyAddedEvent{ return &LockoutPolicyAddedEvent{
@ -29,7 +30,8 @@ func NewLockoutPolicyAddedEvent(
ctx, ctx,
aggregate, aggregate,
LockoutPolicyAddedEventType), LockoutPolicyAddedEventType),
maxAttempts, maxPasswordAttempts,
maxOTPAttempts,
showLockoutFailure), showLockoutFailure),
} }
} }

View File

@ -15,6 +15,7 @@ type LockoutPolicyAddedEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`
MaxPasswordAttempts uint64 `json:"maxPasswordAttempts,omitempty"` MaxPasswordAttempts uint64 `json:"maxPasswordAttempts,omitempty"`
MaxOTPAttempts uint64 `json:"maxOTPAttempts,omitempty"`
ShowLockOutFailures bool `json:"showLockOutFailures,omitempty"` ShowLockOutFailures bool `json:"showLockOutFailures,omitempty"`
} }
@ -28,13 +29,15 @@ func (e *LockoutPolicyAddedEvent) UniqueConstraints() []*eventstore.UniqueConstr
func NewLockoutPolicyAddedEvent( func NewLockoutPolicyAddedEvent(
base *eventstore.BaseEvent, base *eventstore.BaseEvent,
maxAttempts uint64, maxPasswordAttempts,
maxOTPAttempts uint64,
showLockOutFailures bool, showLockOutFailures bool,
) *LockoutPolicyAddedEvent { ) *LockoutPolicyAddedEvent {
return &LockoutPolicyAddedEvent{ return &LockoutPolicyAddedEvent{
BaseEvent: *base, BaseEvent: *base,
MaxPasswordAttempts: maxAttempts, MaxPasswordAttempts: maxPasswordAttempts,
MaxOTPAttempts: maxOTPAttempts,
ShowLockOutFailures: showLockOutFailures, ShowLockOutFailures: showLockOutFailures,
} }
} }
@ -56,6 +59,7 @@ type LockoutPolicyChangedEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`
MaxPasswordAttempts *uint64 `json:"maxPasswordAttempts,omitempty"` MaxPasswordAttempts *uint64 `json:"maxPasswordAttempts,omitempty"`
MaxOTPAttempts *uint64 `json:"maxOTPAttempts,omitempty"`
ShowLockOutFailures *bool `json:"showLockOutFailures,omitempty"` ShowLockOutFailures *bool `json:"showLockOutFailures,omitempty"`
} }
@ -85,12 +89,18 @@ func NewLockoutPolicyChangedEvent(
type LockoutPolicyChanges func(*LockoutPolicyChangedEvent) type LockoutPolicyChanges func(*LockoutPolicyChangedEvent)
func ChangeMaxAttempts(maxAttempts uint64) func(*LockoutPolicyChangedEvent) { func ChangeMaxPasswordAttempts(maxAttempts uint64) func(*LockoutPolicyChangedEvent) {
return func(e *LockoutPolicyChangedEvent) { return func(e *LockoutPolicyChangedEvent) {
e.MaxPasswordAttempts = &maxAttempts e.MaxPasswordAttempts = &maxAttempts
} }
} }
func ChangeMaxOTPAttempts(maxAttempts uint64) func(*LockoutPolicyChangedEvent) {
return func(e *LockoutPolicyChangedEvent) {
e.MaxOTPAttempts = &maxAttempts
}
}
func ChangeShowLockOutFailures(showLockOutFailures bool) func(*LockoutPolicyChangedEvent) { func ChangeShowLockOutFailures(showLockOutFailures bool) func(*LockoutPolicyChangedEvent) {
return func(e *LockoutPolicyChangedEvent) { return func(e *LockoutPolicyChangedEvent) {
e.ShowLockOutFailures = &showLockOutFailures e.ShowLockOutFailures = &showLockOutFailures

View File

@ -6650,6 +6650,12 @@ message UpdateLockoutPolicyRequest {
example: "\"10\"" example: "\"10\""
} }
]; ];
uint32 max_otp_attempts = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum failed attempts for a single OTP type (TOTP, SMS, Email) before the account gets locked. Attempts are reset as soon as the OTP is entered correctly. If set to 0 the account will never be locked."
example: "\"10\""
}
];
} }
message UpdateLockoutPolicyResponse { message UpdateLockoutPolicyResponse {

View File

@ -10412,6 +10412,12 @@ message AddCustomLockoutPolicyRequest {
description: "When the user has reached the maximum password attempts the account will be locked, If this is set to 0 the lockout will not trigger." description: "When the user has reached the maximum password attempts the account will be locked, If this is set to 0 the lockout will not trigger."
} }
]; ];
uint32 max_otp_attempts = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum failed attempts for a single OTP type (TOTP, SMS, Email) before the account gets locked. Attempts are reset as soon as the OTP is entered correctly. If set to 0 the account will never be locked."
example: "\"10\""
}
];
} }
message AddCustomLockoutPolicyResponse { message AddCustomLockoutPolicyResponse {
@ -10424,6 +10430,12 @@ message UpdateCustomLockoutPolicyRequest {
description: "When the user has reached the maximum password attempts the account will be locked, If this is set to 0 the lockout will not trigger." description: "When the user has reached the maximum password attempts the account will be locked, If this is set to 0 the lockout will not trigger."
} }
]; ];
uint32 max_otp_attempts = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum failed attempts for a single OTP type (TOTP, SMS, Email) before the account gets locked. Attempts are reset as soon as the OTP is entered correctly. If set to 0 the account will never be locked."
example: "\"10\""
}
];
} }
message UpdateCustomLockoutPolicyResponse { message UpdateCustomLockoutPolicyResponse {

View File

@ -337,6 +337,12 @@ message LockoutPolicy {
example: "\"10\"" example: "\"10\""
} }
]; ];
uint64 max_otp_attempts = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum failed attempts for a single OTP type (TOTP, SMS, Email) before the account gets locked. Attempts are reset as soon as the OTP is entered correctly. If set to 0 the account will never be locked."
example: "\"10\""
}
];
bool is_default = 4 [ bool is_default = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines if the organization's admin changed the policy" description: "defines if the organization's admin changed the policy"

View File

@ -20,4 +20,10 @@ message LockoutSettings {
description: "resource_owner_type returns if the settings is managed on the organization or on the instance"; description: "resource_owner_type returns if the settings is managed on the organization or on the instance";
} }
]; ];
uint64 max_otp_attempts = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Maximum failed attempts for a single OTP type (TOTP, SMS, Email) before the account gets locked. Attempts are reset as soon as the OTP is entered correctly. If set to 0 the account will never be locked."
example: "\"10\""
}
];
} }