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;
|
redirectUrl: string;
|
||||||
oauth2Clients: Array<OAuth2ClientLoginInfo> = null;
|
oauth2Clients: Array<OAuth2ClientLoginInfo> = null;
|
||||||
twoFactorAuthProviders: Array<TwoFaProviderInfo> = null;
|
twoFactorAuthProviders: Array<TwoFaProviderInfo> = null;
|
||||||
|
forceTwoFactorAuthProviders: Array<TwoFactorAuthProviderType> = null;
|
||||||
|
|
||||||
private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
|
private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
|
||||||
private jwtHelper = new JwtHelperService();
|
private jwtHelper = new JwtHelperService();
|
||||||
@ -117,6 +118,9 @@ export class AuthService {
|
|||||||
if (loginResponse.scope === Authority.PRE_VERIFICATION_TOKEN) {
|
if (loginResponse.scope === Authority.PRE_VERIFICATION_TOKEN) {
|
||||||
this.router.navigateByUrl(`login/mfa`);
|
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 {
|
public forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean {
|
||||||
if (authState && authState.authUser) {
|
if (authState && authState.authUser) {
|
||||||
if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) {
|
if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) {
|
||||||
@ -266,6 +279,8 @@ export class AuthService {
|
|||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {
|
if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {
|
||||||
result = this.router.parseUrl('login/mfa');
|
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)) {
|
} else if (!path || path === 'login' || this.forceDefaultPlace(authState, path, params)) {
|
||||||
if (this.redirectUrl) {
|
if (this.redirectUrl) {
|
||||||
const redirectUrl = this.redirectUrl;
|
const redirectUrl = this.redirectUrl;
|
||||||
@ -399,7 +414,7 @@ export class AuthService {
|
|||||||
loadUserSubject.error(err);
|
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.next(authPayload);
|
||||||
loadUserSubject.complete();
|
loadUserSubject.complete();
|
||||||
} else if (authPayload.authUser?.userId) {
|
} else if (authPayload.authUser?.userId) {
|
||||||
|
|||||||
@ -104,6 +104,16 @@ export class AuthGuard {
|
|||||||
}
|
}
|
||||||
this.authService.logout();
|
this.authService.logout();
|
||||||
return of(this.authService.defaultUrl(false));
|
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 {
|
} else {
|
||||||
return of(true);
|
return of(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,163 +30,216 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<form [formGroup]="twoFaFormGroup" (ngSubmit)="save()">
|
<form [formGroup]="twoFaFormGroup" (ngSubmit)="save()">
|
||||||
<fieldset [disabled]="isLoading$ | async">
|
<fieldset [disabled]="isLoading$ | async">
|
||||||
<div>
|
<div class="tb-form-panel no-padding no-border">
|
||||||
<fieldset class="fields-group" formArrayName="providers">
|
<div class="tb-form-panel stroked tb-slide-toggle">
|
||||||
<legend class="group-title" translate>admin.2fa.available-providers</legend>
|
<mat-expansion-panel class="tb-settings no-padding-bottom">
|
||||||
<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">
|
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title class="flex items-center justify-start">
|
<mat-panel-title>
|
||||||
<mat-slide-toggle (mousedown)="toggleExtensionPanel($event, providersForm.length, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
|
<mat-slide-toggle class="mat-slide flex items-center justify-start"
|
||||||
formControlName="verificationCodeCheckRateLimitEnable">
|
(mousedown)="toggleExtensionPanel($event, 0, twoFaFormGroup.get('enforceTwoFa').value)"
|
||||||
|
formControlName="enforceTwoFa">
|
||||||
|
{{ 'admin.2fa.force-2fa' | translate }}
|
||||||
</mat-slide-toggle>
|
</mat-slide-toggle>
|
||||||
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}
|
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<ng-template matExpansionPanelContent>
|
<ng-template matExpansionPanelContent>
|
||||||
<div class="flex flex-row xs:flex-col gt-xs:gap-2">
|
<section class="tb-form-panel no-padding no-border" formGroupName="enforcedUsersFilter">
|
||||||
<mat-form-field class="mat-block flex-1">
|
<mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic">
|
||||||
<mat-label translate>admin.2fa.number-of-checking-attempts</mat-label>
|
<mat-label translate>admin.2fa.enforce-for</mat-label>
|
||||||
<input matInput formControlName="verificationCodeCheckRateLimitNumber" required type="number" step="1" min="1">
|
<mat-select formControlName="type">
|
||||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')">
|
<mat-option *ngFor="let type of notificationTargetConfigTypes" [value]="type">
|
||||||
{{ 'admin.2fa.number-of-checking-attempts-required' | translate }}
|
{{ notificationTargetConfigTypeInfoMap.get(type).name | translate }}
|
||||||
</mat-error>
|
</mat-option>
|
||||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern')
|
</mat-select>
|
||||||
|| twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')">
|
|
||||||
{{ 'admin.2fa.number-of-checking-attempts-pattern' | translate }}
|
|
||||||
</mat-error>
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="mat-block flex-1">
|
<section class="tb-form-panel no-padding no-border" *ngIf="twoFaFormGroup.get('enforcedUsersFilter.type').value === notificationTargetConfigType.TENANT_ADMINISTRATORS">
|
||||||
<mat-label translate>admin.2fa.within-time</mat-label>
|
<div class="flex flex-1 items-center justify-center">
|
||||||
<input matInput formControlName="verificationCodeCheckRateLimitTime" required type="number" step="1" min="1">
|
<tb-toggle-select class="tb-notification-tenant-group" appearance="fill"
|
||||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('required')">
|
formControlName="filterByTenants">
|
||||||
{{ 'admin.2fa.within-time-required' | translate }}
|
<tb-toggle-option [value]="true">{{ 'tenant.tenant' | translate }}</tb-toggle-option>
|
||||||
</mat-error>
|
<tb-toggle-option [value]="false">{{ 'tenant-profile.tenant-profile' | translate }}</tb-toggle-option>
|
||||||
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('pattern')
|
</tb-toggle-select>
|
||||||
|| twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('min')">
|
</div>
|
||||||
{{ 'admin.2fa.within-time-pattern' | translate }}
|
<ng-container *ngIf="twoFaFormGroup.get('enforcedUsersFilter.filterByTenants').value; else tenantProfiles">
|
||||||
</mat-error>
|
<tb-entity-list
|
||||||
</mat-form-field>
|
formControlName="tenantsIds"
|
||||||
</div>
|
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>
|
</ng-template>
|
||||||
</mat-expansion-panel>
|
</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>
|
||||||
<div class="flex flex-row items-center justify-end gap-2">
|
<div class="flex flex-row items-center justify-end gap-2">
|
||||||
<button mat-button mat-raised-button color="primary"
|
<button mat-button mat-raised-button color="primary"
|
||||||
|
|||||||
@ -71,13 +71,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep {
|
:host ::ng-deep {
|
||||||
|
|
||||||
.mat-mdc-form-field {
|
|
||||||
.mat-mdc-form-field-infix {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-expansion-panel {
|
.mat-expansion-panel {
|
||||||
.mat-expansion-panel-content {
|
.mat-expansion-panel-content {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
/// limitations under the License.
|
/// 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 { PageComponent } from '@shared/components/page.component';
|
||||||
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@ -28,32 +28,40 @@ import {
|
|||||||
TwoFactorAuthSettings,
|
TwoFactorAuthSettings,
|
||||||
TwoFactorAuthSettingsForm
|
TwoFactorAuthSettingsForm
|
||||||
} from '@shared/models/two-factor-auth.models';
|
} from '@shared/models/two-factor-auth.models';
|
||||||
import { isNotEmptyStr } from '@core/utils';
|
import { isDefined, isNotEmptyStr } from '@core/utils';
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
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({
|
@Component({
|
||||||
selector: 'tb-2fa-settings',
|
selector: 'tb-2fa-settings',
|
||||||
templateUrl: './two-factor-auth-settings.component.html',
|
templateUrl: './two-factor-auth-settings.component.html',
|
||||||
styleUrls: [ './settings-card.scss', './two-factor-auth-settings.component.scss']
|
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*$/)];
|
private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)];
|
||||||
|
|
||||||
twoFaFormGroup: UntypedFormGroup;
|
twoFaFormGroup: UntypedFormGroup;
|
||||||
twoFactorAuthProviderType = TwoFactorAuthProviderType;
|
twoFactorAuthProviderType = TwoFactorAuthProviderType;
|
||||||
twoFactorAuthProvidersData = twoFactorAuthProvidersData;
|
twoFactorAuthProvidersData = twoFactorAuthProvidersData;
|
||||||
|
|
||||||
|
notificationTargetConfigType = NotificationTargetConfigType;
|
||||||
|
notificationTargetConfigTypes: NotificationTargetConfigType[] = this.allowNotificationTargetConfigTypes();
|
||||||
|
notificationTargetConfigTypeInfoMap = NotificationTargetConfigTypeInfoMap;
|
||||||
|
|
||||||
|
filterByTenants: boolean;
|
||||||
|
entityType = EntityType;
|
||||||
|
|
||||||
showMainLoadingBar = false;
|
showMainLoadingBar = false;
|
||||||
|
|
||||||
@ViewChildren(MatExpansionPanel) expansionPanel: QueryList<MatExpansionPanel>;
|
@ViewChildren(MatExpansionPanel) expansionPanel: QueryList<MatExpansionPanel>;
|
||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
private twoFaService: TwoFactorAuthenticationService,
|
private twoFaService: TwoFactorAuthenticationService,
|
||||||
private fb: UntypedFormBuilder) {
|
private fb: UntypedFormBuilder,
|
||||||
|
private destroyRef: DestroyRef) {
|
||||||
super(store);
|
super(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,12 +72,6 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
super.ngOnDestroy();
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmForm(): UntypedFormGroup {
|
confirmForm(): UntypedFormGroup {
|
||||||
return this.twoFaFormGroup;
|
return this.twoFaFormGroup;
|
||||||
}
|
}
|
||||||
@ -80,7 +82,10 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
this.joinRateLimit(setting, 'verificationCodeCheckRateLimit');
|
this.joinRateLimit(setting, 'verificationCodeCheckRateLimit');
|
||||||
const providers = setting.providers.filter(provider => provider.enable);
|
const providers = setting.providers.filter(provider => provider.enable);
|
||||||
providers.forEach(provider => delete 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(
|
this.twoFaService.saveTwoFaSettings(config).subscribe(
|
||||||
(settings) => {
|
(settings) => {
|
||||||
this.setAuthConfigFormValue(settings);
|
this.setAuthConfigFormValue(settings);
|
||||||
@ -117,6 +122,13 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
|
|
||||||
private build2faSettingsForm(): void {
|
private build2faSettingsForm(): void {
|
||||||
this.twoFaFormGroup = this.fb.group({
|
this.twoFaFormGroup = this.fb.group({
|
||||||
|
enforceTwoFa: [false],
|
||||||
|
enforcedUsersFilter: this.fb.group({
|
||||||
|
type: [NotificationTargetConfigType.ALL_USERS],
|
||||||
|
filterByTenants: [true],
|
||||||
|
tenantsIds: [],
|
||||||
|
tenantProfilesIds: []
|
||||||
|
}),
|
||||||
maxVerificationFailuresBeforeUserLockout: [30, [
|
maxVerificationFailuresBeforeUserLockout: [30, [
|
||||||
Validators.pattern(/^\d*$/),
|
Validators.pattern(/^\d*$/),
|
||||||
Validators.min(0),
|
Validators.min(0),
|
||||||
@ -137,7 +149,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
this.buildProvidersSettingsForm(provider);
|
this.buildProvidersSettingsForm(provider);
|
||||||
});
|
});
|
||||||
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
|
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe(value => {
|
).subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
|
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
|
||||||
@ -148,7 +160,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.providersForm.valueChanges.pipe(
|
this.providersForm.valueChanges.pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe((value: TwoFactorAuthProviderConfigForm[]) => {
|
).subscribe((value: TwoFactorAuthProviderConfigForm[]) => {
|
||||||
const activeProvider = value.filter(provider => provider.enable);
|
const activeProvider = value.filter(provider => provider.enable);
|
||||||
const indexBackupCode = Object.values(TwoFactorAuthProviderType).indexOf(TwoFactorAuthProviderType.BACKUP_CODE);
|
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.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) {
|
private setAuthConfigFormValue(settings: TwoFactorAuthSettings) {
|
||||||
@ -172,19 +193,24 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
verificationCodeCheckRateLimitTime: checkRateLimitTime || 900,
|
verificationCodeCheckRateLimitTime: checkRateLimitTime || 900,
|
||||||
providers: []
|
providers: []
|
||||||
});
|
});
|
||||||
|
if (settings?.enforceTwoFa) {
|
||||||
|
this.getByIndexPanel(0).open();
|
||||||
|
}
|
||||||
if (checkRateLimitNumber > 0) {
|
if (checkRateLimitNumber > 0) {
|
||||||
this.getByIndexPanel(this.providersForm.length).open();
|
this.getByIndexPanel(this.providersForm.length+1).open();
|
||||||
}
|
}
|
||||||
Object.values(TwoFactorAuthProviderType).forEach((provider, index) => {
|
Object.values(TwoFactorAuthProviderType).forEach((provider, index) => {
|
||||||
const findIndex = allowProvidersConfig.indexOf(provider);
|
const findIndex = allowProvidersConfig.indexOf(provider);
|
||||||
if (findIndex > -1) {
|
if (findIndex > -1) {
|
||||||
processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true}));
|
processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true}));
|
||||||
this.getByIndexPanel(index).open();
|
this.getByIndexPanel(index+1).open();
|
||||||
} else {
|
} else {
|
||||||
processFormValue.providers.push({enable: false});
|
processFormValue.providers.push({enable: false});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.twoFaFormGroup.patchValue(processFormValue);
|
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) {
|
private buildProvidersSettingsForm(provider: TwoFactorAuthProviderType) {
|
||||||
@ -212,7 +238,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
}
|
}
|
||||||
const newProviders = this.fb.group(formControlConfig);
|
const newProviders = this.fb.group(formControlConfig);
|
||||||
newProviders.get('enable').valueChanges.pipe(
|
newProviders.get('enable').valueChanges.pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe(value => {
|
).subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
newProviders.enable({emitEvent: false});
|
newProviders.enable({emitEvent: false});
|
||||||
@ -245,4 +271,12 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
delete processFormValue[`${property}Number`];
|
delete processFormValue[`${property}Number`];
|
||||||
delete processFormValue[`${property}Time`];
|
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()">
|
<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>
|
<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>
|
<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>
|
<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">
|
<mat-form-field class="mat-block code-container flex-1">
|
||||||
<input matInput formControlName="verificationCode"
|
<input matInput formControlName="verificationCode"
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
|
|||||||
|
|
||||||
totpConfigForm: UntypedFormGroup;
|
totpConfigForm: UntypedFormGroup;
|
||||||
totpAuthURL: string;
|
totpAuthURL: string;
|
||||||
|
totpAuthURLSecret: string;
|
||||||
|
|
||||||
@ViewChild('stepper', {static: false}) stepper: MatStepper;
|
@ViewChild('stepper', {static: false}) stepper: MatStepper;
|
||||||
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
|
@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.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => {
|
||||||
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
|
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
|
||||||
this.totpAuthURL = this.authAccountConfig.authUrl;
|
this.totpAuthURL = this.authAccountConfig.authUrl;
|
||||||
|
this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret');
|
||||||
this.authAccountConfig.useByDefault = true;
|
this.authAccountConfig.useByDefault = true;
|
||||||
import('qrcode').then((QRCode) => {
|
import('qrcode').then((QRCode) => {
|
||||||
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);
|
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 { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
|
||||||
import { Authority } from '@shared/models/authority.enum';
|
import { Authority } from '@shared/models/authority.enum';
|
||||||
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
|
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 = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -83,6 +84,16 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
canActivate: [AuthGuard]
|
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',
|
path: 'activationLinkExpired',
|
||||||
component: LinkExpiredComponent,
|
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 { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component';
|
||||||
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
|
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
|
||||||
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -33,7 +34,8 @@ import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.co
|
|||||||
ResetPasswordComponent,
|
ResetPasswordComponent,
|
||||||
CreatePasswordComponent,
|
CreatePasswordComponent,
|
||||||
TwoFactorAuthLoginComponent,
|
TwoFactorAuthLoginComponent,
|
||||||
LinkExpiredComponent
|
LinkExpiredComponent,
|
||||||
|
ForceTwoFactorAuthLoginComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
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 {
|
::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 {
|
button.provider {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
&:not([disabled][disabled]) {
|
&:not([disabled][disabled]) {
|
||||||
border-color: rgba(255, 255, 255, .8);
|
border-color: rgba(255, 255, 255, .8);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,12 +37,12 @@
|
|||||||
(focus)="focus()"
|
(focus)="focus()"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
[required]="required">
|
[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')">
|
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('required')">
|
||||||
{{ 'phone-input.phone-input-required' | translate }}
|
{{ requiredErrorText }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('invalidPhoneNumber')">
|
<mat-error *ngIf="phoneFormGroup.get('phoneNumber').hasError('invalidPhoneNumber')">
|
||||||
{{ 'phone-input.phone-input-validation' | translate }}
|
{{ validationErrorText }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -77,6 +77,15 @@ export class PhoneInputComponent implements OnInit, ControlValueAccessor, Valida
|
|||||||
@Input()
|
@Input()
|
||||||
label = this.translate.instant('phone-input.phone-input-label');
|
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 {
|
get showFlagSelect(): boolean {
|
||||||
return this.enableFlagsSelect && !this.isLegacy;
|
return this.enableFlagsSelect && !this.isLegacy;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,5 +20,6 @@ export enum Authority {
|
|||||||
CUSTOMER_USER = 'CUSTOMER_USER',
|
CUSTOMER_USER = 'CUSTOMER_USER',
|
||||||
REFRESH_TOKEN = 'REFRESH_TOKEN',
|
REFRESH_TOKEN = 'REFRESH_TOKEN',
|
||||||
ANONYMOUS = 'ANONYMOUS',
|
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.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
|
import { UsersFilter } from '@shared/models/notification.models';
|
||||||
|
|
||||||
export interface TwoFactorAuthSettings {
|
export interface TwoFactorAuthSettings {
|
||||||
|
enforceTwoFa: boolean;
|
||||||
|
enforcedUsersFilter: UsersFilter;
|
||||||
maxVerificationFailuresBeforeUserLockout: number;
|
maxVerificationFailuresBeforeUserLockout: number;
|
||||||
providers: Array<TwoFactorAuthProviderConfig>;
|
providers: Array<TwoFactorAuthProviderConfig>;
|
||||||
totalAllowedTimeForVerification: number;
|
totalAllowedTimeForVerification: number;
|
||||||
@ -24,12 +28,18 @@ export interface TwoFactorAuthSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{
|
export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{
|
||||||
|
enforceTwoFa: boolean;
|
||||||
|
enforcedUsersFilter: UsersFilterWithFilterByTenant;
|
||||||
providers: Array<TwoFactorAuthProviderConfigForm>;
|
providers: Array<TwoFactorAuthProviderConfigForm>;
|
||||||
verificationCodeCheckRateLimitEnable: boolean;
|
verificationCodeCheckRateLimitEnable: boolean;
|
||||||
verificationCodeCheckRateLimitNumber: number;
|
verificationCodeCheckRateLimitNumber: number;
|
||||||
verificationCodeCheckRateLimitTime: number;
|
verificationCodeCheckRateLimitTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UsersFilterWithFilterByTenant extends UsersFilter{
|
||||||
|
filterByTenants?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
|
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
|
||||||
EmailTwoFactorAuthProviderConfig>;
|
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-pattern": "Number of codes must be a positive integer.",
|
||||||
"number-of-codes-required": "Number of codes is required.",
|
"number-of-codes-required": "Number of codes is required.",
|
||||||
"provider": "Provider",
|
"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-pattern": "Minimal period time is 5 sec",
|
||||||
"retry-verification-code-period-required": "Retry verification code period is required.",
|
"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-pattern": "Minimal total allowed time is 60 sec",
|
||||||
"total-allowed-time-for-verification-required": "Total allowed time is required.",
|
"total-allowed-time-for-verification-required": "Total allowed time is required.",
|
||||||
"use-system-two-factor-auth-settings": "Use system two factor auth settings",
|
"use-system-two-factor-auth-settings": "Use system two factor auth settings",
|
||||||
"verification-code-check-rate-limit": "Verification code check rate limit",
|
"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-pattern": "Verification code lifetime must be a positive integer.",
|
||||||
"verification-code-lifetime-required": "Verification code lifetime is required.",
|
"verification-code-lifetime-required": "Verification code lifetime is required.",
|
||||||
"verification-message-template": "Verification message template",
|
"verification-message-template": "Verification message template",
|
||||||
"verification-limitations": "Verification limitations",
|
"verification-limitations": "Verification limitations",
|
||||||
"verification-message-template-pattern": "Verification message need to contains pattern: ${code}",
|
"verification-message-template-pattern": "Verification message need to contains pattern: ${code}",
|
||||||
"verification-message-template-required": "Verification message template is required.",
|
"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-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": {
|
"jwt": {
|
||||||
"security-settings": "JWT security settings",
|
"security-settings": "JWT security settings",
|
||||||
@ -3884,7 +3886,48 @@
|
|||||||
"activation-link-expired": "Activation link has expired",
|
"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.",
|
"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": "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": {
|
"markdown": {
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user