From 70cbe29a5ddcdb8e766f382ed1174dba97813ad0 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 6 May 2022 17:52:22 +0300 Subject: [PATCH] UI: Add 2FA profile setting form and added support Email 2FA provider --- .../http/two-factor-authentication.service.ts | 40 ++++++- .../two-factor-auth-settings.component.html | 25 ++++- .../two-factor-auth-settings.component.ts | 26 +++-- .../email-auth-dialog.component.html | 104 +++++++++++++++++ .../email-auth-dialog.component.scss | 15 +++ .../email-auth-dialog.component.ts | 93 ++++++++++++++++ .../sms-auth-dialog.component.html | 105 ++++++++++++++++++ .../sms-auth-dialog.component.scss | 15 +++ .../sms-auth-dialog.component.ts | 91 +++++++++++++++ .../totp-auth-dialog.component.html | 97 ++++++++++++++++ .../totp-auth-dialog.component.scss | 15 +++ .../totp-auth-dialog.component.ts | 80 +++++++++++++ .../pages/profile/profile-routing.module.ts | 19 +++- .../home/pages/profile/profile.component.html | 65 ++++++++++- .../home/pages/profile/profile.component.scss | 17 +++ .../home/pages/profile/profile.component.ts | 103 ++++++++++++++++- .../home/pages/profile/profile.module.ts | 8 +- ui-ngx/src/app/shared/models/constants.ts | 1 + .../shared/models/two-factor-auth.models.ts | 35 +++++- .../assets/locale/locale.constant-en_US.json | 5 +- 20 files changed, 932 insertions(+), 27 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts 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 index 230eddd208..36183db9d8 100644 --- a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts +++ b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts @@ -18,7 +18,12 @@ 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'; +import { + AccountTwoFaSettings, + TwoFactorAuthAccountConfig, + TwoFactorAuthProviderType, + TwoFactorAuthSettings +} from '@shared/models/two-factor-auth.models'; @Injectable({ providedIn: 'root' @@ -38,4 +43,37 @@ export class TwoFactorAuthenticationService { return this.http.post(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config)); } + getAvailableTwoFaProviders(config?: RequestConfig): Observable> { + return this.http.get>(`/api/2fa/providers`, defaultHttpOptionsFromConfig(config)); + } + + generateTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/account/config/generate?providerType=${providerType}`, + defaultHttpOptionsFromConfig(config)); + } + + getAccountTwoFaSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/2fa/account/settings`, defaultHttpOptionsFromConfig(config)); + } + + updateTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, useByDefault: boolean, + config?: RequestConfig): Observable { + return this.http.put(`/api/2fa/account/config?providerType=${providerType}`, {useByDefault}, + defaultHttpOptionsFromConfig(config)); + } + + submitTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/account/config/submit`, authConfig, defaultHttpOptionsFromConfig(config)); + } + + verifyAndSaveTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, verificationCode: number, + config?: RequestConfig): Observable { + return this.http.post(`/api/2fa/account/config?verificationCode=${verificationCode}`, authConfig, defaultHttpOptionsFromConfig(config)); + } + + deleteTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable { + return this.http.delete(`/api/2fa/account/config?providerType=${providerType}`, + defaultHttpOptionsFromConfig(config)); + } + } 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 index c92fe6de74..fccec76b54 100644 --- 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 @@ -81,7 +81,7 @@
- + {{ provider.value.providerType }} @@ -102,10 +102,10 @@ admin.2fa.provider - - {{ twoFactorAuthProviderType }} + + {{ provider }} @@ -144,6 +144,19 @@
+
+ + admin.2fa.verification-code-lifetime + + + {{ "admin.2fa.verification-code-lifetime-required" | translate }} + + + {{ "admin.2fa.verification-code-lifetime-pattern" | translate }} + + +
@@ -159,7 +172,7 @@ || providersForm.length == twoFactorAuthProviderTypes.length || (isLoading$ | async)" (click)="addProvider()"> add - action.add + admin.2fa.add-provider + + + +
+
+ + + done + + + Add email +
+ + user.email + + + {{ 'user.email-required' | translate }} + + + {{ 'user.invalid-email-format' | translate }} + + +
+ + +
+
+
+ + Authentication verification +
+

