From f96cb90da88e9c0c26ff30767a06da38b3043198 Mon Sep 17 00:00:00 2001 From: Jack Anderson Date: Fri, 22 Nov 2024 13:14:20 +0000 Subject: [PATCH] Add Two Factor Authentication Popup --- core/app/core/src/lib/core.ts | 3 + .../check-two-factor-code.ts | 54 ++++++++ .../2fa-check-modal.component.html | 63 +++++++++ .../2fa-check-modal.component.ts | 76 ++++++++++ .../2fa-check-modal/2fa-check-modal.model.ts | 31 +++++ .../2fa-check-modal/2fa-check-modal.module.ts | 48 +++++++ .../views/2fa/components/2fa/2fa.component.ts | 74 ++++++---- .../themes/suite8/css/layout/_two-factor.scss | 49 +++++-- .../CheckTwoFactorCodeHandler.php | 130 ++++++++++++++++++ 9 files changed, 496 insertions(+), 32 deletions(-) create mode 100644 core/app/core/src/lib/services/process/processes/check-two-factor-code/check-two-factor-code.ts create mode 100644 core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.html create mode 100644 core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.ts create mode 100644 core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.model.ts create mode 100644 core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.module.ts create mode 100644 core/backend/Process/LegacyHandler/CheckTwoFactorCodeHandler.php diff --git a/core/app/core/src/lib/core.ts b/core/app/core/src/lib/core.ts index 572b37255..8e4c7c9dd 100644 --- a/core/app/core/src/lib/core.ts +++ b/core/app/core/src/lib/core.ts @@ -725,3 +725,6 @@ export * from './views/2fa/components/2fa/2fa.component'; export * from './views/2fa/components/2fa/2fa.module'; export * from './views/2fa/components/2fa-check/2fa-check.component'; export * from './views/2fa/components/2fa-check/2fa-check.module'; +export * from './views/2fa/components/2fa-check-modal/2fa-check-modal.component'; +export * from './views/2fa/components/2fa-check-modal/2fa-check-modal.module'; +export * from './views/2fa/components/2fa-check-modal/2fa-check-modal.model'; diff --git a/core/app/core/src/lib/services/process/processes/check-two-factor-code/check-two-factor-code.ts b/core/app/core/src/lib/services/process/processes/check-two-factor-code/check-two-factor-code.ts new file mode 100644 index 000000000..a3bc434ce --- /dev/null +++ b/core/app/core/src/lib/services/process/processes/check-two-factor-code/check-two-factor-code.ts @@ -0,0 +1,54 @@ +/** + * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd. + * Copyright (C) 2024 SalesAgility Ltd. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License version 3 as published by the + * Free Software Foundation with the addition of the following permission added + * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK + * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE + * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * In accordance with Section 7(b) of the GNU Affero General Public License + * version 3, these Appropriate Legal Notices must retain the display of the + * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably + * feasible for technical reasons, the Appropriate Legal Notices must display + * the words "Supercharged by SuiteCRM". + */ + +import {Injectable} from '@angular/core'; +import {take} from 'rxjs/operators'; +import {Process, ProcessService} from "../../process.service"; +import {Observable} from "rxjs"; + +@Injectable({providedIn: 'root'}) +export class CheckTwoFactorCode { + + + constructor( + protected processService: ProcessService, + ) { + } + + /** + * Check Auth Code + */ + public checkCode(auth_code): Observable { + + const processType = 'check-two-factor-code'; + + const options = { + auth_code + }; + + return this.processService.submit(processType, options).pipe(take(1)); + } +} diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.html b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.html new file mode 100644 index 000000000..a0b96e013 --- /dev/null +++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.html @@ -0,0 +1,63 @@ + + + +
+
+
+ + + + + +
+ +
+
+ +
+ +
+
+
+
diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.ts b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.ts new file mode 100644 index 000000000..af88f3fc1 --- /dev/null +++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.component.ts @@ -0,0 +1,76 @@ +/** + * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd. + * Copyright (C) 2024 SalesAgility Ltd. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License version 3 as published by the + * Free Software Foundation with the addition of the following permission added + * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK + * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE + * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * In accordance with Section 7(b) of the GNU Affero General Public License + * version 3, these Appropriate Legal Notices must retain the display of the + * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably + * feasible for technical reasons, the Appropriate Legal Notices must display + * the words "Supercharged by SuiteCRM". + */ +import {Component, HostListener} from "@angular/core"; +import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; +import {LanguageStore} from "../../../../store/language/language.store"; +import {CheckTwoFactorCode} from "../../../../services/process/processes/check-two-factor-code/check-two-factor-code"; +import {TwoFactorCheckModalResult} from "./2fa-check-modal.model"; +import {MessageService} from "../../../../services/message/message.service"; + +@Component({ + selector: 'scrm-2fa-modal', + templateUrl: './2fa-check-modal.component.html', + styleUrls: [], +}) +export class TwoFactorCheckModalComponent { + + authCode: string; + + + @HostListener('keyup.control.enter') + onEnterKey() { + this.checkCode(); + } + + constructor( + public activeModal: NgbActiveModal, + protected language: LanguageStore, + protected message: MessageService, + protected checkTwoFactorCode: CheckTwoFactorCode + ) { + } + + + public checkCode() { + const authCode = this.authCode; + + this.checkTwoFactorCode.checkCode(authCode).subscribe({ + next: (response) => { + this.closeModal(response.data.two_factor_complete) + }, + error: () => { + this.message.addDangerMessageByKey('LBL_FACTOR_AUTH_FAIL') + } + }); + } + + public closeModal(authComplete: boolean) { + this.activeModal.close({ + two_factor_complete: authComplete + } as TwoFactorCheckModalResult); + } + +} diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.model.ts b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.model.ts new file mode 100644 index 000000000..ed614a3c4 --- /dev/null +++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.model.ts @@ -0,0 +1,31 @@ +/** + * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd. + * Copyright (C) 2024 SalesAgility Ltd. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License version 3 as published by the + * Free Software Foundation with the addition of the following permission added + * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK + * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE + * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * In accordance with Section 7(b) of the GNU Affero General Public License + * version 3, these Appropriate Legal Notices must retain the display of the + * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably + * feasible for technical reasons, the Appropriate Legal Notices must display + * the words "Supercharged by SuiteCRM". + */ + +export interface TwoFactorCheckModalResult { + [key: string]: any; + + two_factor_complete: boolean +} diff --git a/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.module.ts b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.module.ts new file mode 100644 index 000000000..b8f780a6d --- /dev/null +++ b/core/app/core/src/lib/views/2fa/components/2fa-check-modal/2fa-check-modal.module.ts @@ -0,0 +1,48 @@ +/** + * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd. + * Copyright (C) 2024 SalesAgility Ltd. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License version 3 as published by the + * Free Software Foundation with the addition of the following permission added + * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK + * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE + * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * In accordance with Section 7(b) of the GNU Affero General Public License + * version 3, these Appropriate Legal Notices must retain the display of the + * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably + * feasible for technical reasons, the Appropriate Legal Notices must display + * the words "Supercharged by SuiteCRM". + */ + +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {ModalModule} from "../../../../components/modal/components/modal/modal.module"; +import {TwoFactorCheckModalComponent} from "./2fa-check-modal.component"; +import {TwoFactorCheckModule} from "../2fa-check/2fa-check.module"; +import {FormsModule} from "@angular/forms"; +import {LabelModule} from "../../../../components/label/label.module"; +import {TrustHtmlModule} from "../../../../pipes/trust-html/trust-html.module"; + +@NgModule({ + declarations: [TwoFactorCheckModalComponent], + imports: [ + CommonModule, + ModalModule, + TwoFactorCheckModule, + FormsModule, + LabelModule, + TrustHtmlModule, + ] +}) +export class TwoFactorCheckModalModule { +} diff --git a/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts b/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts index 741b77543..6b7e50d4a 100644 --- a/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts +++ b/core/app/core/src/lib/views/2fa/components/2fa/2fa.component.ts @@ -34,6 +34,8 @@ import {ButtonCallback, ButtonInterface} from "../../../../common/components/but import {UserPreferenceStore} from "../../../../store/user-preference/user-preference.store"; import {Clipboard} from '@angular/cdk/clipboard'; import {GenerateBackupCodes} from "../../../../services/process/processes/generate-backup-codes/generate-backup-codes"; +import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; +import {TwoFactorCheckModalComponent} from "../2fa-check-modal/2fa-check-modal.component"; @Component({ @@ -70,6 +72,7 @@ export class TwoFactorComponent implements OnInit { protected message: MessageService, protected language: LanguageStore, protected userPreference: UserPreferenceStore, + protected modalService: NgbModal, protected clipboard: Clipboard, protected generateBackupCodesService: GenerateBackupCodes, ) { @@ -117,7 +120,7 @@ export class TwoFactorComponent implements OnInit { onClick: ((): void => { this.generateBackupCodes(); }) as ButtonCallback, - labelKey: 'LBL_REGENERATE_BACKUP_CODES', + labelKey: 'LBL_REGENERATE_CODES', titleKey: '' } as ButtonInterface; } @@ -140,35 +143,46 @@ export class TwoFactorComponent implements OnInit { } public disable2FactorAuth(): void { - this.authService.disable2fa().subscribe({ - next: (response) => { - if (isTrue(response?.two_factor_disabled)) { + const modal = this.modalService.open(TwoFactorCheckModalComponent, {size: 'lg'}); - this.isAppMethodEnabled.set(false); - this.areRecoveryCodesGenerated.set(false); - this.isQrCodeGenerated.set(false); - this.message.addSuccessMessageByKey('LBL_FACTOR_AUTH_DISABLE'); - } - }, - error: () => { - this.isAppMethodEnabled.set(true); - this.areRecoveryCodesGenerated.set(true); + modal.result.then((result) => { + if (!result.two_factor_complete){ + this.message.addDangerMessageByKey('LBL_FACTOR_AUTH_FAIL'); + return; } - }); - return; + + this.authService.disable2fa().subscribe({ + next: (response) => { + if (isTrue(response?.two_factor_disabled)) { + + this.isAppMethodEnabled.set(false); + this.areRecoveryCodesGenerated.set(false); + this.isQrCodeGenerated.set(false); + + this.message.addSuccessMessageByKey('LBL_FACTOR_AUTH_DISABLE'); + } + }, + error: () => { + this.isAppMethodEnabled.set(true); + this.areRecoveryCodesGenerated.set(true); + } + }); + return; + }).catch(); + } getTitle(): string { return this.title; } - public finalize2fa() { + public finalize2fa(): void { this.authService.finalize2fa(this.authCode).subscribe(response => { const verified = response?.two_factor_setup_complete ?? false; if (isTrue(verified)) { - this.generateBackupCodes(); + this.generateCodes(); this.message.addSuccessMessageByKey('LBL_FACTOR_AUTH_SUCCESS'); this.isAppMethodEnabled.set(true); @@ -182,24 +196,36 @@ export class TwoFactorComponent implements OnInit { }) } - public copyBackupCodes() { + public copyBackupCodes(): void { this.clipboard.copy(this.backupCodes); } - - public generateBackupCodes(){ - this.areRecoveryCodesGenerated.set(false) + public generateCodes(): void { this.generateBackupCodesService.generate().subscribe({ next: (response) => { - console.log(response); - console.log('inside next'); this.backupCodes = response?.data.backupCodes; this.areRecoveryCodesGenerated.set(true) }, error: () => { - console.log('inside eror'); this.areRecoveryCodesGenerated.set(false) } }); + return; + } + + + public generateBackupCodes(): void { + + const modal = this.modalService.open(TwoFactorCheckModalComponent, {size: 'lg'}); + + modal.result.then((result) => { + if (!result.two_factor_complete){ + this.message.addDangerMessageByKey('LBL_FACTOR_AUTH_FAIL'); + return; + } + + this.areRecoveryCodesGenerated.set(false) + this.generateCodes() + }).catch(); } } diff --git a/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss b/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss index 1d32aaa23..64157b777 100644 --- a/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss +++ b/core/app/shell/src/themes/suite8/css/layout/_two-factor.scss @@ -1,3 +1,30 @@ +.two-factor-popup { + .auth-input { + border: .03em solid $nepal-grey; + padding: .45em; + margin: 0 1em 0 0; + color: #666; + font-size: .8em; + background-color: #fff; + width: 30%; + text-align: center; + } +} + +@media (min-width: 991px) { + .backup-codes-container { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 10px; + + .backup-codes { + padding: 10px; + border: 1px solid lightgrey; + border-radius: .25rem; + } + } +} + @media (min-width: 768px) { #two-factor { @@ -8,6 +35,7 @@ width: fit-content; margin-left: 1.5rem; } + .qr-code-container { display: flex; padding: .25rem 1.5rem .5rem 1.5rem; @@ -39,15 +67,17 @@ } } - .backup-codes-container { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 10px; + @media (max-width: 991px) { + .backup-codes-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; - .backup-codes { - padding: 10px; - border: 1px solid lightgrey; - border-radius: .25rem; + .backup-codes { + padding: 10px; + border: 1px solid lightgrey; + border-radius: .25rem; + } } } @@ -68,6 +98,7 @@ margin-left: 1.5rem; margin-right: 2rem; } + .qr-code-container { display: flex; flex-direction: column; @@ -122,6 +153,7 @@ } } } + @media(max-width: 374px) { #two-factor { @@ -132,6 +164,7 @@ margin-left: 1.5rem; margin-right: 2rem; } + .qr-code-container { display: flex; flex-direction: column; diff --git a/core/backend/Process/LegacyHandler/CheckTwoFactorCodeHandler.php b/core/backend/Process/LegacyHandler/CheckTwoFactorCodeHandler.php new file mode 100644 index 000000000..54e7affea --- /dev/null +++ b/core/backend/Process/LegacyHandler/CheckTwoFactorCodeHandler.php @@ -0,0 +1,130 @@ +. + * + * In accordance with Section 7(b) of the GNU Affero General Public License + * version 3, these Appropriate Legal Notices must retain the display of the + * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably + * feasible for technical reasons, the Appropriate Legal Notices must display + * the words "Supercharged by SuiteCRM". + */ + +namespace App\Process\LegacyHandler; + +use ApiPlatform\Exception\InvalidArgumentException; +use App\Engine\LegacyHandler\LegacyHandler; +use App\Engine\LegacyHandler\LegacyScopeState; +use App\Process\Entity\Process; +use App\Process\Service\ProcessHandlerInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\RequestStack; + +class CheckTwoFactorCodeHandler extends LegacyHandler implements ProcessHandlerInterface +{ + protected const MSG_OPTIONS_NOT_FOUND = 'Process options is not defined'; + protected const PROCESS_TYPE = 'check-two-factor-code'; + + + protected TotpAuthenticatorInterface $totpAuthenticator; + protected Security $security; + + + public function __construct( + string $projectDir, + string $legacyDir, + string $legacySessionName, + string $defaultSessionName, + LegacyScopeState $legacyScopeState, + RequestStack $requestStack, + Security $security, + TotpAuthenticatorInterface $totpAuthenticator, + ) { + parent::__construct( + $projectDir, + $legacyDir, + $legacySessionName, + $defaultSessionName, + $legacyScopeState, + $requestStack + ); + $this->security = $security; + $this->totpAuthenticator = $totpAuthenticator; + } + + public function getHandlerKey(): string + { + return self::PROCESS_TYPE; + } + + public function getProcessType(): string + { + return self::PROCESS_TYPE; + } + + public function requiredAuthRole(): string + { + return 'ROLE_USER'; + } + + public function getRequiredACLs(Process $process): array + { + return []; + } + + public function configure(Process $process): void + { + $process->setId(self::PROCESS_TYPE); + $process->setAsync(false); + } + + public function validate(Process $process): void + { + if (empty($process->getOptions())) { + throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND); + } + } + + public function run(Process $process): void + { + $options = $process->getOptions(); + + $authCode = $options['auth_code'] ?? false; + + $user = $this->security->getToken()->getUser(); + + if (!$options['auth_code']) { + $process->setStatus('error'); + $process->setMessages(['LBL_ACTION_ERROR']); + + return; + } + + $correctCode = $this->totpAuthenticator->checkCode($user, $authCode); + + if (!$correctCode){ + $correctCode = $user->isBackupCode($authCode); + } + + $response = ['two_factor_complete' => $correctCode]; + + $process->setData($response); + + } +}