From d12b40fb96d56786a96c06a621f3d8e0a0d24f4a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 7 Oct 2022 11:06:28 +0200 Subject: Implement two factor in client --- .../app/+my-account/my-account-routing.module.ts | 11 +++ .../my-account-change-email.component.ts | 4 +- .../my-account-change-password.component.ts | 4 +- .../my-account-danger-zone.component.ts | 2 +- .../my-account-settings.component.html | 10 ++ .../my-account-two-factor/index.ts | 3 + .../my-account-two-factor-button.component.html | 12 +++ .../my-account-two-factor-button.component.ts | 49 ++++++++++ .../my-account-two-factor.component.html | 54 +++++++++++ .../my-account-two-factor.component.scss | 16 ++++ .../my-account-two-factor.component.ts | 105 +++++++++++++++++++++ .../my-account-two-factor/two-factor.service.ts | 52 ++++++++++ client/src/app/+my-account/my-account.module.ts | 14 ++- 13 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts create mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html create mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts create mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html create mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss create mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts create mode 100644 client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts (limited to 'client/src/app/+my-account') diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index ef39c1a36..b39b1f6b4 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -7,6 +7,7 @@ import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-b import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' +import { MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor' import { MyAccountComponent } from './my-account.component' const myAccountRoutes: Routes = [ @@ -30,6 +31,16 @@ const myAccountRoutes: Routes = [ } }, + { + path: 'two-factor-auth', + component: MyAccountTwoFactorComponent, + data: { + meta: { + title: $localize`Two factor authentication` + } + } + }, + { path: 'video-channels', redirectTo: '/my-library/video-channels', diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts index 9b87daa40..9e6b8e21d 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts @@ -4,7 +4,7 @@ import { Component, OnInit } from '@angular/core' import { AuthService, ServerService, UserService } from '@app/core' import { USER_EMAIL_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { User } from '@shared/models' +import { HttpStatusCode, User } from '@shared/models' @Component({ selector: 'my-account-change-email', @@ -57,7 +57,7 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni }, error: err => { - if (err.status === 401) { + if (err.status === HttpStatusCode.UNAUTHORIZED_401) { this.error = $localize`You current password is invalid.` return } diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts index 47e54dc23..dd405de33 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts @@ -7,7 +7,7 @@ import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { User } from '@shared/models' +import { HttpStatusCode, User } from '@shared/models' @Component({ selector: 'my-account-change-password', @@ -57,7 +57,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On }, error: err => { - if (err.status === 401) { + if (err.status === HttpStatusCode.UNAUTHORIZED_401) { this.error = $localize`You current password is invalid.` return } diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts index 2bae3499e..9619623ee 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts @@ -18,7 +18,7 @@ export class MyAccountDangerZoneComponent { ) { } async deleteMe () { - const res = await this.confirmService.confirmWithInput( + const res = await this.confirmService.confirmWithExpectedInput( $localize`Are you sure you want to delete your account?` + '

' + // eslint-disable-next-line max-len diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index 42a8d0856..666205de6 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html @@ -62,6 +62,16 @@ +
+
+ +
+ +
+ +
+
+
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts new file mode 100644 index 000000000..ef83009a5 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts @@ -0,0 +1,3 @@ +export * from './my-account-two-factor-button.component' +export * from './my-account-two-factor.component' +export * from './two-factor.service' diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html new file mode 100644 index 000000000..2fcfffbf3 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html @@ -0,0 +1,12 @@ +
+ +

Two factor authentication adds an additional layer of security to your account by requiring a numeric code from another device (most commonly mobile phones) when you log in.

+ + Enable two-factor authentication +
+ + + Disable two-factor authentication + + +
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts new file mode 100644 index 000000000..03b00e933 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts @@ -0,0 +1,49 @@ +import { Subject } from 'rxjs' +import { Component, Input, OnInit } from '@angular/core' +import { AuthService, ConfirmService, Notifier, User } from '@app/core' +import { TwoFactorService } from './two-factor.service' + +@Component({ + selector: 'my-account-two-factor-button', + templateUrl: './my-account-two-factor-button.component.html' +}) +export class MyAccountTwoFactorButtonComponent implements OnInit { + @Input() user: User = null + @Input() userInformationLoaded: Subject + + twoFactorEnabled = false + + constructor ( + private notifier: Notifier, + private twoFactorService: TwoFactorService, + private confirmService: ConfirmService, + private auth: AuthService + ) { + } + + ngOnInit () { + this.userInformationLoaded.subscribe(() => { + this.twoFactorEnabled = this.user.twoFactorEnabled + }) + } + + async disableTwoFactor () { + const message = $localize`Are you sure you want to disable two factor authentication of your account?` + + const { confirmed, password } = await this.confirmService.confirmWithPassword(message, $localize`Disable two factor`) + if (confirmed === false) return + + this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password }) + .subscribe({ + next: () => { + this.twoFactorEnabled = false + + this.auth.refreshUserInformation() + + this.notifier.success($localize`Two factor authentication disabled`) + }, + + error: err => this.notifier.error(err.message) + }) + } +} diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html new file mode 100644 index 000000000..16c344e3b --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html @@ -0,0 +1,54 @@ +

+ + Two factor authentication +

+ +
+ Two factor authentication is already enabled. +
+ +
+ +
+ + +
Confirm your password to enable two factor authentication
+ + + + +
+
+ + + +

+ Scan this QR code into a TOTP app on your phone. This app will generate tokens that you will have to enter when logging in. +

+ + + +
+ If you can't scan the QR code and need to enter it manually, here is the plain-text secret: +
+ +
{{ twoFactorSecret }}
+ +
+ + +
Enter the code generated by your authenticator app to confirm
+ + + + +
+
+ +
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss new file mode 100644 index 000000000..cee016bb8 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss @@ -0,0 +1,16 @@ +@use '_variables' as *; +@use '_mixins' as *; + +.root { + max-width: 600px; +} + +.secret-plain-text { + font-family: monospace; + font-size: 0.9rem; +} + +qrcode { + display: inline-block; + margin: auto; +} diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts new file mode 100644 index 000000000..e4d4188f7 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts @@ -0,0 +1,105 @@ +import { Component, OnInit } from '@angular/core' +import { FormGroup } from '@angular/forms' +import { Router } from '@angular/router' +import { AuthService, Notifier, User } from '@app/core' +import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators' +import { FormReactiveService } from '@app/shared/shared-forms' +import { TwoFactorService } from './two-factor.service' + +@Component({ + selector: 'my-account-two-factor', + templateUrl: './my-account-two-factor.component.html', + styleUrls: [ './my-account-two-factor.component.scss' ] +}) +export class MyAccountTwoFactorComponent implements OnInit { + twoFactorAlreadyEnabled: boolean + + step: 'request' | 'confirm' | 'confirmed' = 'request' + + twoFactorSecret: string + twoFactorURI: string + + inPasswordStep = true + + formPassword: FormGroup + formErrorsPassword: any + + formOTP: FormGroup + formErrorsOTP: any + + private user: User + private requestToken: string + + constructor ( + private notifier: Notifier, + private twoFactorService: TwoFactorService, + private formReactiveService: FormReactiveService, + private auth: AuthService, + private router: Router + ) { + } + + ngOnInit () { + this.buildPasswordForm() + this.buildOTPForm() + + this.auth.userInformationLoaded.subscribe(() => { + this.user = this.auth.getUser() + + this.twoFactorAlreadyEnabled = this.user.twoFactorEnabled + }) + } + + requestTwoFactor () { + this.twoFactorService.requestTwoFactor({ + userId: this.user.id, + currentPassword: this.formPassword.value['current-password'] + }).subscribe({ + next: ({ otpRequest }) => { + this.requestToken = otpRequest.requestToken + this.twoFactorURI = otpRequest.uri + this.twoFactorSecret = otpRequest.secret.replace(/(.{4})/g, '$1 ').trim() + + this.step = 'confirm' + }, + + error: err => this.notifier.error(err.message) + }) + } + + confirmTwoFactor () { + this.twoFactorService.confirmTwoFactorRequest({ + userId: this.user.id, + requestToken: this.requestToken, + otpToken: this.formOTP.value['otp-token'] + }).subscribe({ + next: () => { + this.notifier.success($localize`Two factor authentication has been enabled.`) + + this.auth.refreshUserInformation() + + this.router.navigateByUrl('/my-account/settings') + }, + + error: err => this.notifier.error(err.message) + }) + } + + private buildPasswordForm () { + const { form, formErrors } = this.formReactiveService.buildForm({ + 'current-password': USER_EXISTING_PASSWORD_VALIDATOR + }) + + this.formPassword = form + this.formErrorsPassword = formErrors + } + + private buildOTPForm () { + const { form, formErrors } = this.formReactiveService.buildForm({ + 'otp-token': USER_OTP_TOKEN_VALIDATOR + }) + + this.formOTP = form + this.formErrorsOTP = formErrors + } +} diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts new file mode 100644 index 000000000..c0e5ac492 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/two-factor.service.ts @@ -0,0 +1,52 @@ +import { catchError } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, UserService } from '@app/core' +import { TwoFactorEnableResult } from '@shared/models' + +@Injectable() +export class TwoFactorService { + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor + ) { } + + // --------------------------------------------------------------------------- + + requestTwoFactor (options: { + userId: number + currentPassword: string + }) { + const { userId, currentPassword } = options + + const url = UserService.BASE_USERS_URL + userId + '/two-factor/request' + + return this.authHttp.post(url, { currentPassword }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + confirmTwoFactorRequest (options: { + userId: number + requestToken: string + otpToken: string + }) { + const { userId, requestToken, otpToken } = options + + const url = UserService.BASE_USERS_URL + userId + '/two-factor/confirm-request' + + return this.authHttp.post(url, { requestToken, otpToken }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + disableTwoFactor (options: { + userId: number + currentPassword: string + }) { + const { userId, currentPassword } = options + + const url = UserService.BASE_USERS_URL + userId + '/two-factor/disable' + + return this.authHttp.post(url, { currentPassword }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } +} diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 4081e4f01..f5beaa4db 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -1,3 +1,4 @@ +import { QRCodeModule } from 'angularx-qrcode' import { AutoCompleteModule } from 'primeng/autocomplete' import { TableModule } from 'primeng/table' import { DragDropModule } from '@angular/cdk/drag-drop' @@ -23,12 +24,18 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' +import { + MyAccountTwoFactorButtonComponent, + MyAccountTwoFactorComponent, + TwoFactorService +} from './my-account-settings/my-account-two-factor' import { MyAccountComponent } from './my-account.component' @NgModule({ imports: [ MyAccountRoutingModule, + QRCodeModule, AutoCompleteModule, TableModule, DragDropModule, @@ -52,6 +59,9 @@ import { MyAccountComponent } from './my-account.component' MyAccountChangeEmailComponent, MyAccountApplicationsComponent, + MyAccountTwoFactorButtonComponent, + MyAccountTwoFactorComponent, + MyAccountDangerZoneComponent, MyAccountBlocklistComponent, MyAccountAbusesListComponent, @@ -64,7 +74,9 @@ import { MyAccountComponent } from './my-account.component' MyAccountComponent ], - providers: [] + providers: [ + TwoFactorService + ] }) export class MyAccountModule { } -- cgit v1.2.3