Enter the 6-digit code here

+

Enter the 6 digit verification code we just sent to {{ emailConfigForm.get('email').value }}.

+ + 6-digit code + + +
+ + +
+
+
+ + Two-factor authentication activated +
+

Email authentication enabled

+

If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to your email address.

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss new file mode 100644 index 0000000000..ec6f008a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2022 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. + */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts new file mode 100644 index 0000000000..be5976c78b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts @@ -0,0 +1,93 @@ +/// +/// Copyright © 2016-2022 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, Inject, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { MatStepper } from '@angular/material/stepper'; + +export interface EmailAuthDialogData { + email: string; +} + +@Component({ + selector: 'tb-email-auth-dialog', + templateUrl: './email-auth-dialog.component.html', + styleUrls: ['./email-auth-dialog.component.scss'] +}) +export class EmailAuthDialogComponent extends DialogComponent { + + private authAccountConfig: TwoFactorAuthAccountConfig; + + emailConfigForm: FormGroup; + emailVerificationForm: FormGroup; + + @ViewChild('stepper', {static: false}) stepper: MatStepper; + + constructor(protected store: Store, + protected router: Router, + private twoFaService: TwoFactorAuthenticationService, + @Inject(MAT_DIALOG_DATA) public data: EmailAuthDialogData, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.emailConfigForm = this.fb.group({ + email: [this.data.email, [Validators.required, Validators.email]] + }); + + this.emailVerificationForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + } + + nextStep() { + switch (this.stepper.selectedIndex) { + case 0: + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.EMAIL, + useByDefault: true, + email: this.emailConfigForm.get('email').value as string + }; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => { + this.stepper.next(); + }); + break; + case 1: + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.emailVerificationForm.get('verificationCode').value).subscribe(() => { + this.stepper.next(); + }); + break; + } + } + + closeDialog() { + return this.dialogRef.close(this.stepper.selectedIndex > 1); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html new file mode 100644 index 0000000000..03c6c7a465 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html @@ -0,0 +1,105 @@ + + +

Enable SMS authenticator

+ + +
+ + +
+
+ + + done + + + Add phone number +
+

Enter the phone number including the area code.

+ + Phone number + + + {{ 'admin.number-to-required' | translate }} + + + {{ 'admin.phone-number-pattern' | translate }} + + +
+ + +
+
+
+ + Authentication verification +
+

Enter the 6-digit code here

+

Enter the 6 digit verification code we just sent to {{ smsConfigForm.get('phone').value }}.

+ + 6-digit code + + +
+ + +
+
+
+ + Two-factor authentication activated +
+

SMS authentication enabled

+

