Merge pull request #14043 from ArtemDzhereleiko/AD/imp/enforce-2fa
2FA enforcement
This commit is contained in:
commit
cdacf1fe53
@ -68,6 +68,7 @@ export class AuthService {
|
||||
redirectUrl: string;
|
||||
oauth2Clients: Array<OAuth2ClientLoginInfo> = null;
|
||||
twoFactorAuthProviders: Array<TwoFaProviderInfo> = null;
|
||||
forceTwoFactorAuthProviders: Array<TwoFactorAuthProviderType> = null;
|
||||
|
||||
private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
|
||||
private jwtHelper = new JwtHelperService();
|
||||
@ -117,6 +118,9 @@ export class AuthService {
|
||||
if (loginResponse.scope === Authority.PRE_VERIFICATION_TOKEN) {
|
||||
this.router.navigateByUrl(`login/mfa`);
|
||||
}
|
||||
if (loginResponse.scope === Authority.MFA_CONFIGURATION_TOKEN) {
|
||||
this.router.navigateByUrl(`login/force-mfa`);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
@ -239,6 +243,15 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
public getAvailableTwoFaProviders(): Observable<Array<TwoFaProviderInfo>> {
|
||||
return this.http.get<Array<TwoFaProviderInfo>>(`/api/2fa/providers`, defaultHttpOptions()).pipe(
|
||||
catchError(() => of([])),
|
||||
tap((providers) => {
|
||||
this.forceTwoFactorAuthProviders = providers;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean {
|
||||
if (authState && authState.authUser) {
|
||||
if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) {
|
||||
@ -266,6 +279,8 @@ export class AuthService {
|
||||
if (isAuthenticated) {
|
||||
if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {
|
||||
result = this.router.parseUrl('login/mfa');
|
||||
} else if (authState.authUser.authority === Authority.MFA_CONFIGURATION_TOKEN) {
|
||||
result = this.router.parseUrl('login/force-mfa');
|
||||
} else if (!path || path === 'login' || this.forceDefaultPlace(authState, path, params)) {
|
||||
if (this.redirectUrl) {
|
||||
const redirectUrl = this.redirectUrl;
|
||||
@ -399,7 +414,7 @@ export class AuthService {
|
||||
loadUserSubject.error(err);
|
||||
}
|
||||
);
|
||||
} else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) {
|
||||
} else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN || authPayload.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) {
|
||||
loadUserSubject.next(authPayload);
|
||||
loadUserSubject.complete();
|
||||
} else if (authPayload.authUser?.userId) {
|
||||
|
||||
@ -104,6 +104,16 @@ export class AuthGuard {
|
||||
}
|
||||
this.authService.logout();
|
||||
return of(this.authService.defaultUrl(false));
|
||||
} else if (path === 'login.force-mfa') {
|
||||
if (authState.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) {
|
||||
return this.authService.getAvailableTwoFaProviders().pipe(
|
||||
map(() => {
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
this.authService.logout();
|
||||
return of(this.authService.defaultUrl(false));
|
||||
} else {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
@ -30,163 +30,216 @@
|
||||
<mat-card-content>
|
||||
<form [formGroup]="twoFaFormGroup" (ngSubmit)="save()">
|
||||
<fieldset [disabled]="isLoading$ | async">
|
||||
<div>
|
||||
<fieldset class="fields-group" formArrayName="providers">
|
||||
<legend class="group-title" translate>admin.2fa.available-providers</legend>
|
||||
<ng-container *ngFor="let provider of providersForm.controls; let i = index; let $last = last; trackBy: trackByElement">
|
||||
<mat-expansion-panel class="provider" [formGroupName]="i">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title class="flex items-center justify-start">
|
||||
<mat-slide-toggle
|
||||
(mousedown)="toggleExtensionPanel($event, i, provider.get('enable').value)"
|
||||
formControlName="enable">
|
||||
{{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }}
|
||||
</mat-slide-toggle>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<ng-template matExpansionPanelContent>
|
||||
<ng-container [ngSwitch]="provider.get('providerType').value">
|
||||
<ng-container *ngSwitchCase="twoFactorAuthProviderType.TOTP">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.issuer-name</mat-label>
|
||||
<input matInput formControlName="issuerName" required>
|
||||
<mat-error *ngIf="provider.get('issuerName').hasError('required') ||
|
||||
provider.get('issuerName').hasError('pattern')">
|
||||
{{ "admin.2fa.issuer-name-required" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
<div *ngSwitchCase="twoFactorAuthProviderType.SMS"
|
||||
class="flex flex-row xs:flex-col gt-xs:gap-2">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.verification-message-template</mat-label>
|
||||
<input matInput formControlName="smsVerificationMessageTemplate" required>
|
||||
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('required')">
|
||||
{{ "admin.2fa.verification-message-template-required" | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('pattern')">
|
||||
{{ "admin.2fa.verification-message-template-pattern" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.verification-code-lifetime</mat-label>
|
||||
<input matInput formControlName="verificationCodeLifetime" type="number" step="1" min="1" required>
|
||||
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('required')">
|
||||
{{ "admin.2fa.verification-code-lifetime-required" | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('min') ||
|
||||
provider.get('verificationCodeLifetime').hasError('pattern')">
|
||||
{{ "admin.2fa.verification-code-lifetime-pattern" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngSwitchCase="twoFactorAuthProviderType.EMAIL">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.verification-code-lifetime</mat-label>
|
||||
<input matInput formControlName="verificationCodeLifetime" type="number" step="1" min="1" required>
|
||||
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('required')">
|
||||
{{ "admin.2fa.verification-code-lifetime-required" | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('min') ||
|
||||
provider.get('verificationCodeLifetime').hasError('pattern')">
|
||||
{{ "admin.2fa.verification-code-lifetime-pattern" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngSwitchCase="twoFactorAuthProviderType.BACKUP_CODE">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.number-of-codes</mat-label>
|
||||
<input matInput formControlName="codesQuantity" type="number" step="1" min="1" required>
|
||||
<mat-error *ngIf="provider.get('codesQuantity').hasError('required')">
|
||||
{{ "admin.2fa.number-of-codes-required" | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="provider.get('codesQuantity').hasError('min') ||
|
||||
provider.get('codesQuantity').hasError('pattern')">
|
||||
{{ "admin.2fa.number-of-codes-pattern" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
<mat-divider *ngIf="!$last"></mat-divider>
|
||||
</ng-container>
|
||||
</fieldset>
|
||||
<fieldset class="fields-group">
|
||||
<legend class="group-title" translate>admin.2fa.verification-limitations</legend>
|
||||
<div class="input-row flex flex-row xs:flex-col gt-xs:gap-2">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.total-allowed-time-for-verification</mat-label>
|
||||
<input matInput required formControlName="totalAllowedTimeForVerification" type="number" step="1" min="60">
|
||||
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('required')">
|
||||
{{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('pattern')
|
||||
|| twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('min')">
|
||||
{{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.retry-verification-code-period</mat-label>
|
||||
<input matInput required formControlName="minVerificationCodeSendPeriod" type="number" step="1" min="5">
|
||||
<mat-error *ngIf="twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('required')">
|
||||
{{ 'admin.2fa.retry-verification-code-period-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('pattern')
|
||||
|| twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('min')">
|
||||
{{ 'admin.2fa.retry-verification-code-period-pattern' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.max-verification-failures-before-user-lockout</mat-label>
|
||||
<input matInput formControlName="maxVerificationFailuresBeforeUserLockout" type="number" step="1" min="0" max="65535">
|
||||
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('pattern')
|
||||
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('min')
|
||||
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')">
|
||||
{{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-expansion-panel class="provider">
|
||||
<div class="tb-form-panel no-padding no-border">
|
||||
<div class="tb-form-panel stroked tb-slide-toggle">
|
||||
<mat-expansion-panel class="tb-settings no-padding-bottom">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title class="flex items-center justify-start">
|
||||
<mat-slide-toggle (mousedown)="toggleExtensionPanel($event, providersForm.length, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
|
||||
formControlName="verificationCodeCheckRateLimitEnable">
|
||||
<mat-panel-title>
|
||||
<mat-slide-toggle class="mat-slide flex items-center justify-start"
|
||||
(mousedown)="toggleExtensionPanel($event, 0, twoFaFormGroup.get('enforceTwoFa').value)"
|
||||
formControlName="enforceTwoFa">
|
||||
{{ 'admin.2fa.force-2fa' | translate }}
|
||||
</mat-slide-toggle>
|
||||
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<div class="flex flex-row xs:flex-col gt-xs:gap-2">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.number-of-checking-attempts</mat-label>
|
||||
<input matInput formControlName="verificationCodeCheckRateLimitNumber" required type="number" step="1" min="1">
|
||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')">
|
||||
{{ 'admin.2fa.number-of-checking-attempts-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern')
|
||||
|| twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')">
|
||||
{{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }}
|
||||
</mat-error>
|
||||
<section class="tb-form-panel no-padding no-border" formGroupName="enforcedUsersFilter">
|
||||
<mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label translate>admin.2fa.enforce-for</mat-label>
|
||||
<mat-select formControlName="type">
|
||||
<mat-option *ngFor="let type of notificationTargetConfigTypes" [value]="type">
|
||||
{{ notificationTargetConfigTypeInfoMap.get(type).name | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.2fa.within-time</mat-label>
|
||||
<input matInput formControlName="verificationCodeCheckRateLimitTime" required type="number" step="1" min="1">
|
||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('required')">
|
||||
{{ 'admin.2fa.within-time-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('pattern')
|
||||
|| twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('min')">
|
||||
{{ 'admin.2fa.within-time-pattern' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<section class="tb-form-panel no-padding no-border" *ngIf="twoFaFormGroup.get('enforcedUsersFilter.type').value === notificationTargetConfigType.TENANT_ADMINISTRATORS">
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<tb-toggle-select class="tb-notification-tenant-group" appearance="fill"
|
||||
formControlName="filterByTenants">
|
||||
<tb-toggle-option [value]="true">{{ 'tenant.tenant' | translate }}</tb-toggle-option>
|
||||
<tb-toggle-option [value]="false">{{ 'tenant-profile.tenant-profile' | translate }}</tb-toggle-option>
|
||||
</tb-toggle-select>
|
||||
</div>
|
||||
<ng-container *ngIf="twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value; else tenantProfiles">
|
||||
<tb-entity-list
|
||||
formControlName="tenantsIds"
|
||||
subscriptSizing="dynamic"
|
||||
appearance="outline"
|
||||
labelText="{{ 'tenant.tenants' | translate }}"
|
||||
placeholderText="{{ 'tenant.tenants' | translate }}"
|
||||
hint="{{ 'notification.tenants-list-rule-hint' | translate }}"
|
||||
[entityType]="entityType.TENANT">
|
||||
</tb-entity-list>
|
||||
</ng-container>
|
||||
<ng-template #tenantProfiles>
|
||||
<tb-entity-list
|
||||
formControlName="tenantProfilesIds"
|
||||
subscriptSizing="dynamic"
|
||||
appearance="outline"
|
||||
labelText="{{ 'tenant-profile.tenant-profiles' | translate }}"
|
||||
placeholderText="{{ 'tenant-profile.tenant-profiles' | translate }}"
|
||||
hint="{{ 'notification.tenant-profiles-list-rule-hint' | translate }}"
|
||||
[entityType]="entityType.TENANT_PROFILE">
|
||||
</tb-entity-list>
|
||||
</ng-template>
|
||||
</section>
|
||||
</section>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<section class="tb-form-panel stroked" formArrayName="providers">
|
||||
<div class="tb-form-panel-title" translate>admin.2fa.available-providers</div>
|
||||
<ng-container *ngFor="let provider of providersForm.controls; let i = index; trackBy: trackByElement">
|
||||
<div class="tb-form-panel stroked tb-slide-toggle">
|
||||
<mat-expansion-panel class="tb-settings" [formGroupName]="i">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-slide-toggle class="mat-slide flex items-center justify-start"
|
||||
(mousedown)="toggleExtensionPanel($event, i, provider.get('enable').value)"
|
||||
formControlName="enable">
|
||||
{{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }}
|
||||
</mat-slide-toggle>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<ng-template matExpansionPanelContent>
|
||||
<ng-container [ngSwitch]="provider.get('providerType').value">
|
||||
<ng-container *ngSwitchCase="twoFactorAuthProviderType.TOTP">
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label translate>admin.2fa.issuer-name</mat-label>
|
||||
<input matInput formControlName="issuerName" required>
|
||||
<mat-error *ngIf="provider.get('issuerName').hasError('required') ||
|
||||
provider.get('issuerName').hasError('pattern')">
|
||||
{{ "admin.2fa.issuer-name-required" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
<div *ngSwitchCase="twoFactorAuthProviderType.SMS"
|
||||
>
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label translate>admin.2fa.verification-message-template</mat-label>
|
||||
<input matInput formControlName="smsVerificationMessageTemplate" required>
|
||||
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('required')">
|
||||
{{ "admin.2fa.verification-message-template-required" | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('pattern')">
|
||||
{{ "admin.2fa.verification-message-template-pattern" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<tb-time-unit-input
|
||||
appearance="outline"
|
||||
subscriptSizing="dynamic"
|
||||
required
|
||||
labelText="{{ 'admin.2fa.verification-code-lifetime' | translate }}"
|
||||
requiredText="{{ 'admin.2fa.verification-code-lifetime-required' | translate }}"
|
||||
minErrorText="{{ 'admin.2fa.verification-code-lifetime-pattern' | translate }}"
|
||||
[minTime]="1"
|
||||
formControlName="verificationCodeLifetime">
|
||||
</tb-time-unit-input>
|
||||
</div>
|
||||
<div *ngSwitchCase="twoFactorAuthProviderType.EMAIL">
|
||||
<tb-time-unit-input
|
||||
appearance="outline"
|
||||
subscriptSizing="dynamic"
|
||||
required
|
||||
labelText="{{ 'admin.2fa.verification-code-lifetime' | translate }}"
|
||||
requiredText="{{ 'admin.2fa.verification-code-lifetime-required' | translate }}"
|
||||
minErrorText="{{ 'admin.2fa.verification-code-lifetime-pattern' | translate }}"
|
||||
[minTime]="1"
|
||||
formControlName="verificationCodeLifetime">
|
||||
</tb-time-unit-input>
|
||||
</div>
|
||||
<div *ngSwitchCase="twoFactorAuthProviderType.BACKUP_CODE">
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label translate>admin.2fa.number-of-codes</mat-label>
|
||||
<input matInput formControlName="codesQuantity" type="number" step="1" min="1" required>
|
||||
<mat-error *ngIf="provider.get('codesQuantity').hasError('required')">
|
||||
{{ "admin.2fa.number-of-codes-required" | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="provider.get('codesQuantity').hasError('min') ||
|
||||
provider.get('codesQuantity').hasError('pattern')">
|
||||
{{ "admin.2fa.number-of-codes-pattern" | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
</ng-container>
|
||||
</section>
|
||||
<section class="tb-form-panel stroked mb-4">
|
||||
<div class="tb-form-panel-title" translate>admin.2fa.verification-limitations</div>
|
||||
<div class="tb-form-panel no-gap no-border no-padding">
|
||||
<div class="input-row flex flex-col">
|
||||
<tb-time-unit-input
|
||||
appearance="outline"
|
||||
required
|
||||
labelText="{{ 'admin.2fa.total-allowed-time-for-verification' | translate }}"
|
||||
requiredText="{{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }}"
|
||||
minErrorText="{{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }}"
|
||||
[minTime]="60"
|
||||
formControlName="totalAllowedTimeForVerification">
|
||||
</tb-time-unit-input>
|
||||
<tb-time-unit-input
|
||||
appearance="outline"
|
||||
required
|
||||
labelText="{{ 'admin.2fa.retry-verification-code-period' | translate }}"
|
||||
requiredText="{{ 'admin.2fa.retry-verification-code-period-required' | translate }}"
|
||||
minErrorText="{{ 'admin.2fa.retry-verification-code-period-pattern' | translate }}"
|
||||
[minTime]="5"
|
||||
formControlName="minVerificationCodeSendPeriod">
|
||||
</tb-time-unit-input>
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label translate>admin.2fa.max-verification-failures-before-user-lockout</mat-label>
|
||||
<input matInput formControlName="maxVerificationFailuresBeforeUserLockout" type="number" step="1" min="0" max="65535">
|
||||
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('pattern')
|
||||
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('min')
|
||||
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')">
|
||||
{{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="tb-form-panel stroked tb-slide-toggle">
|
||||
<mat-expansion-panel class="tb-settings">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<mat-slide-toggle class="mat-slide flex items-center justify-start" (mousedown)="toggleExtensionPanel($event, providersForm.length, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
|
||||
formControlName="verificationCodeCheckRateLimitEnable">
|
||||
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}
|
||||
</mat-slide-toggle>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<div class="flex flex-col">
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label translate>admin.2fa.number-of-checking-attempts</mat-label>
|
||||
<input matInput formControlName="verificationCodeCheckRateLimitNumber" required type="number" step="1" min="1">
|
||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')">
|
||||
{{ 'admin.2fa.number-of-checking-attempts-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern')
|
||||
|| twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')">
|
||||
{{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<tb-time-unit-input
|
||||
appearance="outline"
|
||||
subscriptSizing="dynamic"
|
||||
required
|
||||
labelText="{{ 'admin.2fa.within-time' | translate }}"
|
||||
requiredText="{{ 'admin.2fa.within-time-required' | translate }}"
|
||||
minErrorText="{{ 'admin.2fa.within-time-pattern' | translate }}"
|
||||
[minTime]="1"
|
||||
formControlName="verificationCodeCheckRateLimitTime">
|
||||
</tb-time-unit-input>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="flex flex-row items-center justify-end gap-2">
|
||||
<button mat-button mat-raised-button color="primary"
|
||||
|
||||
@ -71,13 +71,6 @@
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
|
||||
.mat-mdc-form-field {
|
||||
.mat-mdc-form-field-infix {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-expansion-panel {
|
||||
.mat-expansion-panel-content {
|
||||
font-size: 16px;
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
|
||||
import { Component, DestroyRef, OnInit, QueryList, ViewChildren } from '@angular/core';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
||||
import { Store } from '@ngrx/store';
|
||||
@ -28,32 +28,40 @@ import {
|
||||
TwoFactorAuthSettings,
|
||||
TwoFactorAuthSettingsForm
|
||||
} from '@shared/models/two-factor-auth.models';
|
||||
import { isNotEmptyStr } from '@core/utils';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { isDefined, isNotEmptyStr } from '@core/utils';
|
||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||
import { NotificationTargetConfigType, NotificationTargetConfigTypeInfoMap } from '@shared/models/notification.models';
|
||||
import { EntityType } from '@shared/models/entity-type.models';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-2fa-settings',
|
||||
templateUrl: './two-factor-auth-settings.component.html',
|
||||
styleUrls: [ './settings-card.scss', './two-factor-auth-settings.component.scss']
|
||||
})
|
||||
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
|
||||
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
|
||||
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)];
|
||||
|
||||
twoFaFormGroup: UntypedFormGroup;
|
||||
twoFactorAuthProviderType = TwoFactorAuthProviderType;
|
||||
twoFactorAuthProvidersData = twoFactorAuthProvidersData;
|
||||
|
||||
notificationTargetConfigType = NotificationTargetConfigType;
|
||||
notificationTargetConfigTypes: NotificationTargetConfigType[] = this.allowNotificationTargetConfigTypes();
|
||||
notificationTargetConfigTypeInfoMap = NotificationTargetConfigTypeInfoMap;
|
||||
|
||||
filterByTenants: boolean;
|
||||
entityType = EntityType;
|
||||
|
||||
showMainLoadingBar = false;
|
||||
|
||||
@ViewChildren(MatExpansionPanel) expansionPanel: QueryList<MatExpansionPanel>;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private twoFaService: TwoFactorAuthenticationService,
|
||||
private fb: UntypedFormBuilder) {
|
||||
private fb: UntypedFormBuilder,
|
||||
private destroyRef: DestroyRef) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
@ -64,12 +72,6 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
confirmForm(): UntypedFormGroup {
|
||||
return this.twoFaFormGroup;
|
||||
}
|
||||
@ -80,7 +82,10 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
this.joinRateLimit(setting, 'verificationCodeCheckRateLimit');
|
||||
const providers = setting.providers.filter(provider => provider.enable);
|
||||
providers.forEach(provider => delete provider.enable);
|
||||
const config = Object.assign(setting, {providers});
|
||||
const enforcedUsersFilter = this.twoFaFormGroup.get('enforcedUsersFilter').value;
|
||||
delete enforcedUsersFilter.filterByTenants;
|
||||
const config = Object.assign(setting, {providers}, {enforcedUsersFilter});
|
||||
this.filterByTenants = this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value;
|
||||
this.twoFaService.saveTwoFaSettings(config).subscribe(
|
||||
(settings) => {
|
||||
this.setAuthConfigFormValue(settings);
|
||||
@ -117,6 +122,13 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
|
||||
private build2faSettingsForm(): void {
|
||||
this.twoFaFormGroup = this.fb.group({
|
||||
enforceTwoFa: [false],
|
||||
enforcedUsersFilter: this.fb.group({
|
||||
type: [NotificationTargetConfigType.ALL_USERS],
|
||||
filterByTenants: [true],
|
||||
tenantsIds: [],
|
||||
tenantProfilesIds: []
|
||||
}),
|
||||
maxVerificationFailuresBeforeUserLockout: [30, [
|
||||
Validators.pattern(/^\d*$/),
|
||||
Validators.min(0),
|
||||
@ -137,7 +149,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
this.buildProvidersSettingsForm(provider);
|
||||
});
|
||||
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
|
||||
takeUntil(this.destroy$)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(value => {
|
||||
if (value) {
|
||||
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
|
||||
@ -148,7 +160,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
}
|
||||
});
|
||||
this.providersForm.valueChanges.pipe(
|
||||
takeUntil(this.destroy$)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe((value: TwoFactorAuthProviderConfigForm[]) => {
|
||||
const activeProvider = value.filter(provider => provider.enable);
|
||||
const indexBackupCode = Object.values(TwoFactorAuthProviderType).indexOf(TwoFactorAuthProviderType.BACKUP_CODE);
|
||||
@ -161,6 +173,15 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
this.providersForm.at(indexBackupCode).get('enable').enable( {emitEvent: false});
|
||||
}
|
||||
});
|
||||
this.twoFaFormGroup.get('enforceTwoFa').valueChanges.pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(value => {
|
||||
if (value) {
|
||||
this.twoFaFormGroup.get('enforcedUsersFilter').enable({emitEvent: false});
|
||||
} else {
|
||||
this.twoFaFormGroup.get('enforcedUsersFilter').disable({emitEvent: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setAuthConfigFormValue(settings: TwoFactorAuthSettings) {
|
||||
@ -172,19 +193,24 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
verificationCodeCheckRateLimitTime: checkRateLimitTime || 900,
|
||||
providers: []
|
||||
});
|
||||
if (settings?.enforceTwoFa) {
|
||||
this.getByIndexPanel(0).open();
|
||||
}
|
||||
if (checkRateLimitNumber > 0) {
|
||||
this.getByIndexPanel(this.providersForm.length).open();
|
||||
this.getByIndexPanel(this.providersForm.length+1).open();
|
||||
}
|
||||
Object.values(TwoFactorAuthProviderType).forEach((provider, index) => {
|
||||
const findIndex = allowProvidersConfig.indexOf(provider);
|
||||
if (findIndex > -1) {
|
||||
processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true}));
|
||||
this.getByIndexPanel(index).open();
|
||||
this.getByIndexPanel(index+1).open();
|
||||
} else {
|
||||
processFormValue.providers.push({enable: false});
|
||||
}
|
||||
});
|
||||
this.twoFaFormGroup.patchValue(processFormValue);
|
||||
this.filterByTenants = isDefined(this.filterByTenants) ? this.filterByTenants : !Array.isArray(settings?.enforcedUsersFilter.tenantProfilesIds);
|
||||
this.twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').patchValue(this.filterByTenants, {onlySelf: true});
|
||||
}
|
||||
|
||||
private buildProvidersSettingsForm(provider: TwoFactorAuthProviderType) {
|
||||
@ -212,7 +238,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
}
|
||||
const newProviders = this.fb.group(formControlConfig);
|
||||
newProviders.get('enable').valueChanges.pipe(
|
||||
takeUntil(this.destroy$)
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(value => {
|
||||
if (value) {
|
||||
newProviders.enable({emitEvent: false});
|
||||
@ -245,4 +271,12 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
delete processFormValue[`${property}Number`];
|
||||
delete processFormValue[`${property}Time`];
|
||||
}
|
||||
|
||||
private allowNotificationTargetConfigTypes(): NotificationTargetConfigType[] {
|
||||
return [
|
||||
NotificationTargetConfigType.ALL_USERS,
|
||||
NotificationTargetConfigType.TENANT_ADMINISTRATORS,
|
||||
NotificationTargetConfigType.SYSTEM_ADMINISTRATORS
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,19 @@
|
||||
<form [formGroup]="totpConfigForm" class="flex flex-col items-center justify-start" (ngSubmit)="onSaveConfig()">
|
||||
<p class="mat-body qr-code-description" translate>security.2fa.dialog.scan-qr-code</p>
|
||||
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
|
||||
<p class="mat-body qr-code-description" translate>login.enter-key-manually</p>
|
||||
<div class="flex flex-row items-center w-full overflow-hidden max-w-[375px]">
|
||||
<span tbTruncateWithTooltip class="w-full">{{ totpAuthURLSecret }}</span>
|
||||
<tb-copy-button
|
||||
class="attribute-copy"
|
||||
[disabled]="isLoading$ | async"
|
||||
[copyText]="totpAuthURLSecret"
|
||||
tooltipText="{{ 'attribute.copy-key' | translate }}"
|
||||
tooltipPosition="above"
|
||||
icon="content_copy"
|
||||
[style]="{'font-size': '24px'}">
|
||||
</tb-copy-button>
|
||||
</div>
|
||||
<p class="mat-body qr-code-description" style="margin-top: 30px;" translate>security.2fa.dialog.enter-verification-code</p>
|
||||
<mat-form-field class="mat-block code-container flex-1">
|
||||
<input matInput formControlName="verificationCode"
|
||||
|
||||
@ -42,6 +42,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
|
||||
|
||||
totpConfigForm: UntypedFormGroup;
|
||||
totpAuthURL: string;
|
||||
totpAuthURLSecret: string;
|
||||
|
||||
@ViewChild('stepper', {static: false}) stepper: MatStepper;
|
||||
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
|
||||
@ -55,6 +56,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
|
||||
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => {
|
||||
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
|
||||
this.totpAuthURL = this.authAccountConfig.authUrl;
|
||||
this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret');
|
||||
this.authAccountConfig.useByDefault = true;
|
||||
import('qrcode').then((QRCode) => {
|
||||
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);
|
||||
|
||||
@ -25,6 +25,7 @@ import { CreatePasswordComponent } from '@modules/login/pages/login/create-passw
|
||||
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
|
||||
import { Authority } from '@shared/models/authority.enum';
|
||||
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
|
||||
import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -83,6 +84,16 @@ const routes: Routes = [
|
||||
},
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'login/force-mfa',
|
||||
component: ForceTwoFactorAuthLoginComponent,
|
||||
data: {
|
||||
title: 'login.two-factor-authentication',
|
||||
auth: [Authority.MFA_CONFIGURATION_TOKEN],
|
||||
module: 'public'
|
||||
},
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'activationLinkExpired',
|
||||
component: LinkExpiredComponent,
|
||||
|
||||
@ -25,6 +25,7 @@ import { ResetPasswordComponent } from '@modules/login/pages/login/reset-passwor
|
||||
import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component';
|
||||
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
|
||||
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
|
||||
import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -33,7 +34,8 @@ import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.co
|
||||
ResetPasswordComponent,
|
||||
CreatePasswordComponent,
|
||||
TwoFactorAuthLoginComponent,
|
||||
LinkExpiredComponent
|
||||
LinkExpiredComponent,
|
||||
ForceTwoFactorAuthLoginComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@ -0,0 +1,304 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2025 The Thingsboard Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<div class="tb-two-factor-auth-login-content mat-app-background tb-dark flex flex-row items-center justify-center"
|
||||
style="width: 100%;">
|
||||
@switch (state()) {
|
||||
@case (ForceTwoFAState.SETUP) {
|
||||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start">
|
||||
<button mat-icon-button type="button" (click)="cancelLogin()">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
{{ (config ? 'login.two-fa' :'login.two-fa-required') | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="providers-container tb-default flex flex-col gap-2">
|
||||
<p class="mat-body"> {{ (config ? 'login.set-up-verification-method-login' :'login.set-up-verification-method') | translate }}</p>
|
||||
@for (provider of allowProviders; track provider) {
|
||||
<button type="button" [disabled]="config?.configs?.[provider]" mat-stroked-button class="provider" (click)="updateState(provider)">
|
||||
<mat-icon class="tb-mat-18" svgIcon="{{ providersData.get(provider).icon }}"></mat-icon>
|
||||
{{ providersData.get(provider).name | translate }}
|
||||
</button>
|
||||
}
|
||||
@if (config) {
|
||||
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="cancelLogin()">
|
||||
{{ 'login.login' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
@case (ForceTwoFAState.AUTHENTICATOR_APP) {
|
||||
@switch (appState()) {
|
||||
@case (ProvidersState.INPUT) {
|
||||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start">
|
||||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
{{ 'login.enable-authenticator-app' | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
<p class="mat-body qr-code-description mb-4" translate>login.scan-qr-code</p>
|
||||
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
|
||||
<p class="mat-body qr-code-description" translate>login.enter-key-manually</p>
|
||||
<div class="flex flex-row items-center mb-8 w-full overflow-hidden">
|
||||
<span tbTruncateWithTooltip class="w-full">{{ totpAuthURLSecret }}</span>
|
||||
<tb-copy-button
|
||||
class="attribute-copy"
|
||||
[disabled]="isLoading$ | async"
|
||||
[copyText]="totpAuthURLSecret"
|
||||
tooltipText="{{ 'login.copy-key' | translate }}"
|
||||
tooltipPosition="above"
|
||||
icon="content_copy"
|
||||
[style]="{'font-size': '24px', color: 'rgba(255,255,255,.8)'}"
|
||||
>
|
||||
</tb-copy-button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-start gap-2 w-full">
|
||||
<button type="button" mat-stroked-button class="navigation w-full" (click)="appState.set(ProvidersState.ENTER_CODE)">
|
||||
{{ 'login.continue' | translate }}
|
||||
</button>
|
||||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(TwoFactorAuthProviderType.TOTP)">
|
||||
{{ 'login.try-another-way' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
@case (ProvidersState.ENTER_CODE) {
|
||||
<ng-container *ngTemplateOutlet="enterCodeTemplateCard; context: {providerType: TwoFactorAuthProviderType.TOTP}"></ng-container>
|
||||
}
|
||||
@case (ProvidersState.SUCCESS) {
|
||||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.TOTP}"></ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
@case (ForceTwoFAState.SMS) {
|
||||
@switch (smsState()) {
|
||||
@case (ProvidersState.INPUT) {
|
||||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start">
|
||||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
{{ 'login.enable-authenticator-sms' | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
<form [formGroup]="smsConfigForm" class="mb-12">
|
||||
<p class="mat-body step-description input" translate>login.sms-description</p>
|
||||
<div class="flex flex-row items-center justify-between gap-3.75">
|
||||
<tb-phone-input class="flex-1"
|
||||
label="{{ 'login.phone-input.phone-input-label' | translate }}"
|
||||
hint="login.phone-input.phone-input-hint"
|
||||
requiredErrorText="{{ 'login.phone-input.phone-input-required' | translate }}"
|
||||
validationErrorText="{{ 'login.phone-input.phone-input-validation' | translate }}"
|
||||
formControlName="phone"
|
||||
[floatLabel]="'auto'">
|
||||
</tb-phone-input>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex flex-col items-center justify-start gap-2 w-full">
|
||||
<button type="button" mat-stroked-button [disabled]="(isLoading$ | async) || smsConfigForm.invalid || !smsConfigForm.dirty" class="navigation w-full" (click)="sendSmsCode()">
|
||||
{{ 'login.send-code' | translate }}
|
||||
</button>
|
||||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(TwoFactorAuthProviderType.SMS)">
|
||||
{{ 'login.try-another-way' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
@case (ProvidersState.ENTER_CODE) {
|
||||
<ng-container *ngTemplateOutlet="enterCodeTemplateCard; context: {providerType: TwoFactorAuthProviderType.SMS}"></ng-container>
|
||||
}
|
||||
@case (ProvidersState.SUCCESS) {
|
||||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.SMS}"></ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
@case (ForceTwoFAState.EMAIL) {
|
||||
@switch (emailState()) {
|
||||
@case (ProvidersState.INPUT) {
|
||||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start">
|
||||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
{{ 'login.enable-authenticator-email' | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
<form [formGroup]="emailConfigForm" class="mb-8">
|
||||
<p class="mat-body step-description input" translate>login.email-description</p>
|
||||
<mat-form-field class="mat-block input-container flex-1">
|
||||
<input matInput formControlName="email"
|
||||
type="email" required
|
||||
placeholder="{{ 'login.email-label' | translate }}" />
|
||||
<mat-error *ngIf="emailConfigForm.get('email').hasError('required')">
|
||||
{{ 'login.email-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="emailConfigForm.get('email').hasError('email')">
|
||||
{{ 'login.invalid-email-format' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<div class="flex flex-col items-center justify-start gap-2 w-full">
|
||||
<button type="button" mat-stroked-button [disabled]="(isLoading$ | async) || emailConfigForm.invalid" class="navigation w-full" (click)="sendEmailCode()">
|
||||
{{ 'login.send-code' | translate }}
|
||||
</button>
|
||||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(TwoFactorAuthProviderType.EMAIL)">
|
||||
{{ 'login.try-another-way' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
@case (ProvidersState.ENTER_CODE) {
|
||||
<ng-container *ngTemplateOutlet="enterCodeTemplateCard; context: {providerType: TwoFactorAuthProviderType.EMAIL}"></ng-container>
|
||||
}
|
||||
@case (ProvidersState.SUCCESS) {
|
||||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.EMAIL}"></ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
@case (ForceTwoFAState.BACKUP_CODE) {
|
||||
@switch (backupCodeState()) {
|
||||
@case (BackupCodeState.CODE) {
|
||||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start">
|
||||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
{{ 'login.get-backup-code' | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div mat-dialog-content tb-toast class="backup-code">
|
||||
<p class="mat-body-2 description" translate>login.backup-code-description</p>
|
||||
<div class="container">
|
||||
@for (code of backupCode?.codes; track code) {
|
||||
<div class="code">{{ code }}</div>
|
||||
}
|
||||
</div>
|
||||
<div class="action-buttons flex flex-row items-center justify-start gap-4">
|
||||
<button type="button" mat-flat-button class="provider w-full" (click)="downloadFile()">
|
||||
{{ 'login.download-txt' | translate }}
|
||||
</button>
|
||||
<button type="button" mat-stroked-button class="provider w-full" (click)="printCode()">
|
||||
{{ 'login.print' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mat-body-2 description" translate>login.backup-code-warn</p>
|
||||
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="backupCodeState.set(BackupCodeState.SUCCESS)">
|
||||
{{ 'login.continue' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
@case (BackupCodeState.SUCCESS) {
|
||||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.BACKUP_CODE}"></ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template #enterCodeTemplateCard let-providerType="providerType">
|
||||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start">
|
||||
<button mat-icon-button type="button" (click)="goBackByType(providerType)">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
{{ twoFactorAuthProvidersEnterCodeCardTranslate.get(providerType).name | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p class="mat-body inline-block">
|
||||
{{ twoFactorAuthProvidersEnterCodeCardTranslate.get(providerType).description | translate }}
|
||||
@if (providerType === TwoFactorAuthProviderType.SMS) {
|
||||
<span>{{ smsConfigForm.get('phone').value }}</span>
|
||||
}
|
||||
@if (providerType === TwoFactorAuthProviderType.EMAIL) {
|
||||
<span>{{ emailConfigForm.get('email').value }}</span>
|
||||
}
|
||||
</p>
|
||||
<form [formGroup]="configForm" class="flex flex-col items-center justify-start">
|
||||
<mat-form-field class="mat-block w-full">
|
||||
<input matInput formControlName="verificationCode"
|
||||
maxlength="6" type="text" required
|
||||
inputmode="numeric" pattern="[0-9]*"
|
||||
autocomplete="off"
|
||||
placeholder="{{ 'login.verification-code' | translate }}">
|
||||
<mat-error *ngIf="configForm.get('verificationCode').invalid">
|
||||
{{ 'login.verification-code-invalid' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<div class="flex flex-col items-center justify-start gap-2 w-full">
|
||||
<button type="button" mat-flat-button color="accent" [disabled]="(isLoading$ | async) || configForm.invalid || !configForm.dirty" class="navigation w-full" (click)="saveConfig(providerType)">
|
||||
{{ 'login.confirm' | translate }}
|
||||
</button>
|
||||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(providerType)">
|
||||
{{ 'login.try-another-way' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
<ng-template #successTemplateCard let-providerType="providerType">
|
||||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start pl-10">
|
||||
{{ twoFactorAuthProvidersSuccessCardTranslate.get(providerType).name | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p class="mat-body mb-16" translate>{{ twoFactorAuthProvidersSuccessCardTranslate.get(providerType).description | translate }}</p>
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
<div class="flex flex-col items-center justify-start gap-2 w-full">
|
||||
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="cancelLogin()">
|
||||
{{ 'login.login' | translate }}
|
||||
</button>
|
||||
@if (isAnyProviderAvailable) {
|
||||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(providerType)">
|
||||
{{ 'login.add-verification-method' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</ng-template>
|
||||
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright © 2016-2025 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@import '../../../../../scss/constants';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.tb-two-factor-auth-login-content {
|
||||
background-color: #eee;
|
||||
|
||||
.tb-two-factor-auth-login-card {
|
||||
max-height: 100vh;
|
||||
overflow: auto;
|
||||
padding: 48px 48px 48px 16px;
|
||||
|
||||
@media #{$mat-xs} {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media #{$mat-gt-xs} {
|
||||
width: 450px !important;
|
||||
}
|
||||
|
||||
.mat-mdc-card-title {
|
||||
font: 400 28px / 36px Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.mat-mdc-card-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mat-mdc-card-content {
|
||||
margin-top: 34px;
|
||||
margin-left: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mat-body {
|
||||
letter-spacing: 0.25px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.backup-code {
|
||||
p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.container {
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
gap: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
justify-items: center;
|
||||
padding: 16px 0;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.code {
|
||||
letter-spacing: 0.25px;
|
||||
font-family: Roboto Mono, "Helvetica Neue", monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.tb-two-factor-auth-login-content {
|
||||
.tb-two-factor-auth-login-card {
|
||||
button.mat-mdc-icon-button {
|
||||
.mat-icon {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
.mat-mdc-form-field .mat-mdc-form-field-hint-wrapper {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
button.provider, button.navigation {
|
||||
text-align: start;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
&:not([disabled][disabled]) {
|
||||
border-color: rgba(255, 255, 255, .8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,300 @@
|
||||
///
|
||||
/// Copyright © 2016-2025 The Thingsboard Authors
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, ElementRef, OnDestroy, OnInit, signal, ViewChild } from '@angular/core';
|
||||
import { AuthService } from '@core/auth/auth.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
|
||||
import {
|
||||
AccountTwoFaSettings,
|
||||
BackupCodeTwoFactorAuthAccountConfig,
|
||||
TotpTwoFactorAuthAccountConfig,
|
||||
TwoFactorAuthAccountConfig,
|
||||
twoFactorAuthProvidersEnterCodeCardTranslate,
|
||||
twoFactorAuthProvidersLoginData,
|
||||
twoFactorAuthProvidersSuccessCardTranslate,
|
||||
TwoFactorAuthProviderType
|
||||
} from '@shared/models/two-factor-auth.models';
|
||||
import { phoneNumberPattern } from '@shared/models/settings.models';
|
||||
import { deepClone, isDefinedAndNotNull, unwrapModule } from '@core/utils';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||
import printTemplate from '@home/pages/security/authentication-dialog/backup-code-print-template.raw';
|
||||
import { ImportExportService } from '@shared/import-export/import-export.service';
|
||||
import { mergeMap, tap } from 'rxjs/operators';
|
||||
|
||||
enum ForceTwoFAState {
|
||||
SETUP = 'setup',
|
||||
AUTHENTICATOR_APP = 'authenticatorApp',
|
||||
SMS = 'sms',
|
||||
EMAIL = 'email',
|
||||
BACKUP_CODE = 'backupCode',
|
||||
}
|
||||
|
||||
enum ProvidersState {
|
||||
INPUT = 'INPUT',
|
||||
ENTER_CODE = 'ENTER_CODE',
|
||||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
enum BackupCodeState {
|
||||
CODE = 'CODE',
|
||||
SUCCESS = 'SUCCESS',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'tb-force-two-factor-auth-login',
|
||||
templateUrl: './force-two-factor-auth-login.component.html',
|
||||
styleUrls: ['./force-two-factor-auth-login.component.scss']
|
||||
})
|
||||
export class ForceTwoFactorAuthLoginComponent extends PageComponent implements OnInit, OnDestroy {
|
||||
|
||||
TwoFactorAuthProviderType = TwoFactorAuthProviderType;
|
||||
providersData = twoFactorAuthProvidersLoginData;
|
||||
allowProviders: TwoFactorAuthProviderType[] = [];
|
||||
config: AccountTwoFaSettings;
|
||||
|
||||
twoFactorAuthProvidersEnterCodeCardTranslate = twoFactorAuthProvidersEnterCodeCardTranslate;
|
||||
twoFactorAuthProvidersSuccessCardTranslate = twoFactorAuthProvidersSuccessCardTranslate;
|
||||
|
||||
ForceTwoFAState = ForceTwoFAState;
|
||||
ProvidersState = ProvidersState;
|
||||
BackupCodeState = BackupCodeState
|
||||
|
||||
state = signal<ForceTwoFAState>(ForceTwoFAState.SETUP);
|
||||
appState = signal<ProvidersState>(ProvidersState.INPUT);
|
||||
smsState = signal<ProvidersState>(ProvidersState.INPUT);
|
||||
emailState = signal<ProvidersState>(ProvidersState.INPUT);
|
||||
backupCodeState = signal<BackupCodeState>(BackupCodeState.CODE);
|
||||
|
||||
totpAuthURL: string;
|
||||
totpAuthURLSecret: string;
|
||||
backupCode: BackupCodeTwoFactorAuthAccountConfig;
|
||||
|
||||
configForm: UntypedFormGroup;
|
||||
smsConfigForm: UntypedFormGroup;
|
||||
emailConfigForm: UntypedFormGroup;
|
||||
|
||||
private providersInfo: TwoFactorAuthProviderType[];
|
||||
private authAccountConfig: TwoFactorAuthAccountConfig;
|
||||
private useByDefault: boolean = true;
|
||||
|
||||
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private authService: AuthService,
|
||||
private twoFaService: TwoFactorAuthenticationService,
|
||||
private importExportService: ImportExportService,
|
||||
public dialog: MatDialog,
|
||||
public dialogService: DialogService,
|
||||
private fb: UntypedFormBuilder) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.providersInfo = this.authService.forceTwoFactorAuthProviders;
|
||||
this.allowedProviders();
|
||||
this.configForm = this.fb.group({
|
||||
verificationCode: ['', [
|
||||
Validators.required,
|
||||
Validators.minLength(6),
|
||||
Validators.maxLength(6),
|
||||
Validators.pattern(/^\d*$/)
|
||||
]]
|
||||
});
|
||||
|
||||
this.smsConfigForm = this.fb.group({
|
||||
phone: ['', [Validators.required, Validators.pattern(phoneNumberPattern)]]
|
||||
});
|
||||
|
||||
this.emailConfigForm = this.fb.group({
|
||||
email: [getCurrentAuthUser(this.store).sub, [Validators.required, Validators.email]]
|
||||
});
|
||||
|
||||
this.twoFaService.getAccountTwoFaSettings().subscribe(accountConfig => {
|
||||
if (accountConfig) {
|
||||
this.config = accountConfig;
|
||||
this.useByDefault = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goBackByType(type: TwoFactorAuthProviderType) {
|
||||
switch (type) {
|
||||
case TwoFactorAuthProviderType.TOTP:
|
||||
this.appState.set(ProvidersState.INPUT);
|
||||
this.updateQRCode();
|
||||
break;
|
||||
case TwoFactorAuthProviderType.SMS:
|
||||
this.smsState.set(ProvidersState.INPUT);
|
||||
break;
|
||||
case TwoFactorAuthProviderType.EMAIL:
|
||||
this.emailState.set(ProvidersState.INPUT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
get isAnyProviderAvailable() {
|
||||
return this.config?.configs ? Object.keys(this.config?.configs)?.length < this.allowProviders?.length : true;
|
||||
}
|
||||
|
||||
private allowedProviders() {
|
||||
if (isDefinedAndNotNull(this.config)) {
|
||||
this.allowProviders = this.providersInfo;
|
||||
} else {
|
||||
this.allowProviders = this.providersInfo.filter(provider => provider !== TwoFactorAuthProviderType.BACKUP_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
updateState(type: TwoFactorAuthProviderType) {
|
||||
switch (type) {
|
||||
case TwoFactorAuthProviderType.TOTP:
|
||||
this.state.set(ForceTwoFAState.AUTHENTICATOR_APP);
|
||||
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => {
|
||||
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
|
||||
this.totpAuthURL = this.authAccountConfig.authUrl;
|
||||
this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret');
|
||||
this.authAccountConfig.useByDefault = this.useByDefault;
|
||||
this.useByDefault = false;
|
||||
this.updateQRCode();
|
||||
});
|
||||
break;
|
||||
case TwoFactorAuthProviderType.SMS:
|
||||
this.state.set(ForceTwoFAState.SMS);
|
||||
break;
|
||||
case TwoFactorAuthProviderType.EMAIL:
|
||||
this.state.set(ForceTwoFAState.EMAIL);
|
||||
break;
|
||||
case TwoFactorAuthProviderType.BACKUP_CODE:
|
||||
this.state.set(ForceTwoFAState.BACKUP_CODE);
|
||||
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.BACKUP_CODE).pipe(
|
||||
tap((data: BackupCodeTwoFactorAuthAccountConfig) => this.backupCode = data),
|
||||
mergeMap(data => this.twoFaService.verifyAndSaveTwoFaAccountConfig(data, null, {ignoreLoading: true}))
|
||||
).subscribe((config) => {
|
||||
this.config = config;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sendSmsCode() {
|
||||
if (this.smsConfigForm.valid) {
|
||||
this.authAccountConfig = {
|
||||
providerType: TwoFactorAuthProviderType.SMS,
|
||||
useByDefault: this.useByDefault,
|
||||
phoneNumber: this.smsConfigForm.get('phone').value as string
|
||||
};
|
||||
this.useByDefault = false;
|
||||
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.smsState.set(ProvidersState.ENTER_CODE));
|
||||
}
|
||||
}
|
||||
|
||||
sendEmailCode() {
|
||||
if (this.emailConfigForm.valid) {
|
||||
this.authAccountConfig = {
|
||||
providerType: TwoFactorAuthProviderType.EMAIL,
|
||||
useByDefault: this.useByDefault,
|
||||
email: this.emailConfigForm.get('email').value as string
|
||||
};
|
||||
this.useByDefault = false;
|
||||
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.emailState.set(ProvidersState.ENTER_CODE));
|
||||
}
|
||||
}
|
||||
|
||||
tryAnotherWay(type: TwoFactorAuthProviderType) {
|
||||
this.state.set(ForceTwoFAState.SETUP);
|
||||
this.configForm.reset();
|
||||
switch (type) {
|
||||
case TwoFactorAuthProviderType.TOTP:
|
||||
this.appState.set(ProvidersState.INPUT);
|
||||
break;
|
||||
case TwoFactorAuthProviderType.SMS:
|
||||
this.smsState.set(ProvidersState.INPUT);
|
||||
this.smsConfigForm.reset();
|
||||
break;
|
||||
case TwoFactorAuthProviderType.EMAIL:
|
||||
this.emailState.set(ProvidersState.INPUT)
|
||||
this.emailConfigForm.get('email').reset(getCurrentAuthUser(this.store).sub);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
saveConfig(type: TwoFactorAuthProviderType) {
|
||||
if (this.configForm.valid) {
|
||||
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
|
||||
this.configForm.get('verificationCode').value).subscribe((config) => {
|
||||
switch (type) {
|
||||
case TwoFactorAuthProviderType.TOTP:
|
||||
this.appState.set(ProvidersState.SUCCESS);
|
||||
break;
|
||||
case TwoFactorAuthProviderType.SMS:
|
||||
this.smsState.set(ProvidersState.SUCCESS);
|
||||
break;
|
||||
case TwoFactorAuthProviderType.EMAIL:
|
||||
this.emailState.set(ProvidersState.SUCCESS);
|
||||
break;
|
||||
}
|
||||
this.config = config;
|
||||
this.authAccountConfig = null;
|
||||
this.allowedProviders();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateQRCode() {
|
||||
import('qrcode').then((QRCode) => {
|
||||
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);
|
||||
this.canvasRef.nativeElement.style.width = 'auto';
|
||||
this.canvasRef.nativeElement.style.height = 'auto';
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
}
|
||||
|
||||
cancelLogin() {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
this.importExportService.exportText(this.backupCode.codes, 'backup-codes');
|
||||
}
|
||||
|
||||
printCode() {
|
||||
const codeTemplate = deepClone(this.backupCode.codes)
|
||||
.map(code => `<div class="code-row"><input type="checkbox"><span class="code">${code}</span></div>`).join('');
|
||||
const printPage = printTemplate.replace('${codesBlock}', codeTemplate);
|
||||
const newWindow = window.open('', 'Print backup code');
|
||||
|
||||
newWindow.document.open();
|
||||
newWindow.document.write(printPage);
|
||||
|
||||
setTimeout(() => {
|
||||
newWindow.print();
|
||||
|
||||
newWindow.document.close();
|
||||
|
||||
setTimeout(() => {
|
||||
newWindow.close();
|
||||
}, 10);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@ -72,9 +72,19 @@
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.tb-two-factor-auth-login-content {
|
||||
.tb-two-factor-auth-login-card {
|
||||
button.mat-mdc-icon-button {
|
||||
.mat-icon {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
button.provider {
|
||||
text-align: start;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
&:not([disabled][disabled]) {
|
||||
border-color: rgba(255, 255, 255, .8);
|
||||
}
|
||||
|
||||
@ -37,12 +37,12 @@
|
||||
(focus)="focus()"
|
||||
autocomplete="off"
|
||||
[required]="required">
|
||||
<mat-hint innerHTML="{{ 'phone-input.phone-input-hint' | translate: {phoneNumber: phonePlaceholder} }}"></mat-hint>
|
||||
<mat-hint innerHTML="{{ hint | translate: {phoneNumber: phonePlaceholder} }}"></mat-hint>
|
||||
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('required')">
|
||||
{{ 'phone-input.phone-input-required' | translate }}
|
||||
{{ requiredErrorText }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('invalidPhoneNumber')">
|
||||
{{ 'phone-input.phone-input-validation' | translate }}
|
||||
{{ validationErrorText }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@ -77,6 +77,15 @@ export class PhoneInputComponent implements OnInit, ControlValueAccessor, Valida
|
||||
@Input()
|
||||
label = this.translate.instant('phone-input.phone-input-label');
|
||||
|
||||
@Input()
|
||||
hint = 'phone-input.phone-input-hint';
|
||||
|
||||
@Input()
|
||||
requiredErrorText = this.translate.instant('phone-input.phone-input-required');
|
||||
|
||||
@Input()
|
||||
validationErrorText = this.translate.instant('phone-input.phone-input-validation');
|
||||
|
||||
get showFlagSelect(): boolean {
|
||||
return this.enableFlagsSelect && !this.isLegacy;
|
||||
}
|
||||
|
||||
@ -20,5 +20,6 @@ export enum Authority {
|
||||
CUSTOMER_USER = 'CUSTOMER_USER',
|
||||
REFRESH_TOKEN = 'REFRESH_TOKEN',
|
||||
ANONYMOUS = 'ANONYMOUS',
|
||||
PRE_VERIFICATION_TOKEN = 'PRE_VERIFICATION_TOKEN'
|
||||
PRE_VERIFICATION_TOKEN = 'PRE_VERIFICATION_TOKEN',
|
||||
MFA_CONFIGURATION_TOKEN = 'MFA_CONFIGURATION_TOKEN'
|
||||
}
|
||||
|
||||
@ -14,7 +14,11 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { UsersFilter } from '@shared/models/notification.models';
|
||||
|
||||
export interface TwoFactorAuthSettings {
|
||||
enforceTwoFa: boolean;
|
||||
enforcedUsersFilter: UsersFilter;
|
||||
maxVerificationFailuresBeforeUserLockout: number;
|
||||
providers: Array<TwoFactorAuthProviderConfig>;
|
||||
totalAllowedTimeForVerification: number;
|
||||
@ -24,12 +28,18 @@ export interface TwoFactorAuthSettings {
|
||||
}
|
||||
|
||||
export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{
|
||||
enforceTwoFa: boolean;
|
||||
enforcedUsersFilter: UsersFilterWithFilterByTenant;
|
||||
providers: Array<TwoFactorAuthProviderConfigForm>;
|
||||
verificationCodeCheckRateLimitEnable: boolean;
|
||||
verificationCodeCheckRateLimitNumber: number;
|
||||
verificationCodeCheckRateLimitTime: number;
|
||||
}
|
||||
|
||||
export interface UsersFilterWithFilterByTenant extends UsersFilter{
|
||||
filterByTenants?: boolean;
|
||||
}
|
||||
|
||||
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
|
||||
EmailTwoFactorAuthProviderConfig>;
|
||||
|
||||
@ -183,3 +193,61 @@ export const twoFactorAuthProvidersLoginData = new Map<TwoFactorAuthProviderType
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
export const twoFactorAuthProvidersEnterCodeCardTranslate = new Map<TwoFactorAuthProviderType, Omit<TwoFactorAuthProviderData, 'activatedHint'>>(
|
||||
[
|
||||
[
|
||||
TwoFactorAuthProviderType.TOTP, {
|
||||
name: 'login.enable-authenticator-app',
|
||||
description: 'login.enable-authenticator-app-description'
|
||||
}
|
||||
],
|
||||
[
|
||||
TwoFactorAuthProviderType.SMS, {
|
||||
name: 'login.enable-authenticator-sms',
|
||||
description: 'login.enable-authenticator-sms-description'
|
||||
}
|
||||
],
|
||||
[
|
||||
TwoFactorAuthProviderType.EMAIL, {
|
||||
name: 'login.enable-authenticator-email',
|
||||
description: 'login.enable-authenticator-email-description'
|
||||
}
|
||||
],
|
||||
[
|
||||
TwoFactorAuthProviderType.BACKUP_CODE, {
|
||||
name: 'security.2fa.provider.backup_code',
|
||||
description: 'login.backup-code-auth-description'
|
||||
}
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
export const twoFactorAuthProvidersSuccessCardTranslate = new Map<TwoFactorAuthProviderType, Omit<TwoFactorAuthProviderData, 'activatedHint'>>(
|
||||
[
|
||||
[
|
||||
TwoFactorAuthProviderType.TOTP, {
|
||||
name: 'login.authenticator-app-success',
|
||||
description: 'login.authenticator-app-success-description'
|
||||
}
|
||||
],
|
||||
[
|
||||
TwoFactorAuthProviderType.SMS, {
|
||||
name: 'login.authenticator-sms-success',
|
||||
description: 'login.authenticator-sms-success-description'
|
||||
}
|
||||
],
|
||||
[
|
||||
TwoFactorAuthProviderType.EMAIL, {
|
||||
name: 'login.authenticator-email-success',
|
||||
description: 'login.authenticator-email-success-description'
|
||||
}
|
||||
],
|
||||
[
|
||||
TwoFactorAuthProviderType.BACKUP_CODE, {
|
||||
name: 'login.authenticator-backup-code-success',
|
||||
description: 'login.authenticator-backup-code-success-description'
|
||||
}
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
@ -496,24 +496,26 @@
|
||||
"number-of-codes-pattern": "Number of codes must be a positive integer.",
|
||||
"number-of-codes-required": "Number of codes is required.",
|
||||
"provider": "Provider",
|
||||
"retry-verification-code-period": "Retry verification code period (sec)",
|
||||
"retry-verification-code-period": "Retry verification code period",
|
||||
"retry-verification-code-period-pattern": "Minimal period time is 5 sec",
|
||||
"retry-verification-code-period-required": "Retry verification code period is required.",
|
||||
"total-allowed-time-for-verification": "Total allowed time for verification (sec)",
|
||||
"total-allowed-time-for-verification": "Total allowed time for verification",
|
||||
"total-allowed-time-for-verification-pattern": "Minimal total allowed time is 60 sec",
|
||||
"total-allowed-time-for-verification-required": "Total allowed time is required.",
|
||||
"use-system-two-factor-auth-settings": "Use system two factor auth settings",
|
||||
"verification-code-check-rate-limit": "Verification code check rate limit",
|
||||
"verification-code-lifetime": "Verification code lifetime (sec)",
|
||||
"verification-code-lifetime": "Verification code lifetime",
|
||||
"verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.",
|
||||
"verification-code-lifetime-required": "Verification code lifetime is required.",
|
||||
"verification-message-template": "Verification message template",
|
||||
"verification-limitations": "Verification limitations",
|
||||
"verification-message-template-pattern": "Verification message need to contains pattern: ${code}",
|
||||
"verification-message-template-required": "Verification message template is required.",
|
||||
"within-time": "Within time (sec)",
|
||||
"within-time": "Within time",
|
||||
"within-time-pattern": "Time must be a positive integer.",
|
||||
"within-time-required": "Time is required."
|
||||
"within-time-required": "Time is required.",
|
||||
"force-2fa": "Force two-factor authentication",
|
||||
"enforce-for": "Enforce for"
|
||||
},
|
||||
"jwt": {
|
||||
"security-settings": "JWT security settings",
|
||||
@ -3884,7 +3886,48 @@
|
||||
"activation-link-expired": "Activation link has expired",
|
||||
"activation-link-expired-message": "The link to activate your profile has expired. You can return to the login page to receive a new email.",
|
||||
"reset-password-link-expired": "Password reset link has expired",
|
||||
"reset-password-link-expired-message": "The link to reset your password has expired. You can return to the login page to receive a new email."
|
||||
"reset-password-link-expired-message": "The link to reset your password has expired. You can return to the login page to receive a new email.",
|
||||
"two-fa": "Two-factor authentication",
|
||||
"two-fa-required": "Two-factor authentication is required",
|
||||
"set-up-verification-method": "Set up a verification method to continue",
|
||||
"set-up-verification-method-login": "Set up a verification method or login",
|
||||
"enable-authenticator-app": "Enable authenticator app",
|
||||
"enable-authenticator-app-description": "Please enter the security code from your authenticator app",
|
||||
"enable-authenticator-sms": "Enable SMS authenticator",
|
||||
"enable-authenticator-sms-description": "Enter a 6-digit code we just sent to ",
|
||||
"enable-authenticator-email": "Enable email authenticator",
|
||||
"enable-authenticator-email-description": "A security code has been sent to your email address at ",
|
||||
"enter-key-manually": "or enter this 32-digits key manually:",
|
||||
"continue": "Continue",
|
||||
"confirm": "Confirm",
|
||||
"authenticator-app-success": "Authenticator app successfully enabled",
|
||||
"authenticator-app-success-description": "The next time you log in, you will need to provide a two-factor authentication code",
|
||||
"authenticator-sms-success": "SMS authenticator successfully enabled",
|
||||
"authenticator-sms-success-description": "The next time you log in, you will be prompted to enter the security code that will be sent to the phone number",
|
||||
"authenticator-email-success": "Email authenticator successfully enabled",
|
||||
"authenticator-email-success-description": "The next time you log in, you will be prompted to enter the security code that will be sent to your email address",
|
||||
"authenticator-backup-code-success": "Backup code successfully enabled",
|
||||
"authenticator-backup-code-success-description": "The next time you log in, you will be prompted to enter the security code or use one of backup code.",
|
||||
"add-verification-method": "Add verification method",
|
||||
"get-backup-code": "Get backup code",
|
||||
"copy-key": "Copy key",
|
||||
"send-code": "Send code",
|
||||
"email-label": "Email",
|
||||
"sms-description": "Enter a phone number to use as your authenticator.",
|
||||
"backup-code-description": "Print out the codes so you have them handy when you need to use them to log in to your account. You can use each backup code once.",
|
||||
"backup-code-warn": "Once you leave this page, these codes cannot be shown again. Store them safely using the options below.",
|
||||
"download-txt": "Download (txt)",
|
||||
"print": "Print",
|
||||
"verification-code": "6-digit code",
|
||||
"verification-code-invalid": "Invalid verification code format",
|
||||
"scan-qr-code": "Scan this QR code with your verification app",
|
||||
"phone-input": {
|
||||
"phone-input-label": "Phone number",
|
||||
"phone-input-required": "Phone number is required",
|
||||
"phone-input-validation": "Phone number is invalid or not possible",
|
||||
"phone-input-pattern": "Invalid phone number. Should be in E.164 format, ex. {{phoneNumber}}",
|
||||
"phone-input-hint": "Phone Number in E.164 format, ex. {{phoneNumber}}"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"edit": "Edit",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user