diff --git a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts new file mode 100644 index 0000000000..d608d3b362 --- /dev/null +++ b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models'; + +@Injectable({ + providedIn: 'root' +}) +export class TwoFactorAuthenticationService { + + constructor( + private http: HttpClient + ) { + } + + getTwoFaSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/2fa/settings`, defaultHttpOptionsFromConfig(config)); + } + + saveTwoFaSettings(settings: TwoFactorAuthSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 4a52fc66f6..294513c782 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -108,7 +108,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '240px', + height: '280px', icon: 'settings', pages: [ { @@ -146,6 +146,14 @@ export class MenuService { path: '/settings/oauth2', icon: 'security' }, + { + id: guid(), + name: 'admin.2fa.2fa', + type: 'link', + path: '/settings/2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true + }, { id: guid(), name: 'resource.resources-library', @@ -216,6 +224,12 @@ export class MenuService { icon: 'security', path: '/settings/oauth2' }, + { + name: 'admin.2fa.2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true, + path: '/settings/2fa' + }, { name: 'resource.resources-library', icon: 'folder', diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index ddccbb4da4..e63eec7b52 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -32,6 +32,7 @@ import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/ import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -183,6 +184,20 @@ const routes: Routes = [ } } ] + }, + { + path: '2fa', + component: TwoFactorAuthSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'admin.2fa.2fa', + breadcrumb: { + label: 'admin.2fa.2fa', + icon: 'mdi:two-factor-authentication', + isMdiIcon: true + } + } } ] } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 326b16f950..37214d7ce5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; @NgModule({ declarations: @@ -39,7 +40,8 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources- SecuritySettingsComponent, OAuth2SettingsComponent, HomeSettingsComponent, - ResourcesLibraryComponent + ResourcesLibraryComponent, + TwoFactorAuthSettingsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html new file mode 100644 index 0000000000..9a507e45f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -0,0 +1,142 @@ +
+ + +
+ admin.2fa.2fa + + +
+
+ + +
+ +
+ + {{ 'admin.2fa.use-system-two-factor-auth-settings' | translate }} + + + + admin.2fa.total-allowed-time-for-verification + + + {{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }} + + + {{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }} + + + + admin.2fa.max-verification-failures-before-user-lockout + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }} + + + {{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }} + + + + admin.2fa.verification-code-send-rate-limit + + admin.2fa.verification-code-send-rate-limit-hint + + {{ 'admin.2fa.verification-code-send-rate-limit-pattern' | translate }} + + + + admin.2fa.verification-code-check-rate-limit + + admin.2fa.verification-code-check-rate-limit-hint + + {{ 'admin.2fa.verification-code-check-rate-limit-pattern' | translate }} + + +
Providers
+ +
+ + + + {{ provider.value.providerType }} + + + + + + + +
+
+
+ + admin.oauth2.login-provider + + + {{ provider }} + + + +
+ + admin.oauth2.allowed-platforms + + + {{ platformTypeTranslations.get(platform) | translate }} + + + +
+
+ + admin.oauth2.client-id + + + {{ 'admin.oauth2.client-id-required' | translate }} + + + {{ 'admin.oauth2.client-id-max-length' | translate }} + + + + + admin.oauth2.client-secret + + + {{ 'admin.oauth2.client-secret-required' | translate }} + + + {{ 'admin.oauth2.client-secret-max-length' | translate }} + + +
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts new file mode 100644 index 0000000000..1995605786 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -0,0 +1,155 @@ +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ActivatedRoute } from '@angular/router'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DialogService } from '@core/services/dialog.service'; +import { TranslateService } from '@ngx-translate/core'; +import { WINDOW } from '@core/services/window.service'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { AuthState } from '@core/auth/auth.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; +import { + TwoFactorAuthProviderType, + TwoFactorAuthSettings, + TwoFactorAuthSettingsForm +} from '@shared/models/two-factor-auth.models'; + +@Component({ + selector: 'tb-2fa-settings', + templateUrl: './two-factor-auth-settings.component.html', + styleUrls: ['./two-factor-auth-settings.component.scss', './settings-card.scss'] +}) +export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { + + private authState: AuthState = getCurrentAuthState(this.store); + private authUser = this.authState.authUser; + + twoFaFormGroup: FormGroup; + + constructor(protected store: Store, + private route: ActivatedRoute, + private twoFaService: TwoFactorAuthenticationService, + private fb: FormBuilder, + private dialogService: DialogService, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) { + super(store); + } + + ngOnInit() { + this.build2faSettingsForm(); + this.twoFaService.getTwoFaSettings().subscribe((setting) => { + console.log(this.formDataPreprocessing(setting)); + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + confirmForm(): FormGroup { + return this.twoFaFormGroup; + } + + isTenantAdmin(): boolean { + return this.authUser.authority === Authority.TENANT_ADMIN; + } + + save() { + + } + + private build2faSettingsForm(): void { + this.twoFaFormGroup = this.fb.group({ + useSystemTwoFactorAuthSettings: [false], + maxVerificationFailuresBeforeUserLockout: [30, [ + Validators.required, + Validators.pattern(/^\d*$/), + Validators.min(0), + Validators.max(65535) + ]], + totalAllowedTimeForVerification: [3600, [ + Validators.required, + Validators.min(1), + Validators.pattern(/^\d*$/) + ]], + verificationCodeCheckRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)], + verificationCodeSendRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)], + providers: this.fb.array([]) + }); + } + + addProviders() { + const newProviders = this.fb.group({ + providerType: [TwoFactorAuthProviderType.TOTP], + issuerName: ['', Validators.required], + smsVerificationMessageTemplate: [{ + value: 'Verification code: ${verificationCode}', + disabled: true + }, [ + Validators.required, + Validators.pattern(/\${verificationCode}/) + ]], + verificationCodeLifetime: [{ + value: 120, + disabled: true + }, [ + Validators.required, + Validators.min(1), + Validators.pattern(/^\d*$/) + ]] + }); + newProviders.get('providerType').valueChanges.subscribe(type => { + switch (type) { + case TwoFactorAuthProviderType.SMS: + newProviders.get('issuerName').disable({emitEvent: false}); + newProviders.get('smsVerificationMessageTemplate').enable({emitEvent: false}); + newProviders.get('verificationCodeLifetime').enable({emitEvent: false}); + break; + case TwoFactorAuthProviderType.TOTP: + newProviders.get('issuerName').enable({emitEvent: false}); + newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false}); + newProviders.get('verificationCodeLifetime').disable({emitEvent: false}); + break; + } + }); + if (this.providersForm.length) { + const selectProvidersType = this.providersForm.value[0].providerType; + if (selectProvidersType !== TwoFactorAuthProviderType.TOTP) { + newProviders.get('providerType').patchValue(TwoFactorAuthProviderType.SMS, {emitEvents: true}) + } + } + this.providersForm.push(newProviders); + } + + removeProviders($event: Event, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.providersForm.removeAt(index); + this.providersForm.markAsTouched(); + this.providersForm.markAsDirty(); + } + + get providersForm(): FormArray { + return this.twoFaFormGroup.get('providers') as FormArray; + } + + private formDataPreprocessing(data: TwoFactorAuthSettings): TwoFactorAuthSettingsForm { + return data; + } + + private formDataPostprocessing(data: TwoFactorAuthSettingsForm): TwoFactorAuthSettings{ + return data; + } + + trackByParams(index: number): number { + return index; + } + +} diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts new file mode 100644 index 0000000000..b2c32e6156 --- /dev/null +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -0,0 +1,30 @@ +export interface TwoFactorAuthSettings { + maxVerificationFailuresBeforeUserLockout: number; + providers: Array; + totalAllowedTimeForVerification: number; + useSystemTwoFactorAuthSettings: boolean; + verificationCodeCheckRateLimit: string; + verificationCodeSendRateLimit: string; +} + +export type TwoFactorAuthProviderConfig = Partial + +export interface TotpTwoFactorAuthProviderConfig { + providerType: TwoFactorAuthProviderType; + issuerName: string; +} + +export interface SmsTwoFactorAuthProviderConfig { + providerType: TwoFactorAuthProviderType; + smsVerificationMessageTemplate: string; + verificationCodeLifetime: number; +} + +export enum TwoFactorAuthProviderType{ + TOTP = 'TOTP', + SMS = 'SMS' +} + +export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings { + +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index aab433c992..642592b669 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -252,6 +252,22 @@ "platform-ios": "iOS", "all-platforms": "All platforms", "allowed-platforms": "Allowed platforms" + }, + "2fa": { + "2fa": "Two-factor authentication", + "use-system-two-factor-auth-settings": "Use system two factor auth settings", + "total-allowed-time-for-verification": "Total allowed time for verification", + "total-allowed-time-for-verification-required": "Total allowed time is required.", + "total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.", + "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", + "max-verification-failures-before-user-lockout-required": "Max verification failures is required.", + "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", + "verification-code-check-rate-limit": "Verification code check rate limit", + "verification-code-check-rate-limit-hint": "If empty field, the limit not be apply", + "verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format", + "verification-code-send-rate-limit": "Verification code send rate limit", + "verification-code-send-rate-limit-hint": "If empty field, the limit not be apply", + "verification-code-send-rate-limit-pattern": "Verification code send limit has invalid format" } }, "alarm": {