If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to the phone number.

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss new file mode 100644 index 0000000000..ec6f008a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2022 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. + */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts new file mode 100644 index 0000000000..ed707922eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2022 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, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { phoneNumberPattern } from '@shared/models/settings.models'; +import { MatStepper } from '@angular/material/stepper'; + +@Component({ + selector: 'tb-sms-auth-dialog', + templateUrl: './sms-auth-dialog.component.html', + styleUrls: ['./sms-auth-dialog.component.scss'] +}) +export class SMSAuthDialogComponent extends DialogComponent { + + private authAccountConfig: TwoFactorAuthAccountConfig; + + phoneNumberPattern = phoneNumberPattern; + + smsConfigForm: FormGroup; + smsVerificationForm: FormGroup; + + @ViewChild('stepper', {static: false}) stepper: MatStepper; + + constructor(protected store: Store, + protected router: Router, + private twoFaService: TwoFactorAuthenticationService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.smsConfigForm = this.fb.group({ + phone: ['', [Validators.required, Validators.pattern(phoneNumberPattern)]] + }); + + this.smsVerificationForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + } + + nextStep() { + switch (this.stepper.selectedIndex) { + case 0: + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.SMS, + useByDefault: true, + phoneNumber: this.smsConfigForm.get('phone').value as string + }; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => { + this.stepper.next(); + }); + break; + case 1: + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.smsVerificationForm.get('verificationCode').value).subscribe(() => { + this.stepper.next(); + }); + break; + } + } + + closeDialog() { + return this.dialogRef.close(this.stepper.selectedIndex > 1); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html new file mode 100644 index 0000000000..09afbb2de0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html @@ -0,0 +1,97 @@ + + +

Enable authenticator app

+ + +
+ + +
+
+ + + done + + + Get app +
+

You'll need to use a verification app such as Google Authenticator, Authy, or Duo. Install from your app store

+
+ + +
+
+
+ + Authentication verification +
+

1. Scan this QR code with your verification app

+

Once your app reads the QR code, you'll get a 6-digit code.

+ +

2. Enter the 6-digit code here

+

Enter the code from the app below. Once connected, we'll remember your phone so you can use it each time you log in.

+ + 6-digit code + + +
+ + +
+
+
+ + Two-factor authentication activated +
+

Success

+

The next time you login from an unrecognized browser or device, you will need to provide a two-factor authentication code.

+
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss new file mode 100644 index 0000000000..ec6f008a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss @@ -0,0 +1,15 @@ +/** + * Copyright © 2016-2022 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. + */ diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts new file mode 100644 index 0000000000..86b596ac2b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts @@ -0,0 +1,80 @@ +/// +/// Copyright © 2016-2022 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, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { TotpTwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { MatStepper } from '@angular/material/stepper'; + +@Component({ + selector: 'tb-totp-auth-dialog', + templateUrl: './totp-auth-dialog.component.html', + styleUrls: ['./totp-auth-dialog.component.scss'] +}) +export class TotpAuthDialogComponent extends DialogComponent { + + private authAccountConfig: TotpTwoFactorAuthAccountConfig; + + totpConfigForm: FormGroup; + totpAuthURL: string; + + @ViewChild('stepper', {static: false}) stepper: MatStepper; + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + + constructor(protected store: Store, + protected router: Router, + private twoFaService: TwoFactorAuthenticationService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => { + this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig; + this.totpAuthURL = this.authAccountConfig.authUrl; + this.authAccountConfig.useByDefault = true; + import('qrcode').then((QRCode) => { + QRCode.toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); + this.canvasRef.nativeElement.style.width = 'auto'; + this.canvasRef.nativeElement.style.height = 'auto'; + }); + }); + this.totpConfigForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + } + + onSaveConfig() { + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.totpConfigForm.get('verificationCode').value).subscribe((res) => { + this.stepper.next(); + }); + } + + closeDialog() { + return this.dialogRef.close(this.stepper.selectedIndex > 1); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts index 440db606fd..e7dae87288 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts @@ -26,6 +26,8 @@ import { AppState } from '@core/core.state'; import { UserService } from '@core/http/user.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Observable } from 'rxjs'; +import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; @Injectable() export class UserProfileResolver implements Resolve { @@ -40,6 +42,17 @@ export class UserProfileResolver implements Resolve { } } +@Injectable() +export class UserTwoFAProvidersResolver implements Resolve> { + + constructor(private twoFactorAuthService: TwoFactorAuthenticationService) { + } + + resolve(): Observable> { + return this.twoFactorAuthService.getAvailableTwoFaProviders(); + } +} + const routes: Routes = [ { path: 'profile', @@ -54,7 +67,8 @@ const routes: Routes = [ } }, resolve: { - user: UserProfileResolver + user: UserProfileResolver, + providers: UserTwoFAProvidersResolver } } ]; @@ -63,7 +77,8 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], providers: [ - UserProfileResolver + UserProfileResolver, + UserTwoFAProvidersResolver ] }) export class ProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index b0bcabf62b..a1e9ec0523 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -93,8 +93,7 @@ {{ expirationJwtData }} -
- +
diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss index 08916ca584..f3f41c41d8 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.scss @@ -59,4 +59,21 @@ } } } + .description { + padding-bottom: 8px; + color: #808080; + margin-right: 8px; + } + + .provider { + padding: 14px 0; + + .mat-h3 { + margin-bottom: 8px; + } + + .checkbox-label { + font-size: 14px; + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts index 610006a3fd..0c676aca46 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { UserService } from '@core/http/user.service'; import { AuthUser, User } from '@shared/models/user.model'; import { Authority } from '@shared/models/authority.enum'; @@ -37,6 +37,12 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { DatePipe } from '@angular/common'; import { ClipboardService } from 'ngx-clipboard'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { AccountTwoFaSettings, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { TotpAuthDialogComponent } from '@home/pages/profile/authentication-dialog/totp-auth-dialog.component'; +import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component'; +import { EmailAuthDialogComponent, } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component'; @Component({ selector: 'tb-profile', @@ -47,8 +53,25 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir authorities = Authority; profile: FormGroup; + twoFactorAuth: FormGroup; user: User; languageList = env.supportedLangs; + allowTwoFactorAuth = false; + allowSMS2faProvider = false; + allowTOTP2faProvider = false; + allowEmail2faProvider = false; + twoFactorAuthProviderType = TwoFactorAuthProviderType; + + @ViewChild('totp') totp: MatSlideToggle; + + private authDialogMap = new Map( +[ + [TwoFactorAuthProviderType.TOTP, TotpAuthDialogComponent], + [TwoFactorAuthProviderType.SMS, SMSAuthDialogComponent], + [TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent] + ] + ); + private readonly authUser: AuthUser; get jwtToken(): string { @@ -69,6 +92,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir private userService: UserService, private authService: AuthService, private translate: TranslateService, + private twoFaService: TwoFactorAuthenticationService, public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder, @@ -80,10 +104,12 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir ngOnInit() { this.buildProfileForm(); + this.buildTwoFactorForm(); this.userLoaded(this.route.snapshot.data.user); + this.twoFactorLoad(this.route.snapshot.data.providers); } - buildProfileForm() { + private buildProfileForm() { this.profile = this.fb.group({ email: ['', [Validators.required, Validators.email]], firstName: [''], @@ -94,6 +120,19 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir }); } + private buildTwoFactorForm() { + this.twoFactorAuth = this.fb.group({ + TOTP: [false], + SMS: [false], + EMAIL: [false], + useByDefault: [null] + }); + this.twoFactorAuth.get('useByDefault').valueChanges.subscribe(value => { + this.twoFaService.updateTwoFaAccountConfig(value, true, {ignoreLoading: true}) + .subscribe(data => this.processTwoFactorAuthConfig(data)); + }); + } + save(): void { this.user = {...this.user, ...this.profile.value}; if (!this.user.additionalInfo) { @@ -128,7 +167,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir }); } - userLoaded(user: User) { + private userLoaded(user: User) { this.user = user; this.profile.reset(user); let lang; @@ -151,6 +190,37 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar); } + private twoFactorLoad(providers: TwoFactorAuthProviderType[]) { + if (providers.length) { + this.allowTwoFactorAuth = true; + this.twoFaService.getAccountTwoFaSettings().subscribe(data => this.processTwoFactorAuthConfig(data)); + providers.forEach(provider => { + switch (provider) { + case TwoFactorAuthProviderType.SMS: + this.allowSMS2faProvider = true; + break; + case TwoFactorAuthProviderType.TOTP: + this.allowTOTP2faProvider = true; + break; + case TwoFactorAuthProviderType.EMAIL: + this.allowEmail2faProvider = true; + break; + } + }); + } + } + + private processTwoFactorAuthConfig(setting?: AccountTwoFaSettings) { + if (setting) { + Object.values(setting.configs).forEach(config => { + this.twoFactorAuth.get(config.providerType).setValue(true); + if (config.useByDefault) { + this.twoFactorAuth.get('useByDefault').setValue(config.providerType, {emitEvent: false}); + } + }); + } + } + confirmForm(): FormGroup { return this.profile; } @@ -179,4 +249,31 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir })); } } + + confirm2FAChange(event: MouseEvent, provider: TwoFactorAuthProviderType) { + event.stopPropagation(); + event.preventDefault(); + const providerName = provider === TwoFactorAuthProviderType.TOTP ? 'authenticator app' : `${provider.toLowerCase()} authentication`; + if (this.twoFactorAuth.get(provider).value) { + this.dialogService.confirm(`Are you sure you want to disable ${providerName}?`, + `Disabling ${providerName} will make your account less secure`).subscribe(res => { + if (res) { + this.twoFaService.deleteTwoFaAccountConfig(provider).subscribe(data => this.processTwoFactorAuthConfig(data)); + } + }); + } else { + this.dialog.open(this.authDialogMap.get(provider), { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + email: this.user.email + } + }).afterClosed().subscribe(res => { + if (res) { + this.twoFactorAuth.get(provider).setValue(res); + this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false}); + } + }); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts index 4a8bcab074..5210158537 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.module.ts @@ -20,11 +20,17 @@ import { ProfileComponent } from './profile.component'; import { SharedModule } from '@shared/shared.module'; import { ProfileRoutingModule } from './profile-routing.module'; import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; +import { TotpAuthDialogComponent } from './authentication-dialog/totp-auth-dialog.component'; +import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component'; +import { EmailAuthDialogComponent } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component'; @NgModule({ declarations: [ ProfileComponent, - ChangePasswordDialogComponent + ChangePasswordDialogComponent, + TotpAuthDialogComponent, + SMSAuthDialogComponent, + EmailAuthDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 4bfce6414a..833efd39ab 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -63,6 +63,7 @@ export const HelpLinks = { smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings', securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', + twoFactorAuthSettings: helpBaseUrl + '/docs/', ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/', ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node', ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node', 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 index ab64143a66..731c1212e9 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -23,7 +23,8 @@ export interface TwoFactorAuthSettings { verificationCodeSendRateLimit: string; } -export type TwoFactorAuthProviderConfig = Partial; +export type TwoFactorAuthProviderConfig = Partial; export interface TotpTwoFactorAuthProviderConfig { providerType: TwoFactorAuthProviderType; @@ -36,7 +37,37 @@ export interface SmsTwoFactorAuthProviderConfig { verificationCodeLifetime: number; } +export interface EmailTwoFactorAuthProviderConfig { + providerType: TwoFactorAuthProviderType; + verificationCodeLifetime: number; +} + export enum TwoFactorAuthProviderType{ TOTP = 'TOTP', - SMS = 'SMS' + SMS = 'SMS', + EMAIL = 'EMAIL' +} + +interface GeneralTwoFactorAuthAccountConfig { + providerType: TwoFactorAuthProviderType; + useByDefault: boolean; +} + +export interface TotpTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig { + authUrl: string; +} + +export interface SmsTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig { + phoneNumber: string; +} + +export interface EmailTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig { + email: string; +} + +export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig | EmailTwoFactorAuthAccountConfig; + + +export interface AccountTwoFaSettings { + configs?: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig}; } 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 89b9bc5d2d..743068fc22 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -255,6 +255,7 @@ }, "2fa": { "2fa": "Two-factor authentication", + "add-provider": "Add provider", "available-providers": "Available providers:", "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", @@ -262,14 +263,14 @@ "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", "max-verification-failures-before-user-lockout-required": "Max verification failures is required.", "provider": "Provider", - "total-allowed-time-for-verification": "Total allowed time for verification", + "total-allowed-time-for-verification": "Total allowed time for verification (sec)", "total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.", "total-allowed-time-for-verification-required": "Total allowed time is required.", "use-system-two-factor-auth-settings": "Use system two factor auth settings", "verification-code-check-rate-limit": "Verification code check rate limit", "verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format", "verification-code-check-rate-limit-required": "Verification code check rate limit is required.", - "verification-code-lifetime": "Verification code lifetime", + "verification-code-lifetime": "Verification code lifetime (sec)", "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", "verification-code-lifetime-required": "Verification code lifetime is required.", "verification-code-send-rate-limit": "Verification code send rate limit",