mirror of
https://github.com/SuiteCRM/SuiteCRM-Core.git
synced 2025-08-28 21:58:03 +08:00
Two Factor
This commit is contained in:
parent
50e5c54c26
commit
ff4ba2bdd2
28 changed files with 1354 additions and 125 deletions
479
composer.lock
generated
479
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "b00e8c43ab15eb49c8f498866f9b531d",
|
||||
"content-hash": "e9f5e143fb7cfc803d711f5b63eb2e09",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/core",
|
||||
|
@ -194,6 +194,60 @@
|
|||
},
|
||||
"time": "2024-05-29T05:48:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "v3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||
"reference": "510de6eca6248d77d31b339d62437cc995e2fb41"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/510de6eca6248d77d31b339d62437cc995e2fb41",
|
||||
"reference": "510de6eca6248d77d31b339d62437cc995e2fb41",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dasprid/enum": "^1.0.3",
|
||||
"ext-iconv": "*",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phly/keep-a-changelog": "^2.12",
|
||||
"phpunit/phpunit": "^10.5.11 || 11.0.4",
|
||||
"spatie/phpunit-snapshot-assertions": "^5.1.5",
|
||||
"squizlabs/php_codesniffer": "^3.9"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BaconQrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"support": {
|
||||
"issues": "https://github.com/Bacon/BaconQrCode/issues",
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.0"
|
||||
},
|
||||
"time": "2024-04-18T11:16:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "beberlei/assert",
|
||||
"version": "v3.3.2",
|
||||
|
@ -452,6 +506,56 @@
|
|||
],
|
||||
"time": "2021-08-17T13:49:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dasprid/enum",
|
||||
"version": "1.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DASPRiD/Enum.git",
|
||||
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
|
||||
"reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1 <9.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
|
||||
"squizlabs/php_codesniffer": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DASPRiD\\Enum\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP 7.1 enum implementation",
|
||||
"keywords": [
|
||||
"enum",
|
||||
"map"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/DASPRiD/Enum/issues",
|
||||
"source": "https://github.com/DASPRiD/Enum/tree/1.0.6"
|
||||
},
|
||||
"time": "2024-08-09T14:30:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "defuse/php-encryption",
|
||||
"version": "v2.4.0",
|
||||
|
@ -1980,6 +2084,78 @@
|
|||
},
|
||||
"time": "2023-04-21T15:31:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "endroid/qr-code",
|
||||
"version": "5.0.9",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/endroid/qr-code.git",
|
||||
"reference": "3dcdfab4c9122874f3915d8bf80a43b9df11852d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/endroid/qr-code/zipball/3dcdfab4c9122874f3915d8bf80a43b9df11852d",
|
||||
"reference": "3dcdfab4c9122874f3915d8bf80a43b9df11852d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"bacon/bacon-qr-code": "^3.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"endroid/quality": "dev-main",
|
||||
"ext-gd": "*",
|
||||
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
|
||||
"setasign/fpdf": "^1.8.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Enables you to write PNG images",
|
||||
"khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
|
||||
"roave/security-advisories": "Makes sure package versions with known security issues are not installed",
|
||||
"setasign/fpdf": "Enables you to use the PDF writer"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "5.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Endroid\\QrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jeroen van den Enden",
|
||||
"email": "info@endroid.nl"
|
||||
}
|
||||
],
|
||||
"description": "Endroid QR Code",
|
||||
"homepage": "https://github.com/endroid/qr-code",
|
||||
"keywords": [
|
||||
"code",
|
||||
"endroid",
|
||||
"php",
|
||||
"qr",
|
||||
"qrcode"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/endroid/qr-code/issues",
|
||||
"source": "https://github.com/endroid/qr-code/tree/5.0.9"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/endroid",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-08T08:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ezimuel/guzzlestreams",
|
||||
"version": "3.1.0",
|
||||
|
@ -5460,6 +5636,225 @@
|
|||
},
|
||||
"time": "2020-09-05T13:00:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scheb/2fa-backup-code",
|
||||
"version": "v6.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/scheb/2fa-backup-code.git",
|
||||
"reference": "1ad84e7eb26eb425c609e03097cac99387dde44c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/1ad84e7eb26eb425c609e03097cac99387dde44c",
|
||||
"reference": "1ad84e7eb26eb425c609e03097cac99387dde44c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"scheb/2fa-bundle": "self.version"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Scheb\\TwoFactorBundle\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Scheb",
|
||||
"email": "me@christianscheb.de"
|
||||
}
|
||||
],
|
||||
"description": "Extends scheb/2fa-bundle with backup codes support",
|
||||
"homepage": "https://github.com/scheb/2fa",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"Authentication",
|
||||
"backup-codes",
|
||||
"symfony",
|
||||
"two-factor",
|
||||
"two-step"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/scheb/2fa-backup-code/tree/v6.12.0"
|
||||
},
|
||||
"time": "2023-12-03T15:44:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scheb/2fa-bundle",
|
||||
"version": "v6.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/scheb/2fa-bundle.git",
|
||||
"reference": "6e51477c53070f27ac3e3d36be1a991870db415a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/6e51477c53070f27ac3e3d36be1a991870db415a",
|
||||
"reference": "6e51477c53070f27ac3e3d36be1a991870db415a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"symfony/config": "^5.4 || ^6.0",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.0",
|
||||
"symfony/event-dispatcher": "^5.4 || ^6.0",
|
||||
"symfony/framework-bundle": "^5.4 || ^6.0",
|
||||
"symfony/http-foundation": "^5.4 || ^6.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.0",
|
||||
"symfony/property-access": "^5.4 || ^6.0",
|
||||
"symfony/security-bundle": "^5.4 || ^6.0",
|
||||
"symfony/twig-bundle": "^5.4 || ^6.0"
|
||||
},
|
||||
"conflict": {
|
||||
"scheb/two-factor-bundle": "*",
|
||||
"symfony/security-core": "^7"
|
||||
},
|
||||
"suggest": {
|
||||
"scheb/2fa-backup-code": "Emergency codes when you have no access to other methods",
|
||||
"scheb/2fa-email": "Send codes by email",
|
||||
"scheb/2fa-google-authenticator": "Google Authenticator support",
|
||||
"scheb/2fa-totp": "Temporary one-time password (TOTP) support (Google Authenticator compatible)",
|
||||
"scheb/2fa-trusted-device": "Trusted devices support"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Scheb\\TwoFactorBundle\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Scheb",
|
||||
"email": "me@christianscheb.de"
|
||||
}
|
||||
],
|
||||
"description": "A generic interface to implement two-factor authentication in Symfony applications",
|
||||
"homepage": "https://github.com/scheb/2fa",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"Authentication",
|
||||
"symfony",
|
||||
"two-factor",
|
||||
"two-step"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/scheb/2fa-bundle/tree/v6.12.0"
|
||||
},
|
||||
"time": "2023-12-03T16:02:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scheb/2fa-google-authenticator",
|
||||
"version": "v6.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/scheb/2fa-google-authenticator.git",
|
||||
"reference": "2c43bbe432fdc465d8f1d1b2d73ca9ea5276fe34"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/scheb/2fa-google-authenticator/zipball/2c43bbe432fdc465d8f1d1b2d73ca9ea5276fe34",
|
||||
"reference": "2c43bbe432fdc465d8f1d1b2d73ca9ea5276fe34",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"paragonie/constant_time_encoding": "^2.4",
|
||||
"php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"scheb/2fa-bundle": "self.version",
|
||||
"spomky-labs/otphp": "^10.0 || ^11.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Scheb\\TwoFactorBundle\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Scheb",
|
||||
"email": "me@christianscheb.de"
|
||||
}
|
||||
],
|
||||
"description": "Extends scheb/2fa-bundle with two-factor authentication using Google Authenticator",
|
||||
"homepage": "https://github.com/scheb/2fa",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"Authentication",
|
||||
"google-authenticator",
|
||||
"symfony",
|
||||
"two-factor",
|
||||
"two-step"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/scheb/2fa-google-authenticator/tree/v6.12.0"
|
||||
},
|
||||
"time": "2023-12-03T15:44:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scheb/2fa-totp",
|
||||
"version": "v6.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/scheb/2fa-totp.git",
|
||||
"reference": "a233f4638b75941e97f089c4c917f6101f2983e3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/scheb/2fa-totp/zipball/a233f4638b75941e97f089c4c917f6101f2983e3",
|
||||
"reference": "a233f4638b75941e97f089c4c917f6101f2983e3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"paragonie/constant_time_encoding": "^2.4",
|
||||
"php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"scheb/2fa-bundle": "self.version",
|
||||
"spomky-labs/otphp": "^10.0 || ^11.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Scheb\\TwoFactorBundle\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Scheb",
|
||||
"email": "me@christianscheb.de"
|
||||
}
|
||||
],
|
||||
"description": "Extends scheb/2fa-bundle with two-factor authentication using TOTP",
|
||||
"homepage": "https://github.com/scheb/2fa",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"Authentication",
|
||||
"symfony",
|
||||
"totp",
|
||||
"two-factor",
|
||||
"two-step"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/scheb/2fa-totp/tree/v6.12.0"
|
||||
},
|
||||
"time": "2023-12-03T15:44:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "shivas/versioning-bundle",
|
||||
"version": "4.1.0",
|
||||
|
@ -5728,6 +6123,88 @@
|
|||
},
|
||||
"time": "2017-04-19T22:01:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spomky-labs/otphp",
|
||||
"version": "11.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Spomky-Labs/otphp.git",
|
||||
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
|
||||
"reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
|
||||
"php": ">=8.1",
|
||||
"psr/clock": "^1.0",
|
||||
"symfony/deprecation-contracts": "^3.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"ekino/phpstan-banned-code": "^1.0",
|
||||
"infection/infection": "^0.26|^0.27|^0.28|^0.29",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.0",
|
||||
"phpunit/phpunit": "^9.5.26|^10.0|^11.0",
|
||||
"qossmic/deptrac-shim": "^1.0",
|
||||
"rector/rector": "^1.0",
|
||||
"symfony/phpunit-bridge": "^6.1|^7.0",
|
||||
"symplify/easy-coding-standard": "^12.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"OTPHP\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Florent Morselli",
|
||||
"homepage": "https://github.com/Spomky"
|
||||
},
|
||||
{
|
||||
"name": "All contributors",
|
||||
"homepage": "https://github.com/Spomky-Labs/otphp/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
|
||||
"homepage": "https://github.com/Spomky-Labs/otphp",
|
||||
"keywords": [
|
||||
"FreeOTP",
|
||||
"RFC 4226",
|
||||
"RFC 6238",
|
||||
"google authenticator",
|
||||
"hotp",
|
||||
"otp",
|
||||
"totp"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Spomky-Labs/otphp/issues",
|
||||
"source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/Spomky",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/FlorentMorselli",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-12T11:22:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/asset",
|
||||
"version": "v6.4.8",
|
||||
|
|
|
@ -17,4 +17,5 @@ return [
|
|||
Nbgrp\OneloginSamlBundle\NbgrpOneloginSamlBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||
];
|
||||
|
|
|
@ -214,7 +214,7 @@ services:
|
|||
tags: [ { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } ]
|
||||
arguments:
|
||||
$routes:
|
||||
- path: '(/login$|/$|/auth$|/logout$|/logged-out$|/session-status|/auth/logout|/auth/login|/auth/session-status)'
|
||||
- path: '(/login$|/profile-auth/2fa/enable|/profile-auth/2fa/enable-finalize|/profile-auth|/2fa|/$|/auth$|/logout$|/logged-out$|/session-status|/auth/logout|/auth/login|/auth/session-status)'
|
||||
$cookieName: 'XSRF-TOKEN'
|
||||
$cookieExpire: 0
|
||||
$cookiePath: /
|
||||
|
@ -330,6 +330,22 @@ services:
|
|||
alias: App\Security\AppSecretGenerator
|
||||
public: true
|
||||
|
||||
api_success_handler:
|
||||
alias: App\Security\TwoFactor\AuthenticationSuccessHandler
|
||||
public: true
|
||||
|
||||
2fa_required:
|
||||
alias: App\Security\TwoFactor\TwoFactorAuthenticationRequiredHandler
|
||||
public: true
|
||||
|
||||
2fa_success:
|
||||
alias: App\Security\TwoFactor\TwoFactorAuthenticationSuccessHandler
|
||||
public: true
|
||||
|
||||
2fa_failed:
|
||||
alias: App\Security\TwoFactor\TwoFactorAuthenticationFailureHandler
|
||||
public: true
|
||||
|
||||
App\Process\Service\ActionNameMapperInterface: '@App\Engine\LegacyHandler\ActionNameMapperHandler'
|
||||
App\Process\Service\BaseActionDefinitionProviderInterface: '@App\Process\Service\BaseActionDefinitionProvider'
|
||||
App\Process\Service\BulkActionDefinitionProviderInterface: '@App\Process\Service\BulkActionDefinitionProvider'
|
||||
|
|
|
@ -3,7 +3,7 @@ doctrine:
|
|||
url: '%env(resolve:DATABASE_URL)%'
|
||||
driver: pdo_mysql
|
||||
charset: UTF8
|
||||
auto_commit: false
|
||||
auto_commit: true
|
||||
schema_filter: '/^(users|migration_versions|)$/'
|
||||
default_table_options:
|
||||
charset: utf8
|
||||
|
|
13
config/packages/scheb_2fa.yaml
Normal file
13
config/packages/scheb_2fa.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html
|
||||
scheb_two_factor:
|
||||
# two_factor_condition: acme.custom_two_factor_condition
|
||||
totp:
|
||||
enabled: true
|
||||
server_name: SuiteCRM # Server name used in QR code
|
||||
issuer: SuiteCRM # Issuer name used in QR code
|
||||
leeway: 0
|
||||
backup_codes:
|
||||
enabled: true
|
||||
security_tokens:
|
||||
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
|
||||
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
|
@ -87,10 +87,14 @@ return static function (ContainerConfigurator $containerConfig) {
|
|||
|
||||
//Note: Only the *first* access control that matches will be used
|
||||
$baseAccessControl = [
|
||||
['path' => '^/logged-out', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/logout$', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/login$', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/session-status$', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/logout$', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/logged-out', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/profile-auth/2fa/enable-finalize', 'roles' => 'ROLE_USER'],
|
||||
['path' => '^/profile-auth/2fa/check', 'roles' => 'IS_AUTHENTICATED_2FA_IN_PROGRESS'],
|
||||
['path' => '^/profile-auth/2fa/enable', 'roles' => 'ROLE_USER'],
|
||||
['path' => '^/profile-auth/2fa/disable', 'roles' => 'ROLE_USER'],
|
||||
['path' => '^/$', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/api', 'roles' => 'PUBLIC_ACCESS'],
|
||||
['path' => '^/api/graphql', 'roles' => 'PUBLIC_ACCESS'],
|
||||
|
@ -116,8 +120,10 @@ return static function (ContainerConfigurator $containerConfig) {
|
|||
$containerConfig->extension('security', [
|
||||
'firewalls' => array_merge_recursive($baseFirewall, [
|
||||
'main' => [
|
||||
'stateless' => false,
|
||||
'json_login' => [
|
||||
'check_path' => 'app_login',
|
||||
'success_handler' => 'api_success_handler'
|
||||
],
|
||||
'login_throttling' => [
|
||||
'limiter' => 'app.login_rate_limiter'
|
||||
|
@ -125,6 +131,18 @@ return static function (ContainerConfigurator $containerConfig) {
|
|||
'logout' => [
|
||||
'path' => 'app_logout',
|
||||
],
|
||||
'two_factor' => [
|
||||
'auth_form_path' => 'app_2fa_enable',
|
||||
'check_path' => 'app_2fa_check',
|
||||
'prepare_on_login' => true,
|
||||
'prepare_on_access_denied' => true,
|
||||
'auth_code_parameter_name' => '_auth_code',
|
||||
'default_target_path' => '/',
|
||||
'provider' => 'app_user_provider',
|
||||
'authentication_required_handler' => '2fa_required',
|
||||
'success_handler' => '2fa_success',
|
||||
'failure_handler' => '2fa_failed'
|
||||
]
|
||||
],
|
||||
]),
|
||||
'access_control' => $baseAccessControl
|
||||
|
|
|
@ -7,12 +7,12 @@ controllers:
|
|||
auth_controllers:
|
||||
resource:
|
||||
path: ../core/backend/Authentication/Controller/
|
||||
namespace: App\Authentication\Controller\
|
||||
namespace: App\Authentication\Controller
|
||||
type: attribute
|
||||
|
||||
engine_controllers:
|
||||
resource:
|
||||
path: ../core/backend/Engine/Controller/
|
||||
namespace: App\Engine\Controller\
|
||||
namespace: App\Engine\Controller
|
||||
type: attribute
|
||||
|
||||
|
|
|
@ -491,6 +491,7 @@ export * from './services/auth/auth.service';
|
|||
export * from './services/auth/error.interceptor';
|
||||
export * from './services/auth/install-auth-guard.service';
|
||||
export * from './services/auth/login-auth-guard.service';
|
||||
export * from './services/auth/two-factor-auth-guard.service';
|
||||
export * from './services/base-route/base-route.service';
|
||||
export * from './services/condition-operators/active-fields-checker.service';
|
||||
export * from './services/condition-operators/condition-operator.action';
|
||||
|
@ -720,3 +721,7 @@ export * from './views/record/store/record-pagination/record-pagination.service'
|
|||
export * from './views/record/store/record-pagination/record-pagination.store';
|
||||
export * from './views/record/store/record-view/record-view.store.model';
|
||||
export * from './views/record/store/record-view/record-view.store';
|
||||
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';
|
||||
|
|
44
core/app/core/src/lib/pipes/trust-html/trust-html.module.ts
Normal file
44
core/app/core/src/lib/pipes/trust-html/trust-html.module.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
|
||||
* Copyright (C) 2021 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 {TrustHtmlPipe} from './trust-html.pipe';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TrustHtmlPipe
|
||||
],
|
||||
exports: [
|
||||
TrustHtmlPipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class TrustHtmlModule {
|
||||
}
|
41
core/app/core/src/lib/pipes/trust-html/trust-html.pipe.ts
Normal file
41
core/app/core/src/lib/pipes/trust-html/trust-html.pipe.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
|
||||
* Copyright (C) 2021 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 {Pipe, PipeTransform} from '@angular/core';
|
||||
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'trustHtml'
|
||||
})
|
||||
export class TrustHtmlPipe implements PipeTransform {
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {
|
||||
}
|
||||
|
||||
transform(data): SafeHtml {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(data);
|
||||
}
|
||||
}
|
|
@ -68,7 +68,7 @@ export class AuthGuard {
|
|||
protected authorizeUser(route: ActivatedRouteSnapshot, snapshot: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
// Note: this session and acl are not always booleans
|
||||
return forkJoin([
|
||||
this.authorizeUserSession(route, snapshot),
|
||||
this.authService.authorizeUserSession(route, snapshot),
|
||||
this.authorizeUserACL(route)
|
||||
]).pipe(map(([session, acl]: any) => {
|
||||
|
||||
|
@ -141,87 +141,6 @@ export class AuthGuard {
|
|||
catchError(() => of(homeUrlTree))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize user session
|
||||
*
|
||||
* @returns {object} Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
|
||||
* @param {ActivatedRouteSnapshot} route information about the current route
|
||||
* @param snapshot
|
||||
*/
|
||||
protected authorizeUserSession(route: ActivatedRouteSnapshot, snapshot: RouterStateSnapshot):
|
||||
Observable<boolean | UrlTree> {
|
||||
|
||||
if (this.authService.isUserLoggedIn.value && route.data.checkSession !== true) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
let sessionExpiredUrl = this.authService.getSessionExpiredRoute();
|
||||
const redirect = this.authService.sessionExpiredRedirect();
|
||||
|
||||
const sessionExpiredUrlTree: UrlTree = this.router.parseUrl(sessionExpiredUrl);
|
||||
|
||||
return this.authService.fetchSessionStatus()
|
||||
.pipe(
|
||||
take(1),
|
||||
map((user: SessionStatus) => {
|
||||
|
||||
if (user && user.appStatus.installed === false) {
|
||||
return this.router.parseUrl('install');
|
||||
}
|
||||
|
||||
if (user && user.active === true) {
|
||||
const wasLoggedIn = !!this.appState.getCurrentUser();
|
||||
this.authService.setCurrentUser(user);
|
||||
|
||||
if (!wasLoggedIn) {
|
||||
this.language.appStrings$.pipe(
|
||||
filter(appStrings => appStrings && !emptyObject(appStrings)),
|
||||
tap(() => {
|
||||
setTimeout(() => {
|
||||
this.notificationStore.enableNotifications();
|
||||
this.notificationStore.refreshNotifications();
|
||||
}, 2000);
|
||||
}),
|
||||
take(1)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
if (user?.redirect?.route && (!snapshot.url.includes(user.redirect.route))) {
|
||||
const redirectUrlTree: UrlTree = this.router.parseUrl(user.redirect.route);
|
||||
redirectUrlTree.queryParams = user?.redirect?.queryParams ?? {}
|
||||
return redirectUrlTree;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
this.appState.setPreLoginUrl(snapshot.url);
|
||||
this.authService.resetState();
|
||||
|
||||
if (redirect) {
|
||||
this.authService.handleSessionExpiredRedirect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Re-direct to login
|
||||
return sessionExpiredUrlTree;
|
||||
}),
|
||||
catchError(() => {
|
||||
if (redirect) {
|
||||
this.authService.handleSessionExpiredRedirect();
|
||||
return of(false);
|
||||
}
|
||||
|
||||
this.authService.logout('LBL_SESSION_EXPIRED', false);
|
||||
return of(sessionExpiredUrlTree);
|
||||
}),
|
||||
tap((result: boolean | UrlTree) => {
|
||||
if (result === true) {
|
||||
this.authService.isUserLoggedIn.next(true);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -25,12 +25,12 @@
|
|||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
|
||||
import {HttpClient, HttpErrorResponse, HttpHeaders, HttpParams} from '@angular/common/http';
|
||||
import {BehaviorSubject, Observable, Subscription, throwError} from 'rxjs';
|
||||
import {catchError, distinctUntilChanged, filter, finalize, take} from 'rxjs/operators';
|
||||
import {BehaviorSubject, Observable, of, Subscription, throwError} from 'rxjs';
|
||||
import {catchError, distinctUntilChanged, filter, finalize, map, take, tap} from 'rxjs/operators';
|
||||
import {User} from '../../common/types/user';
|
||||
import {isTrue, isEmptyString} from '../../common/utils/value-utils';
|
||||
import {emptyObject, isEmptyString, isTrue} from '../../common/utils/value-utils';
|
||||
import {MessageService} from '../message/message.service';
|
||||
import {StateManager} from '../../store/state-manager';
|
||||
import {LanguageStore} from '../../store/language/language.store';
|
||||
|
@ -96,7 +96,8 @@ export class AuthService {
|
|||
username: string,
|
||||
password: string,
|
||||
onSuccess: (response: string) => void,
|
||||
onError: (error: HttpErrorResponse) => void
|
||||
onError: (error: HttpErrorResponse) => void,
|
||||
onTwoFactor: (error: HttpErrorResponse) => void
|
||||
): Subscription {
|
||||
let loginUrl = 'login';
|
||||
loginUrl = this.baseRoute.appendNativeAuth(loginUrl);
|
||||
|
@ -114,6 +115,12 @@ export class AuthService {
|
|||
{headers}
|
||||
).subscribe((response: any) => {
|
||||
|
||||
if (response?.two_factor_complete === 'false') {
|
||||
this.isUserLoggedIn.next(false);
|
||||
onTwoFactor(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.baseRoute.isNativeAuth()) {
|
||||
window.location.href = this.baseRoute.removeNativeAuth();
|
||||
}
|
||||
|
@ -170,6 +177,79 @@ export class AuthService {
|
|||
}
|
||||
}
|
||||
|
||||
public enable2fa(): Observable<any> {
|
||||
let route = './profile-auth/2fa/enable';
|
||||
route = this.baseRoute.appendNativeAuth(route);
|
||||
|
||||
route = this.baseRoute.calculateRoute(route);
|
||||
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
return this.http.get(route, {headers});
|
||||
}
|
||||
|
||||
public check2fa(code: string): Observable<any> {
|
||||
|
||||
let route = './profile-auth/2fa/check';
|
||||
|
||||
route = this.baseRoute.appendNativeAuth(route);
|
||||
route = this.baseRoute.calculateRoute(route);
|
||||
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
});
|
||||
|
||||
return this.http.post(route, {_auth_code: code}, {headers: headers});
|
||||
}
|
||||
|
||||
public finalize2fa(code: string): Observable<any> {
|
||||
|
||||
let route = './profile-auth/2fa/enable-finalize';
|
||||
|
||||
route = this.baseRoute.appendNativeAuth(route);
|
||||
route = this.baseRoute.calculateRoute(route);
|
||||
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
const body = JSON.stringify({_auth_code: code});
|
||||
|
||||
return this.http.post(route, {auth_code: code}, {headers: headers});
|
||||
}
|
||||
|
||||
setLanguage(result: any): void {
|
||||
this.languageStore.setSessionLanguage()
|
||||
.pipe(catchError(() => of({})))
|
||||
.subscribe(() => {
|
||||
if (result && result.redirect && result.redirect.route) {
|
||||
this.router.navigate(
|
||||
[result.redirect.route],
|
||||
{
|
||||
queryParams: result.redirect.queryParams ?? {}
|
||||
}).then();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.appStateStore.getPreLoginUrl()) {
|
||||
this.router.navigateByUrl(this.appStateStore.getPreLoginUrl()).then(() => {
|
||||
this.appStateStore.setPreLoginUrl('');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultModule = this.configs.getHomePage();
|
||||
this.router.navigate(['/' + defaultModule]).then();
|
||||
});
|
||||
|
||||
if (this.configs.getConfigValue('login_language')) {
|
||||
this.languageStore.setUserLanguage().subscribe();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call logout
|
||||
* @param logoutUrl
|
||||
|
@ -253,6 +333,85 @@ export class AuthService {
|
|||
return this.http.get(url, {headers});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize user session
|
||||
*
|
||||
* @returns {object} Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree
|
||||
* @param {ActivatedRouteSnapshot} route information about the current route
|
||||
* @param snapshot
|
||||
*/
|
||||
public authorizeUserSession(route: ActivatedRouteSnapshot, snapshot: RouterStateSnapshot):
|
||||
Observable<boolean | UrlTree> {
|
||||
|
||||
if (this.isUserLoggedIn.value && route.data.checkSession !== true) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
let sessionExpiredUrl = this.getSessionExpiredRoute();
|
||||
const redirect = this.sessionExpiredRedirect();
|
||||
|
||||
const sessionExpiredUrlTree: UrlTree = this.router.parseUrl(sessionExpiredUrl);
|
||||
|
||||
return this.fetchSessionStatus()
|
||||
.pipe(
|
||||
take(1),
|
||||
map((user: SessionStatus) => {
|
||||
|
||||
if (user && user.appStatus.installed === false) {
|
||||
return this.router.parseUrl('install');
|
||||
}
|
||||
|
||||
if (user && user.active === true) {
|
||||
const wasLoggedIn = !!this.appStateStore.getCurrentUser();
|
||||
this.setCurrentUser(user);
|
||||
|
||||
if (!wasLoggedIn) {
|
||||
this.languageStore.appStrings$.pipe(
|
||||
filter(appStrings => appStrings && !emptyObject(appStrings)),
|
||||
tap(() => {
|
||||
this.notificationStore.enableNotifications();
|
||||
this.notificationStore.refreshNotifications();
|
||||
}),
|
||||
take(1)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
if (user?.redirect?.route && (!snapshot.url.includes(user.redirect.route))) {
|
||||
const redirectUrlTree: UrlTree = this.router.parseUrl(user.redirect.route);
|
||||
redirectUrlTree.queryParams = user?.redirect?.queryParams ?? {}
|
||||
return redirectUrlTree;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
this.appStateStore.setPreLoginUrl(snapshot.url);
|
||||
this.resetState();
|
||||
|
||||
if (redirect) {
|
||||
this.handleSessionExpiredRedirect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Re-direct to login
|
||||
return sessionExpiredUrlTree;
|
||||
}),
|
||||
catchError(() => {
|
||||
if (redirect) {
|
||||
this.handleSessionExpiredRedirect();
|
||||
return of(false);
|
||||
}
|
||||
|
||||
this.logout('LBL_SESSION_EXPIRED', false);
|
||||
return of(sessionExpiredUrlTree);
|
||||
}),
|
||||
tap((result: boolean | UrlTree) => {
|
||||
if (result === true) {
|
||||
this.isUserLoggedIn.next(true);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route for session expired handling
|
||||
* @return string
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* 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 {ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree} from '@angular/router';
|
||||
import {Observable} from 'rxjs';
|
||||
import {AuthService} from './auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TwoFactorAuthGuard {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, snapshot: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
return this.authService.authorizeUserSession(route, snapshot);
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
<scrm-logo-ui></scrm-logo-ui>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" *ngIf="vm.showLanguages">
|
||||
<div class="form-row" *ngIf="vm.showLanguages && cardState !== '2fa'">
|
||||
<div class="col">
|
||||
<label class="" for="languages">{{vm.appStrings['LBL_LANGUAGE']}}</label>
|
||||
</div>
|
||||
|
@ -151,6 +151,20 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2fa Card-->
|
||||
<div class="fade-card-back col"
|
||||
*ngIf="cardState ==='2fa'"
|
||||
[@fade]>
|
||||
<div class="inner-addon left-addon">
|
||||
<scrm-2fa-check [class]="'login-button'"></scrm-2fa-check>
|
||||
</div>
|
||||
<div>
|
||||
<a class="back-link forgotten-password-link" (click)="returnToLogin()">
|
||||
{{vm.appStrings['LBL_BACK']}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -157,9 +157,14 @@ export class LoginUiComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
returnToLogin(): void {
|
||||
this.cardState = 'front';
|
||||
return;
|
||||
}
|
||||
|
||||
doLogin(): void {
|
||||
this.loading = true;
|
||||
this.auth.doLogin(this.uname, this.passw, this.onLoginSuccess.bind(this), this.onLoginError.bind(this));
|
||||
this.auth.doLogin(this.uname, this.passw, this.onLoginSuccess.bind(this), this.onLoginError.bind(this), this.onTwoFactor.bind(this));
|
||||
}
|
||||
|
||||
recoverPassword(): void {
|
||||
|
@ -192,32 +197,7 @@ export class LoginUiComponent implements OnInit {
|
|||
this.message.log('Login success');
|
||||
this.message.removeMessages();
|
||||
|
||||
this.languageStore.setSessionLanguage()
|
||||
.pipe(catchError(() => of({})))
|
||||
.subscribe(() => {
|
||||
if (result && result.redirect && result.redirect.route) {
|
||||
this.router.navigate(
|
||||
[result.redirect.route],
|
||||
{
|
||||
queryParams: result.redirect.queryParams ?? {}
|
||||
}).then();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.appState.getPreLoginUrl()) {
|
||||
this.router.navigateByUrl(this.appState.getPreLoginUrl()).then(() => {
|
||||
this.appState.setPreLoginUrl('');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultModule = this.configs.getHomePage();
|
||||
this.router.navigate(['/' + defaultModule]).then();
|
||||
});
|
||||
|
||||
if (this.configs.getConfigValue('login_language')) {
|
||||
this.languageStore.setUserLanguage().subscribe();
|
||||
}
|
||||
this.auth.setLanguage(result);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -241,6 +221,10 @@ export class LoginUiComponent implements OnInit {
|
|||
this.message.addDangerMessage(message);
|
||||
}
|
||||
|
||||
onTwoFactor(result: any): void {
|
||||
this.cardState = '2fa';
|
||||
}
|
||||
|
||||
protected getTooManyFailedMessage(defaultTooManyFailedMessage: string): string {
|
||||
let tooManyFailedMessage = this.languageStore.getFieldLabel('LOGIN_TOO_MANY_FAILED');
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import {AngularSvgIconModule} from 'angular-svg-icon';
|
|||
import {ButtonLoadingUiModule} from '../../../../directives/button-loading/button-loading.module';
|
||||
import {LogoUiModule} from '../../../../components/logo/logo.module';
|
||||
import {ImageModule} from '../../../../components/image/image.module';
|
||||
import {TwoFactorCheckModule} from "../../../2fa/components/2fa-check/2fa-check.module";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -49,7 +50,8 @@ import {ImageModule} from '../../../../components/image/image.module';
|
|||
CommonModule,
|
||||
AngularSvgIconModule,
|
||||
ImageModule,
|
||||
ButtonLoadingUiModule
|
||||
ButtonLoadingUiModule,
|
||||
TwoFactorCheckModule
|
||||
]
|
||||
})
|
||||
export class LoginUiModule {
|
||||
|
|
11
core/app/shell/src/themes/suite8/css/layout/_two-factor.scss
Normal file
11
core/app/shell/src/themes/suite8/css/layout/_two-factor.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
#two-factor {
|
||||
.two-factor-border {
|
||||
border-right: 0.03em solid grey;
|
||||
}
|
||||
|
||||
.backup-codes {
|
||||
background-color: $midnight-blue;
|
||||
color: white;
|
||||
border: 2px solid black;
|
||||
}
|
||||
}
|
|
@ -108,6 +108,7 @@
|
|||
@import 'layout/install';
|
||||
@import 'layout/admin';
|
||||
@import 'layout/scrollbar';
|
||||
@import 'layout/two-factor';
|
||||
|
||||
@import 'fields/email';
|
||||
@import 'fields/composite-mobile';
|
||||
|
|
|
@ -28,17 +28,29 @@
|
|||
|
||||
namespace App\Authentication\Controller;
|
||||
|
||||
use App\Data\LegacyHandler\PreparedStatementHandler;
|
||||
use App\Security\TwoFactor\BackupCodeGenerator;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Encoding\Encoding;
|
||||
use App\Authentication\LegacyHandler\Authentication;
|
||||
use App\Module\Users\Entity\User;
|
||||
use Endroid\QrCode\ErrorCorrectionLevel;
|
||||
use Endroid\QrCode\RoundBlockSizeMode;
|
||||
use Endroid\QrCode\Writer\SvgWriter;
|
||||
use RuntimeException;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
/**
|
||||
|
@ -57,20 +69,41 @@ class SecurityController extends AbstractController
|
|||
*/
|
||||
private $requestStack;
|
||||
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $entityManager;
|
||||
private PreparedStatementHandler $preparedStatementHandler;
|
||||
|
||||
private BackupCodeGenerator $backupCodeGenerator;
|
||||
|
||||
/**
|
||||
* SecurityController constructor.
|
||||
* @param Authentication $authentication
|
||||
* @param RequestStack $requestStack
|
||||
*/
|
||||
public function __construct(Authentication $authentication, RequestStack $requestStack)
|
||||
public function __construct(
|
||||
Authentication $authentication,
|
||||
RequestStack $requestStack,
|
||||
EntityManagerInterface $entityManager,
|
||||
PreparedStatementHandler $preparedStatementHandler,
|
||||
BackupCodeGenerator $backupCodeGenerator
|
||||
)
|
||||
{
|
||||
$this->authentication = $authentication;
|
||||
$this->requestStack = $requestStack;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->preparedStatementHandler = $preparedStatementHandler;
|
||||
$this->backupCodeGenerator = $backupCodeGenerator;
|
||||
}
|
||||
|
||||
#[Route('/login', name: 'app_login', methods: ["GET", "POST"])]
|
||||
public function login(AuthenticationUtils $authenticationUtils, #[CurrentUser] ?User $user): JsonResponse
|
||||
{
|
||||
// if ($user->getIsTotpEnabled()) {
|
||||
// return new Response('{"login_success": "true", "two_factor_complete": "false"}');
|
||||
// }
|
||||
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
$isAppInstalled = $this->authentication->getAppInstallStatus();
|
||||
$isAppInstallerLocked = $this->authentication->getAppInstallerLockStatus();
|
||||
|
@ -105,6 +138,108 @@ class SecurityController extends AbstractController
|
|||
return $this->json($data, Response::HTTP_OK);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
#[Route('/profile-auth/2fa/enable', name: 'app_2fa_enable', methods: ["GET", "POST"])]
|
||||
#[isGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function enable2fa(#[CurrentUser] ?User $user, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
// if ($user->getIsTotpEnabled()){
|
||||
// return new Response('');
|
||||
// }
|
||||
$secret = $totpAuthenticator->generateSecret();
|
||||
|
||||
$user->setTotpSecret($secret);
|
||||
|
||||
$backupCodes = $this->backupCodeGenerator->generate();
|
||||
|
||||
$this->setupBackupCodes($user,$backupCodes);
|
||||
|
||||
$this->preparedStatementHandler->update(
|
||||
'UPDATE users SET totp_secret = :totp_secret WHERE id = :id',
|
||||
['totp_secret' => $secret, 'id' => $user->getId()],
|
||||
[['param' => 'totp_secret', 'type' => 'string'], ['param' => 'id', 'type' => 'string']]
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$qrCodeUrl = $totpAuthenticator->getQRContent($user);
|
||||
|
||||
$response = [
|
||||
'url' => $qrCodeUrl,
|
||||
'svg' => $this->displayQRCode($qrCodeUrl),
|
||||
'backupCodes' => $backupCodes
|
||||
];
|
||||
|
||||
return new Response(json_encode($response), Response::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Route('/profile-auth/2fa/disable', name: 'app_2fa_disable', methods: ["GET"])]
|
||||
public function disable2fa(#[CurrentUser] ?User $user, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
error_log('inside disable 2fa');
|
||||
|
||||
$id = $user->getId();
|
||||
|
||||
$this->preparedStatementHandler->update(
|
||||
'UPDATE users SET totp_secret = NULL WHERE id = :id',
|
||||
['id' => $id],
|
||||
[['param' => 'id', 'type' => 'string']]
|
||||
);
|
||||
|
||||
return $this->redirect('../#/users/edit/'.$id);
|
||||
}
|
||||
|
||||
#[Route('/profile-auth/2fa/enable-finalize', name: 'app_2fa_enable_finalize', methods: ["GET", "POST"])]
|
||||
public function enableFinalize2fa(#[CurrentUser] ?User $user, Security $security, Request $request, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
error_log('inside enableFinalize2fa');
|
||||
|
||||
$maybe = $this->getUser();
|
||||
|
||||
$auth_code = $request->getPayload()->get('auth_code') ?? '';
|
||||
|
||||
$user_no = $security->getToken()->getUser();
|
||||
|
||||
$user_diff = $security->getUser();
|
||||
// $auth_code = $_POST['auth_code'] ?? null;
|
||||
|
||||
$correctCode = $totpAuthenticator->checkCode($user, $auth_code);
|
||||
|
||||
|
||||
if ($correctCode){
|
||||
$this->preparedStatementHandler->update(
|
||||
'UPDATE users SET is_totp_enabled = true WHERE id = :id',
|
||||
['id' => $user->getId()],
|
||||
[['param' => 'id', 'type' => 'string']]
|
||||
);
|
||||
}
|
||||
|
||||
$response = ['two_factor_setup_complete' => $correctCode];
|
||||
|
||||
return new Response(json_encode($response), Response::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Route('/profile-auth/2fa/check', name: 'app_2fa_check', methods: ["GET", "POST"])]
|
||||
public function check2fa(#[CurrentUser] ?User $user, Request $request, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
error_log('inside 2fa check');
|
||||
// request
|
||||
$auth_code = $request->getPayload()->get('auth_code') ?? '';
|
||||
|
||||
$correctCode = $totpAuthenticator->checkCode($user, $auth_code);
|
||||
|
||||
if (!$correctCode){
|
||||
$correctCode = $user->isBackupCode($auth_code);
|
||||
}
|
||||
|
||||
$response = ['two_factor_complete' => $correctCode];
|
||||
|
||||
return new Response(json_encode($response), Response::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Route('/logout', name: 'app_logout', methods: ["GET", "POST"])]
|
||||
public function logout(): void
|
||||
{
|
||||
|
@ -210,4 +345,34 @@ class SecurityController extends AbstractController
|
|||
'userName' => $userName
|
||||
];
|
||||
}
|
||||
|
||||
private function displayQrCode(string $qrCodeContent): string
|
||||
{
|
||||
// ErrorCorrectionLevelHigh
|
||||
$result = Builder::create()
|
||||
->writer(new SvgWriter())
|
||||
->writerOptions(['exclude_xml_declaration' => true])
|
||||
->data($qrCodeContent)
|
||||
->encoding(new Encoding('UTF-8'))
|
||||
->errorCorrectionLevel(ErrorCorrectionLevel::High)
|
||||
->size(200)
|
||||
->margin(0)
|
||||
->roundBlockSizeMode(RoundBlockSizeMode::Margin)
|
||||
->build();
|
||||
|
||||
return $result->getString();
|
||||
}
|
||||
|
||||
protected function setupBackupCodes($user, $backupCodes): void
|
||||
{
|
||||
error_log(print_r($backupCodes, true));
|
||||
error_log(print_r(array_keys($backupCodes), true));
|
||||
|
||||
// $backupCodes = json_encode();
|
||||
|
||||
$this->preparedStatementHandler->update("UPDATE users SET backup_codes = '{':backup_codes'}' WHERE id = :id",
|
||||
['id' => $user->getId(), 'backup_codes' => $backupCodes],
|
||||
[['param' => 'id', 'type' => 'string'], ['param' => 'backup_codes', 'type' => 'array']]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,4 +96,25 @@ class PreparedStatementHandler
|
|||
|
||||
return $stmt->executeQuery($params)->fetchAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function update (
|
||||
string $query,
|
||||
array $params,
|
||||
array $binds
|
||||
): int {
|
||||
$stmt = $this->entityManager->getConnection()->prepare($query);
|
||||
|
||||
if (!empty($binds)) {
|
||||
foreach ($binds as $bind) {
|
||||
$stmt->bindValue($bind['param'], $params[$bind['param']], $bind['type']);
|
||||
}
|
||||
}
|
||||
$result = $stmt->executeStatement();
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
36
core/backend/Migrations/Version20241001074858.php
Normal file
36
core/backend/Migrations/Version20241001074858.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20241001074858 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add Totp Secret Field to DB';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
/** @var EntityManagerInterface $entityManager */
|
||||
$entityManager = $this->container->get('entity_manager');
|
||||
|
||||
try {
|
||||
$entityManager->getConnection()->executeQuery('ALTER TABLE users ADD COLUMN `totp_secret` varchar NULL');
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Security\TwoFactor;
|
||||
|
||||
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
|
||||
{
|
||||
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
|
||||
{
|
||||
if ($token instanceof TwoFactorTokenInterface) {
|
||||
// Return the response to tell the client two-factor authentication is required.
|
||||
return new Response('{"login_success": "true", "two_factor_complete": "false"}');
|
||||
}
|
||||
|
||||
// $data = [
|
||||
// 'appStatus' => 'installed',
|
||||
// 'active' => true,
|
||||
// 'id' => 'seed_will_id',
|
||||
// 'firstName' => 'will',
|
||||
// 'lastName' => 'will',
|
||||
// 'userName' => 'will',
|
||||
// ];
|
||||
|
||||
$data = [
|
||||
'appStatus' => 'installed',
|
||||
'active' => true,
|
||||
'id' => '1',
|
||||
'firstName' => '',
|
||||
'lastName' => '',
|
||||
'userName' => 'admin',
|
||||
];
|
||||
|
||||
return new Response(json_encode($data));
|
||||
}
|
||||
}
|
42
core/backend/Security/TwoFactor/BackupCodeGenerator.php
Normal file
42
core/backend/Security/TwoFactor/BackupCodeGenerator.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?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\Security\TwoFactor;
|
||||
|
||||
class BackupCodeGenerator
|
||||
{
|
||||
public function generate($len = 10): array
|
||||
{
|
||||
$codes = [];
|
||||
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$codes[] = bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
return $codes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Security\TwoFactor;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
|
||||
class TwoFactorAuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
|
||||
{
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
error_log(' in onAuthenticationFailure');
|
||||
// Return the response to tell the client that 2fa failed. You may want to add more details
|
||||
// from the $exception.
|
||||
return new Response('{"error": "2fa_failed", "two_factor_complete": false}');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Security\TwoFactor;
|
||||
|
||||
use Scheb\TwoFactorBundle\Security\Http\Authentication\AuthenticationRequiredHandlerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
class TwoFactorAuthenticationRequiredHandler implements AuthenticationRequiredHandlerInterface
|
||||
{
|
||||
public function onAuthenticationRequired(Request $request, TokenInterface $token): Response
|
||||
{
|
||||
error_log(' in onAuthenticationRequired');
|
||||
// Return the response to tell the client that authentication hasn't completed yet and
|
||||
// two-factor authentication is required.
|
||||
return new Response('{"error": "access_denied", "two_factor_complete": false}');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Security\TwoFactor;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
class TwoFactorAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
|
||||
{
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
|
||||
{
|
||||
error_log(' in onAuthenticationSuccess');
|
||||
// Return the response to tell the client that authentication including two-factor
|
||||
// authentication is complete now.
|
||||
return new Response('{"login_success": true, "two_factor_complete": true}');
|
||||
}
|
||||
}
|
|
@ -35,6 +35,10 @@ use App\Module\Users\Repository\UserRepository;
|
|||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
|
||||
use Scheb\TwoFactorBundle\Model\BackupCodeInterface;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
@ -55,7 +59,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
|||
name: "idx_user_name",
|
||||
options: ['lengths' => [null, null, null, 30, 30]]
|
||||
)]
|
||||
class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUserInterface
|
||||
class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface, BackupCodeInterface
|
||||
{
|
||||
#[ApiProperty(
|
||||
identifier: true,
|
||||
|
@ -659,6 +663,28 @@ class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUs
|
|||
)]
|
||||
private ?string $factorAuthInterface;
|
||||
|
||||
#[ApiProperty(
|
||||
openapiContext: [
|
||||
'type' => 'string',
|
||||
'description' => 'totp secret',
|
||||
]
|
||||
)]
|
||||
#[ORM\Column(name: "totp_secret", type: 'string', length: 255, nullable: true, options: ["collation" => "utf8_general_ci"])]
|
||||
private ?string $totpSecret;
|
||||
|
||||
#[ORM\Column(
|
||||
name: "is_totp_enabled",
|
||||
type: "boolean",
|
||||
nullable: true
|
||||
)]
|
||||
private ?bool $isTotpEnabled = false;
|
||||
|
||||
#[ORM\Column(
|
||||
name: "backup_codes",
|
||||
type: "json",
|
||||
)]
|
||||
private ?array $backupCodes = [];
|
||||
|
||||
/**
|
||||
* @see UserInterface
|
||||
*/
|
||||
|
@ -1218,4 +1244,82 @@ class User implements UserInterface, EquatableInterface, PasswordAuthenticatedUs
|
|||
{
|
||||
return $this->getUserName();
|
||||
}
|
||||
|
||||
public function isTotpAuthenticationEnabled(): bool
|
||||
{
|
||||
return $this->totpSecret ? true : false;
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationUsername(): string
|
||||
{
|
||||
return $this->getUserName();
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
|
||||
{
|
||||
error_log('inside getTotpAuthenticationconfig');
|
||||
error_log($this->getTotpSecret());
|
||||
|
||||
// You could persist the other configuration options in the user entity to make it individual per user.
|
||||
return new TotpConfiguration($this->getTotpSecret(), TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
|
||||
public function getTotpSecret(): ?string
|
||||
{
|
||||
return $this->totpSecret;
|
||||
}
|
||||
|
||||
public function setTotpSecret(?string $totpSecret): self
|
||||
{
|
||||
$this->totpSecret = $totpSecret;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsTotpEnabled(): ?bool
|
||||
{
|
||||
return $this->isTotpEnabled;
|
||||
}
|
||||
|
||||
public function setIsTotpEnabled(?bool $isTotpEnabled): self
|
||||
{
|
||||
$this->isTotpEnabled = $isTotpEnabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it is a valid backup code.
|
||||
*/
|
||||
public function isBackupCode(string $code): bool
|
||||
{
|
||||
$correctCode = false;
|
||||
if (in_array($code, $backup)){
|
||||
$correctCode = true;
|
||||
$this->invalidateBackupCode($code);
|
||||
}
|
||||
return $correctCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a backup code
|
||||
*/
|
||||
public function invalidateBackupCode(string $code): void
|
||||
{
|
||||
$key = array_search($code, $this->backupCodes);
|
||||
if ($key !== false){
|
||||
unset($this->backupCodes[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a backup code
|
||||
*/
|
||||
public function addBackUpCode(string $backUpCode): void
|
||||
{
|
||||
if (!in_array($backUpCode, $this->backupCodes)) {
|
||||
$this->backupCodes[] = $backUpCode;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
13
symfony.lock
13
symfony.lock
|
@ -504,6 +504,19 @@
|
|||
"robrichards/xmlseclibs": {
|
||||
"version": "3.1.1"
|
||||
},
|
||||
"scheb/2fa-bundle": {
|
||||
"version": "6.12",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.0",
|
||||
"ref": "1e6f68089146853a790b5da9946fc5974f6fcd49"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/scheb_2fa.yaml",
|
||||
"config/routes/scheb_2fa.yaml"
|
||||
]
|
||||
},
|
||||
"scssphp/scssphp": {
|
||||
"version": "v1.7.0"
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue