Files
zitadel/console/src/app/modules/policies/login-policy/login-policy.component.html
T
381727e919 fix(console): create login policy before adding a factor (#10202)
<!--
Please inform yourself about the contribution guidelines on submitting a
PR here:
https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr.
Take note of how PR/commit titles should be written and replace the
template texts in the sections below. Don't remove any of the sections.
It is important that the commit history clearly shows what is changed
and why.
Important: By submitting a contribution you agree to the terms from our
Licensing Policy as described here:
https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions.
-->

# Which Problems Are Solved

The api call for adding a mfa/2fa factor was running before creating a
login policy.

# How the Problems Are Solved

A new event emitter `beforeTypeAdd` is responsible for taking in a
callback, and then the parent component (login-policy) checks if a login
policy should be created, and then it is successfully created, calls the
callback.
The callback then adds the mfa/2fa factor as before.

# Additional Context

- Closes #9047

Co-authored-by: David Skewis <[email protected]>
Co-authored-by: Stefan Benz <[email protected]>
2025-10-16 11:12:07 +01:00

487 lines
15 KiB
HTML

<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<h2>{{ 'SETTINGS.LIST.LOGIN' | translate }}</h2>
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault">
<ng-template cnslHasRole [hasRole]="['policy.delete']">
<button
color="primary"
class="loginpolicy-reset-button"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="removePolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
</ng-container>
<br />
<h3>{{ 'MFA.LIST.MULTIFACTORTITLE' | translate }}</h3>
<p class="cnsl-secondary-text">{{ 'MFA.LIST.MULTIFACTORDESCRIPTION' | translate }}</p>
<div class="login-policy-row" *ngIf="loginData">
<cnsl-form-field class="passwordless-allowed" label="Access Code" required="true">
<cnsl-label>{{ 'LOGINPOLICY.PASSWORDLESS' | translate }}</cnsl-label>
<mat-select
[(ngModel)]="loginData.passwordlessType"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
<mat-option *ngFor="let pt of passwordlessTypes" [value]="pt">
{{ 'LOGINPOLICY.PASSWORDLESSTYPE.' + pt | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<cnsl-card class="max-card-width" *ngIf="loginData">
<cnsl-factor-table
[service]="service"
[serviceType]="serviceType"
[componentType]="LoginMethodComponentType.MultiFactor"
[list]="loginData.multiFactorsList"
(typeRemoved)="removeFactor($event)"
(beforeTypeAdd)="beforeAddFactor($event)"
(typeAdded)="addFactor($event)"
[disabled]="
loginData.passwordlessType === PasswordlessType.PASSWORDLESS_TYPE_NOT_ALLOWED ||
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
</cnsl-factor-table>
</cnsl-card>
<br />
<h3>{{ 'MFA.LIST.SECONDFACTORTITLE' | translate }}</h3>
<p class="cnsl-secondary-text">{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}</p>
<div *ngIf="loginData" class="login-policy-row">
<mat-checkbox
card-actions
class="login-policy-toggle"
color="primary"
ngDefaultControl
[(ngModel)]="loginData.forceMfa"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'POLICY.DATA.FORCEMFA' | translate }}
</mat-checkbox>
</div>
<div *ngIf="loginData" class="login-policy-row">
<mat-checkbox
card-actions
class="login-policy-toggle"
color="primary"
ngDefaultControl
[(ngModel)]="loginData.forceMfaLocalOnly"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'POLICY.DATA.FORCEMFALOCALONLY' | translate }}
</mat-checkbox>
</div>
<cnsl-card class="max-card-width" *ngIf="loginData">
<cnsl-factor-table
[service]="service"
[serviceType]="serviceType"
[componentType]="LoginMethodComponentType.SecondFactor"
[list]="loginData.secondFactorsList"
(typeRemoved)="removeFactor($event)"
(beforeTypeAdd)="beforeAddFactor($event)"
(typeAdded)="addFactor($event)"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
</cnsl-factor-table>
</cnsl-card>
<br />
<h3>{{ 'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.TITLE' | translate }}</h3>
<p class="cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.DESCRIPTION' | translate }}</p>
<form class="lifetime-form" (ngSubmit)="savePolicy()" [formGroup]="lifetimeForm" autocomplete="off">
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.PW_CHECK.TITLE' | translate"
[description]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.PW_CHECK.DESCRIPTION' | translate"
>
<cnsl-form-field class="lifetime-form-field" label="Password Check Lifetime" required="true">
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.LABEL' | translate }}</cnsl-label>
<input cnslInput type="number" name="passwordCheckLifetime" formControlName="passwordCheckLifetime" min="1" step="1" />
</cnsl-form-field>
</cnsl-card>
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.EXT_LOGIN_CHECK.TITLE' | translate"
[description]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.EXT_LOGIN_CHECK.DESCRIPTION' | translate"
>
<cnsl-form-field class="lifetime-form-field" label="external Login Check Lifetime" required="true">
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.LABEL' | translate }}</cnsl-label>
<input
cnslInput
type="number"
name="externalLoginCheckLifetime"
formControlName="externalLoginCheckLifetime"
min="1"
step="1"
/>
</cnsl-form-field>
</cnsl-card>
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.MULTI_FACTOR_INIT.TITLE' | translate"
[description]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.MULTI_FACTOR_INIT.DESCRIPTION' | translate"
>
<cnsl-form-field class="lifetime-form-field" label="MFA Init Skip Lifetime" required="true">
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.LABEL' | translate }}</cnsl-label>
<input cnslInput type="number" name="mfaInitSkipLifetime" formControlName="mfaInitSkipLifetime" min="0" step="1" />
</cnsl-form-field>
</cnsl-card>
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.SECOND_FACTOR_CHECK.TITLE' | translate"
[description]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.SECOND_FACTOR_CHECK.DESCRIPTION' | translate"
>
<cnsl-form-field class="lifetime-form-field" label="Second Factor Check Lifetime" required="true">
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.LABEL' | translate }}</cnsl-label>
<input
cnslInput
type="number"
name="secondFactorCheckLifetime"
formControlName="secondFactorCheckLifetime"
min="1"
step="1"
/>
</cnsl-form-field>
</cnsl-card>
<cnsl-card
[title]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.MULTI_FACTOR_CHECK.TITLE' | translate"
[description]="'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.MULTI_FACTOR_CHECK.DESCRIPTION' | translate"
>
<cnsl-form-field class="lifetime-form-field" label="Multi-factor Check Lifetime" required="true">
<cnsl-label>{{ 'DESCRIPTIONS.SETTINGS.LOGIN.LIFETIMES.LABEL' | translate }}</cnsl-label>
<input
cnslInput
type="number"
name="multiFactorCheckLifetime"
formControlName="multiFactorCheckLifetime"
min="1"
step="1"
/>
</cnsl-form-field>
</cnsl-card>
</form>
<br />
<h3>{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.TITLE' | translate }}</h3>
<p class="cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.DESCRIPTION' | translate }}</p>
<div class="max-card-width login-policy-content" *ngIf="loginData">
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.USERNAME_PASSWORD_ALLOWED.DESCRIPTION' | translate }}"
ngDefaultControl
[(ngModel)]="loginData.allowUsernamePassword"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.USERNAME_PASSWORD_ALLOWED.TITLE' | translate }}
</mat-checkbox>
<!-- <cnsl-info-section class="info">
{{ 'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate }}
</cnsl-info-section> -->
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.USER_REGISTRATION_ALLOWED.DESCRIPTION' | translate }}"
[(ngModel)]="loginData.allowRegister"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.USER_REGISTRATION_ALLOWED.TITLE' | translate }}
</mat-checkbox>
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.ORG_REGISTRATION_ALLOWED.DESCRIPTION' | translate }}"
[(ngModel)]="allowOrgRegistration"
*ngIf="serviceType === PolicyComponentServiceType.ADMIN"
[disabled]="(['iam.restrictions.write'] | hasRole | async) === false"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.ORG_REGISTRATION_ALLOWED.TITLE' | translate }}
</mat-checkbox>
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.EXTERNAL_LOGIN_ALLOWED.DESCRIPTION' | translate }}"
[(ngModel)]="loginData.allowExternalIdp"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.EXTERNAL_LOGIN_ALLOWED.TITLE' | translate }}
</mat-checkbox>
<!-- <ng-template #idpInfo>
<cnsl-info-section class="info">
{{ 'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate }}
</cnsl-info-section>
</ng-template> -->
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.HIDE_PASSWORD_RESET.DESCRIPTION' | translate }}"
[(ngModel)]="loginData.hidePasswordReset"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.HIDE_PASSWORD_RESET.TITLE' | translate }}
</mat-checkbox>
<!-- <ng-template #passwordResetInfo>
<cnsl-info-section class="info">
{{ 'POLICY.DATA.HIDEPASSWORDRESET_DESC' | translate }}
</cnsl-info-section>
</ng-template> -->
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.DOMAIN_DISCOVERY_ALLOWED.DESCRIPTION' | translate }}"
[(ngModel)]="loginData.allowDomainDiscovery"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.DOMAIN_DISCOVERY_ALLOWED.TITLE' | translate }}
</mat-checkbox>
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.IGNORE_UNKNOWN_USERNAMES.DESCRIPTION' | translate }}"
[(ngModel)]="loginData.ignoreUnknownUsernames"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.IGNORE_UNKNOWN_USERNAMES.TITLE' | translate }}
</mat-checkbox>
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.DISABLE_EMAIL_LOGIN.DESCRIPTION' | translate }}"
[(ngModel)]="loginData.disableLoginWithEmail"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.DISABLE_EMAIL_LOGIN.TITLE' | translate }}
</mat-checkbox>
</div>
<div class="login-policy-row">
<mat-checkbox
class="login-policy-toggle"
color="primary"
ngDefaultControl
matTooltip="{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.DISABLE_PHONE_LOGIN.DESCRIPTION' | translate }}"
[(ngModel)]="loginData.disableLoginWithPhone"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'DESCRIPTIONS.SETTINGS.LOGIN.FORM.DISABLE_PHONE_LOGIN.TITLE' | translate }}
</mat-checkbox>
</div>
<div class="login-policy-row">
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>{{ 'POLICY.DATA.DEFAULTREDIRECTURI' | translate }}</cnsl-label>
<input
cnslInput
placeholder="https://"
[(ngModel)]="loginData.defaultRedirectUri"
[disabled]="
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
/>
</cnsl-form-field>
</div>
</div>
<br />
<div class="login-policy-btn-container">
<button
class="login-policy-save-button"
(click)="savePolicy()"
color="primary"
type="submit"
mat-raised-button
[disabled]="
(['iam.restrictions.write'] | hasRole | async) === false &&
([
serviceType === PolicyComponentServiceType.ADMIN
? 'iam.policy.write'
: serviceType === PolicyComponentServiceType.MGMT
? 'policy.write'
: '',
]
| hasRole
| async) === false
"
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>