Add Two Factor Authentication Popup

This commit is contained in:
Jack Anderson 2024-11-22 13:14:20 +00:00
parent e99d51b4e7
commit f96cb90da8
9 changed files with 496 additions and 32 deletions

View file

@ -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';

View file

@ -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 <http://www.gnu.org/licenses/>.
*
* 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<Process> {
const processType = 'check-two-factor-code';
const options = {
auth_code
};
return this.processService.submit(processType, options).pipe(take(1));
}
}

View file

@ -0,0 +1,63 @@
<! --
/**
* 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 http://www.gnu.org/licenses.
*
* 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".
*/
-->
<scrm-modal [closable]="false"
bodyKlass="m-0 small-font"
footerKlass="border-0"
headerKlass="border-0"
klass="two-factor-popup">
<div modal-body>
<div>
<div class="d-flex flex-column pt-4 align-items-center">
<scrm-label labelKey="LBL_ENTER_AUTH_APP_2FA_CODE" class="pb-4"></scrm-label>
<input [(ngModel)]="authCode"
id="auth_code"
type="text"
name="auth_code"
autocomplete="off"
class="mb-3 auth-input"
maxlength="6"/>
<button id="submit-2fa-code"
(click)="checkCode()"
type="submit"
class="btn btn-sm btn-main mb-1">
<scrm-label labelKey="LBL_VERIFY_2FA"></scrm-label>
</button>
<div class="small mt-2 text-muted">
<scrm-label labelKey="LBL_PROBLEMS_GENERATING_CODE"></scrm-label>
</div>
<div class="small mb-2 text-muted">
<scrm-label labelKey="LBL_BACKUP_CODES_FALLBACK_INSTRUCTIONS"></scrm-label>
</div>
</div>
</div>
</div>
</scrm-modal>

View file

@ -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 <http://www.gnu.org/licenses/>.
*
* 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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
* 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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*
* 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 {
}

View file

@ -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();
}
}

View file

@ -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;

View file

@ -0,0 +1,130 @@
<?php
/**
* 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 <http://www.gnu.org/licenses/>.
*
* 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);
}
}