From 0c794915816a7f266744e28727e20046f83d21aa Mon Sep 17 00:00:00 2001 From: Clemente Raposo Date: Thu, 30 Jun 2022 11:33:44 +0100 Subject: [PATCH] Use new symfony authenticator mechanism - Add auth success listener to initialize legacy session - Add password encoder to support legacy style encoding - Add xsrf-token check to json_login - enable authenticator_manager --- config/core_services.yaml | 11 +++ config/packages/security.yaml | 7 +- .../Security/AppJsonLoginAuthenticator.php | 50 +++++++++++ .../Security/LegacyPasswordEncoder.php | 89 +++++++++++++++++++ .../Security/LoginSuccessEventListener.php | 69 ++++++++++++++ 5 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 core/backend/Security/AppJsonLoginAuthenticator.php create mode 100644 core/backend/Security/LegacyPasswordEncoder.php create mode 100644 core/backend/Security/LoginSuccessEventListener.php diff --git a/config/core_services.yaml b/config/core_services.yaml index 1fa8c0766..c586b7789 100644 --- a/config/core_services.yaml +++ b/config/core_services.yaml @@ -136,6 +136,17 @@ services: App\Engine\Service\Extensions\ExtensionAssetCacheWarmupDecorator: decorates: 'cache_warmer' + security.authenticator.json_login: + class: App\Security\AppJsonLoginAuthenticator + abstract: true + arguments: + - '@security.http_utils' + - !abstract user provider + - !abstract authentication success handler + - !abstract authentication failure handler + - !abstract options + - '@?property_accessor' + App\Security\LegacySessionLogoutHandler: tags: - name: 'kernel.event_listener' diff --git a/config/packages/security.yaml b/config/packages/security.yaml index bc4f54a33..d7adc607c 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,7 +1,11 @@ security: + enable_authenticator_manager: true + encoders: + app_encoder: + id: App\Security\LegacyPasswordEncoder App\Module\Users\Entity\User: - algorithm: auto + id: App\Security\LegacyPasswordEncoder providers: app_user_provider: @@ -27,6 +31,7 @@ security: # Note: Only the *first* access control that matches will be used access_control: + - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/session-status$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/logout$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } diff --git a/core/backend/Security/AppJsonLoginAuthenticator.php b/core/backend/Security/AppJsonLoginAuthenticator.php new file mode 100644 index 000000000..27ca9399a --- /dev/null +++ b/core/backend/Security/AppJsonLoginAuthenticator.php @@ -0,0 +1,50 @@ +. + * + * 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; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; + +class AppJsonLoginAuthenticator extends JsonLoginAuthenticator +{ + /** + * @param Request $request + * @return PassportInterface + */ + public function authenticate(Request $request): PassportInterface + { + $passport = parent::authenticate($request); + + $csrfToken = $request->headers->get('x-xsrf-token'); + $passport->addBadge(new CsrfTokenBadge('angular', $csrfToken)); + + return $passport; + } +} diff --git a/core/backend/Security/LegacyPasswordEncoder.php b/core/backend/Security/LegacyPasswordEncoder.php new file mode 100644 index 000000000..5b644f1a2 --- /dev/null +++ b/core/backend/Security/LegacyPasswordEncoder.php @@ -0,0 +1,89 @@ +. + * + * 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; + +use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; + +class LegacyPasswordEncoder extends BasePasswordEncoder +{ + + /** + * @inheritDoc + */ + public function encodePassword($raw, $salt): string + { + if ($this->isPasswordTooLong($raw)) { + throw new BadCredentialsException('Invalid password.'); + } + + return password_hash(strtolower(md5($raw)), PASSWORD_DEFAULT); + } + + /** + * @inheritDoc + */ + public function isPasswordValid($encoded, $raw, $salt): bool + { + if ($this->isPasswordTooLong($raw)) { + return false; + } + + $userHash = $encoded; + $password = (md5($raw)); + + $valid = self::checkPasswordMD5($password, $userHash); + + if ($valid) { + return true; + } + + return false; + } + + /** + * Check that md5-encoded password matches existing hash + * @param string $passwordMd5 MD5-encoded password + * @param string $userHash DB hash + * @return bool Match or not? + */ + public static function checkPasswordMD5(string $passwordMd5, string $userHash): bool + { + if (empty($userHash)) { + return false; + } + + if ($userHash[0] !== '$' && strlen($userHash) === 32) { + $valid = strtolower($passwordMd5) === $userHash; + } else { + $valid = password_verify(strtolower($passwordMd5), $userHash); + } + + return $valid; + } +} diff --git a/core/backend/Security/LoginSuccessEventListener.php b/core/backend/Security/LoginSuccessEventListener.php new file mode 100644 index 000000000..0230786f5 --- /dev/null +++ b/core/backend/Security/LoginSuccessEventListener.php @@ -0,0 +1,69 @@ +. + * + * 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; + +use App\Authentication\LegacyHandler\Authentication; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; + +class LoginSuccessEventListener implements EventSubscriberInterface +{ + + /** + * @var Authentication + */ + private $authentication; + + public function __construct(Authentication $authentication) + { + $this->authentication = $authentication; + } + + public static function getSubscribedEvents(): array + { + return [ + LoginSuccessEvent::class => 'onLoginSuccess', + ]; + } + + public function onLoginSuccess(LoginSuccessEvent $event): void + { + if (null === $this->authentication) { + return; + } + + $user = $event->getUser(); + + $result = $this->authentication->initLegacyUserSession($user->getUsername()); + + if ($result === false) { + throw new CustomUserMessageAuthenticationException('Authentication: Invalid login credentials'); + } + } +}