From e273c1d7af0169ad29b807a0df13d3807d7859b2 Mon Sep 17 00:00:00 2001 From: Jaap Romijn Date: Tue, 10 Mar 2026 10:25:05 +0100 Subject: [PATCH] feat: upgrade to Symfony 6.4/7.0 and PHP 8.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PHP: ^7.3 → ^8.1 - All Symfony constraints: ^4.4 || ^5.0 → ^6.4 || ^7.0 - Remove symfony/security-guard (dropped in Symfony 6) - Remove symfony/templating (dropped in Symfony 6) - Remove sensio/framework-extra-bundle (dropped in Symfony 7) - Replace php-http/guzzle6-adapter with guzzle7-adapter - Update twig/twig to ^3.0 - Update haydenpierce/class-finder to ^0.5.0 - Update league/html-to-markdown to ^5.0 - Update gisostallenberg/response-content-negotiation-bundle to ^2.0 - Update rollerworks/password-strength-validator to ^1.7 - Update doctrine/collections to ^1.6 || ^2.0 - Update doctrine/orm to ^2.17 || ^3.0 - Update doctrine/persistence to ^2.0 || ^3.0 - Update doctrine/doctrine-bundle to ^2.7 - Update stof/doctrine-extensions-bundle to ^1.9 - Update php-http/httplug-bundle to ^1.16 || ^2.0 - Add symfony/password-hasher and symfony/deprecation-contracts - Update require-dev: phpstan ^1.0, phpunit ^10, rector ^1.0, php-cs-fixer ^3.0 - Remove deprecated require-dev packages (security-checker, composer-unused, composer-require-checker) - Update branch-alias to 3.0.x-dev Security: - Rewrite UserBundleAuthenticator to extend AbstractLoginFormAuthenticator (new authenticator system, replaces Guard) - Replace GuardAuthenticatorHandler with UserAuthenticatorInterface in AuthenticateUserSubscriber Controllers: - Replace @Route/@IsGranted docblock annotations with PHP 8 attributes - Use Symfony\Component\Routing\Attribute\Route (not Annotation\Route) - Use Symfony\Component\Security\Http\Attribute\IsGranted - Remove unused Session injection from RegistrationController and ResetController Password hashing: - Replace UserPasswordEncoderInterface with UserPasswordHasherInterface - Replace encodePassword() with hashPassword() throughout Session: - Replace SessionInterface injection with RequestStack in FlashSubscriber Services: - Update service wiring for new security services - Replace @security.user_password_encoder.generic with @security.password_hasher - Replace @security.authentication.guard_handler with @security.user_authenticator - Replace @session with @request_stack where needed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- composer.json | 101 +++++++--------- src/Controller/Account/AccountController.php | 46 ++++---- src/Controller/Account/ProfileController.php | 22 ++-- src/Controller/RegistrationController.php | 43 +++---- src/Controller/ResetController.php | 50 +++----- src/Controller/SecurityController.php | 17 ++- .../AuthenticateUserSubscriber.php | 37 +++--- src/EventSubscriber/CreateUserSubscriber.php | 8 +- src/EventSubscriber/FlashSubscriber.php | 14 +-- src/Resources/config/services.yaml | 12 +- src/Security/UserBundleAuthenticator.php | 110 ++++++------------ 11 files changed, 181 insertions(+), 279 deletions(-) diff --git a/composer.json b/composer.json index 9fe7897..81dfcb2 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "connectholland/user-bundle", "type": "symfony-bundle", - "description": "User bundle for Symfony 4 projects", + "description": "User bundle for Symfony 6/7 projects", "license": "MIT", "authors": [ { @@ -10,60 +10,54 @@ } ], "require": { - "php": "^7.3", + "php": "^8.1", "ext-json": "*", - "doctrine/collections": "^1.6", - "doctrine/doctrine-bundle": "^1.11 || ^2.0", - "doctrine/orm": "^2.6", - "doctrine/persistence": "^1.1 || ^2.0", - "gisostallenberg/response-content-negotiation-bundle": "^0.9", - "haydenpierce/class-finder": "^0.4.0", - "league/html-to-markdown": "^4.8", - "php-http/guzzle6-adapter": "^1.0 || ^2.0", - "php-http/httplug-bundle": "^1.16", - "rollerworks/password-strength-validator": "^1.2", - "sensio/framework-extra-bundle": "^5.5", - "stof/doctrine-extensions-bundle": "^1.3", - "symfony/config": "^4.4 || ^5.0", - "symfony/console": "^4.4 || ^5.0", - "symfony/css-selector": "^5.2", - "symfony/dependency-injection": "^4.4 || ^5.0", - "symfony/doctrine-bridge": "^4.4 || ^5.0", - "symfony/dom-crawler": "^4.4 || ^5.0", - "symfony/event-dispatcher": "^4.4 || ^5.0", - "symfony/event-dispatcher-contracts": "^1.1 || ^2.0", - "symfony/form": "^4.4 || ^5.0", - "symfony/framework-bundle": "^4.4 || ^5.0", - "symfony/http-foundation": "^4.4 || ^5.0", - "symfony/http-kernel": "^4.4 || ^5.0", - "symfony/mailer": "^4.4 || ^5.0", - "symfony/options-resolver": "^4.4 || ^5.0", - "symfony/routing": "^4.4 || ^5.0", - "symfony/security-bundle": "^4.4 || ^5.0", - "symfony/security-core": "^4.4 || ^5.0", - "symfony/security-csrf": "^4.4 || ^5.0", - "symfony/security-guard": "^4.4 || ^5.0", - "symfony/security-http": "^4.4 || ^5.0", - "symfony/templating": "^4.4 || ^5.0", - "symfony/translation": "^4.4 || ^5.0", - "symfony/twig-bundle": "^4.4 || ^5.0", - "symfony/validator": "^4.4 || ^5.0", - "twig/twig": "^2.0 || ^3.0" + "doctrine/collections": "^1.6 || ^2.0", + "doctrine/doctrine-bundle": "^2.7", + "doctrine/orm": "^2.17 || ^3.0", + "doctrine/persistence": "^2.0 || ^3.0", + "gisostallenberg/response-content-negotiation-bundle": "^2.0", + "haydenpierce/class-finder": "^0.5.0", + "league/html-to-markdown": "^5.0", + "php-http/guzzle7-adapter": "^1.0", + "php-http/httplug-bundle": "^1.16 || ^2.0", + "rollerworks/password-strength-validator": "^1.7", + "stof/doctrine-extensions-bundle": "^1.9", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/css-selector": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/deprecation-contracts": "^3.0", + "symfony/doctrine-bridge": "^6.4 || ^7.0", + "symfony/dom-crawler": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/event-dispatcher-contracts": "^3.0", + "symfony/form": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/mailer": "^6.4 || ^7.0", + "symfony/options-resolver": "^6.4 || ^7.0", + "symfony/password-hasher": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/security-core": "^6.4 || ^7.0", + "symfony/security-csrf": "^6.4 || ^7.0", + "symfony/security-http": "^6.4 || ^7.0", + "symfony/translation": "^6.4 || ^7.0", + "symfony/twig-bundle": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "twig/twig": "^3.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.0.1", - "friendsofphp/php-cs-fixer": "^2.15", + "friendsofphp/php-cs-fixer": "^3.0", "hwi/oauth-bundle": "^0.6.3 || ^1.0", - "icanhazstring/composer-unused": "^0.7.5", - "maglnet/composer-require-checker": "^2.0", "nikic/php-parser": "^4.9", - "php-http/guzzle6-adapter": "^1.0 || ^2.0", - "php-http/httplug-bundle": "^1.16", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^8.3", - "rector/rector": "^0.8.6", - "sensiolabs/security-checker": "^6.0", - "symfony/var-dumper": "^4.4 || ^5.0" + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^10", + "rector/rector": "^1.0", + "symfony/var-dumper": "^6.4 || ^7.0" }, "suggest": { "api-platform/api-pack": "Add api-platform/api-pack to add API support to the user bundle, run 'composer req api-pack' to install and follow api platform installation instructions.", @@ -79,16 +73,14 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { "psr-4": { "ConnectHolland\\UserBundle\\": "src" }, - "files": [ - "doctrine-persistence-bc-layer.php" - ] + "files": [] }, "autoload-dev": { "psr-4": { @@ -99,9 +91,6 @@ "prefer-stable": true, "scripts": { "analyse": [ - "composer unused --excludePackage=php-http/guzzle6-adapter --no-ansi", - "security-checker security:check", - "composer-require-checker --config-file=.composer-require-checker.json", "phpstan analyse --level=7 src/ tests/" ], "fix": [ diff --git a/src/Controller/Account/AccountController.php b/src/Controller/Account/AccountController.php index f347c3e..ef1c5b1 100644 --- a/src/Controller/Account/AccountController.php +++ b/src/Controller/Account/AccountController.php @@ -17,14 +17,14 @@ use GisoStallenberg\Bundle\ResponseContentNegotiationBundle\Content\ResultData; use GisoStallenberg\Bundle\ResponseContentNegotiationBundle\Content\ResultInterface; use GisoStallenberg\Bundle\ResponseContentNegotiationBundle\Content\ResultServiceLocatorInterface; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Twig\Environment; @@ -34,7 +34,7 @@ final class AccountController { /** - * @var UserPasswordEncoderInterface + * @var UserPasswordHasherInterface */ private $encoder; @@ -63,7 +63,7 @@ final class AccountController */ private $groups = ['account']; - public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $eventDispatcher, Environment $twig, ManagerRegistry $registry, TokenStorageInterface $tokenStorage) + public function __construct(UserPasswordHasherInterface $encoder, EventDispatcherInterface $eventDispatcher, Environment $twig, ManagerRegistry $registry, TokenStorageInterface $tokenStorage) { $this->encoder = $encoder; $this->eventDispatcher = $eventDispatcher; @@ -72,16 +72,14 @@ public function __construct(UserPasswordEncoderInterface $encoder, EventDispatch $this->tokenStorage = $tokenStorage; } - /** - * @Route( - * {"en"="/account/details", "nl"="/account/gegevens"}, - * name="connectholland_user_account_account", - * methods={"GET", "POST"}, - * defaults={"formName"="ConnectHolland\UserBundle\Form\Account\AccountType"} - * ) - * @Route("/api/account/details", name="connectholland_user_account_account.api", methods={"GET", "POST"}, defaults={"formName"="ConnectHolland\UserBundle\Form\Account\AccountType"}) - * @IsGranted("IS_AUTHENTICATED_FULLY") - */ + #[Route( + path: ['en' => '/account/details', 'nl' => '/account/gegevens'], + name: 'connectholland_user_account_account', + methods: ['GET', 'POST'], + defaults: ['formName' => 'ConnectHolland\UserBundle\Form\Account\AccountType'] + )] + #[Route(path: '/api/account/details', name: 'connectholland_user_account_account.api', methods: ['GET', 'POST'], defaults: ['formName' => 'ConnectHolland\UserBundle\Form\Account\AccountType'])] + #[IsGranted('IS_AUTHENTICATED_FULLY')] public function edit(ResultServiceLocatorInterface $resultServiceLocator, UserInterface $user, Request $request, FormInterface $form): ResultInterface { if ($form->isSubmitted() && $form->isValid()) { @@ -98,7 +96,7 @@ public function edit(ResultServiceLocatorInterface $resultServiceLocator, UserIn } if (!empty($plainPassword)) { - $password = $this->encoder->encodePassword($user, $plainPassword); + $password = $this->encoder->hashPassword($user, $plainPassword); $user->setPassword($password); } @@ -130,15 +128,13 @@ public function edit(ResultServiceLocatorInterface $resultServiceLocator, UserIn ); } - /** - * @Route( - * {"en"="/account/delete", "nl"="/account/verwijderen"}, - * name="connectholland_user_account_delete", - * methods={"GET", "POST"}, - * defaults={"formName"="ConnectHolland\UserBundle\Form\AccountDeleteType"} - * ) - * @IsGranted("IS_AUTHENTICATED_FULLY") - */ + #[Route( + path: ['en' => '/account/delete', 'nl' => '/account/verwijderen'], + name: 'connectholland_user_account_delete', + methods: ['GET', 'POST'], + defaults: ['formName' => 'ConnectHolland\UserBundle\Form\AccountDeleteType'] + )] + #[IsGranted('IS_AUTHENTICATED_FULLY')] public function delete(UserInterface $user, Request $request, FormInterface $form): Response { if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Controller/Account/ProfileController.php b/src/Controller/Account/ProfileController.php index c30e550..7c49fa1 100644 --- a/src/Controller/Account/ProfileController.php +++ b/src/Controller/Account/ProfileController.php @@ -14,10 +14,10 @@ use GisoStallenberg\Bundle\ResponseContentNegotiationBundle\Content\ResultData; use GisoStallenberg\Bundle\ResponseContentNegotiationBundle\Content\ResultInterface; use GisoStallenberg\Bundle\ResponseContentNegotiationBundle\Content\ResultServiceLocatorInterface; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Twig\Environment; @@ -44,16 +44,14 @@ public function __construct(EventDispatcherInterface $eventDispatcher, Environme $this->twig = $twig; } - /** - * @Route( - * {"en"="/account/profile", "nl"="/account/profiel"}, - * name="connectholland_user_account_profile", - * methods={"GET", "POST"}, - * defaults={"formName"="ConnectHolland\UserBundle\Form\Account\ProfileType" - * }) - * @Route("/api/account/profile", name="connectholland_user_account_profile.api", methods={"GET", "POST"}, defaults={"formName"="ConnectHolland\UserBundle\Form\Account\ProfileType"}) - * @IsGranted("IS_AUTHENTICATED_FULLY") - */ + #[Route( + path: ['en' => '/account/profile', 'nl' => '/account/profiel'], + name: 'connectholland_user_account_profile', + methods: ['GET', 'POST'], + defaults: ['formName' => 'ConnectHolland\UserBundle\Form\Account\ProfileType'] + )] + #[Route(path: '/api/account/profile', name: 'connectholland_user_account_profile.api', methods: ['GET', 'POST'], defaults: ['formName' => 'ConnectHolland\UserBundle\Form\Account\ProfileType'])] + #[IsGranted('IS_AUTHENTICATED_FULLY')] public function edit(ResultServiceLocatorInterface $resultServiceLocator, Request $request, FormInterface $form): ResultInterface { if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index 4e2a022..7b62fde 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -27,9 +27,8 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\UriSigner; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\RouterInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -43,11 +42,6 @@ final class RegistrationController */ private $registry; - /** - * @var Session - */ - private $session; - /** * @var EventDispatcherInterface */ @@ -58,26 +52,21 @@ final class RegistrationController */ private $router; - /** - * @param Session $session - */ - public function __construct(ManagerRegistry $registry, Session $session, EventDispatcherInterface $eventDispatcher, RouterInterface $router) + public function __construct(ManagerRegistry $registry, EventDispatcherInterface $eventDispatcher, RouterInterface $router) { $this->registry = $registry; - $this->session = $session; $this->eventDispatcher = $eventDispatcher; $this->router = $router; } + #[Route( + path: ['en' => '/register', 'nl' => '/registreren'], + name: 'connectholland_user_registration', + methods: ['GET', 'POST'], + defaults: ['formName' => 'ConnectHolland\UserBundle\Form\RegistrationType'] + )] + #[Route(path: '/api/register', name: 'connectholland_user_registration.api', methods: ['GET', 'POST'], defaults: ['formName' => 'ConnectHolland\UserBundle\Form\RegistrationType'])] /** - * @Route( - * {"en"="/register", "nl"="/registreren"}, - * name="connectholland_user_registration", - * methods={"GET", "POST"}, - * defaults={"formName"="ConnectHolland\UserBundle\Form\RegistrationType"} - * ) - * @Route("/api/register", name="connectholland_user_registration.api", methods={"GET", "POST"}, defaults={"formName"="ConnectHolland\UserBundle\Form\RegistrationType"}) - * * @param FormInterface $form */ public function register(ResultServiceLocatorInterface $resultServiceLocator, Request $request, FormInterface $form): ResultInterface @@ -120,14 +109,12 @@ public function register(ResultServiceLocatorInterface $resultServiceLocator, Re ); } - /** - * @Route( - * {"en"="/register/confirm/{email}/{token}", "nl"="/registreren/bevestigen/{email}/{token}"}, - * name="connectholland_user_registration_confirm", - * methods={"GET", "POST"} - * ) - * @Route("/api/register/confirm/{email}/{token}", name="connectholland_user_registration_confirm.api", methods={"GET", "POST"}) - */ + #[Route( + path: ['en' => '/register/confirm/{email}/{token}', 'nl' => '/registreren/bevestigen/{email}/{token}'], + name: 'connectholland_user_registration_confirm', + methods: ['GET', 'POST'] + )] + #[Route(path: '/api/register/confirm/{email}/{token}', name: 'connectholland_user_registration_confirm.api', methods: ['GET', 'POST'])] public function registrationConfirm(Request $request, string $email, string $token, UriSigner $uriSigner): Response { /** @var UserRepository $userRepository */ diff --git a/src/Controller/ResetController.php b/src/Controller/ResetController.php index 7de388f..ead6f5b 100644 --- a/src/Controller/ResetController.php +++ b/src/Controller/ResetController.php @@ -28,11 +28,10 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\UriSigner; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\RouterInterface; -use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Twig\Environment; @@ -49,11 +48,6 @@ final class ResetController */ private $registry; - /** - * @var Session - */ - private $session; - /** * @var EventDispatcherInterface */ @@ -69,27 +63,22 @@ final class ResetController */ private $twig; - /** - * @param Session $session - */ - public function __construct(ManagerRegistry $registry, Session $session, EventDispatcherInterface $eventDispatcher, RouterInterface $router, Environment $twig) + public function __construct(ManagerRegistry $registry, EventDispatcherInterface $eventDispatcher, RouterInterface $router, Environment $twig) { $this->registry = $registry; - $this->session = $session; $this->eventDispatcher = $eventDispatcher; $this->router = $router; $this->twig = $twig; } + #[Route( + path: ['en' => '/password-reset', 'nl' => '/wachtwoord-vergeten'], + name: 'connectholland_user_reset', + methods: ['GET', 'POST'], + defaults: ['formName' => 'ConnectHolland\UserBundle\Form\ResetType'] + )] + #[Route(path: '/api/account/password-reset', name: 'connectholland_user_reset.api', methods: ['GET', 'POST'], defaults: ['formName' => 'ConnectHolland\UserBundle\Form\ResetType'])] /** - * @Route( - * {"en"="/password-reset", "nl"="/wachtwoord-vergeten"}, - * name="connectholland_user_reset", - * methods={"GET", "POST"}, - * defaults={"formName"="ConnectHolland\UserBundle\Form\ResetType"} - * ) - * @Route("/api/account/password-reset", name="connectholland_user_reset.api", methods={"GET", "POST"}, defaults={"formName"="ConnectHolland\UserBundle\Form\ResetType"}) - * * @param FormInterface $form */ public function reset(ResultServiceLocatorInterface $resultServiceLocator, Request $request, FormInterface $form, FormFactoryInterface $formFactory): ResultInterface @@ -131,15 +120,14 @@ public function reset(ResultServiceLocatorInterface $resultServiceLocator, Reque ); } + #[Route( + path: ['en' => '/password-reset/{email}/{token}', 'nl' => '/wachtwoord-vergeten/{email}/{token}'], + name: 'connectholland_user_reset_confirm', + methods: ['GET', 'POST'], + defaults: ['formName' => 'ConnectHolland\UserBundle\Form\NewPasswordType'] + )] + #[Route(path: '/api/password-reset-confirm/{email}/{token}', name: 'connectholland_user_reset_confirm.api', methods: ['GET', 'POST'], defaults: ['formName' => 'ConnectHolland\UserBundle\Form\NewPasswordType'])] /** - * @Route( - * {"en"="/password-reset/{email}/{token}", "nl"="/wachtwoord-vergeten/{email}/{token}"}, - * name="connectholland_user_reset_confirm", - * methods={"GET", "POST"}, - * defaults={"formName"="ConnectHolland\UserBundle\Form\NewPasswordType"} - * ) - * @Route("/api/password-reset-confirm/{email}/{token}", name="connectholland_user_reset_confirm.api", methods={"GET", "POST"}, defaults={"formName"="ConnectHolland\UserBundle\Form\NewPasswordType"}) - * * @param FormInterface $form */ public function resetPassword( @@ -148,7 +136,7 @@ public function resetPassword( string $email, string $token, FormInterface $form, - UserPasswordEncoderInterface $encoder, + UserPasswordHasherInterface $encoder, UriSigner $uriSigner ): Response { if ($uriSigner->check(sprintf('%s://%s%s', $request->getScheme(), $request->getHttpHost(), $request->getRequestUri())) === false) { @@ -170,7 +158,7 @@ public function resetPassword( if ($form->isSubmitted() && $form->isValid()) { $plainPassword = $form->get('password')->getData(); - $password = $encoder->encodePassword($user, $plainPassword); + $password = $encoder->hashPassword($user, $plainPassword); $user->setPassword($password); $user->setPasswordRequestToken(null); diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index a417747..4f96210 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -12,7 +12,7 @@ use ConnectHolland\UserBundle\Form\LoginType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Twig\Environment; @@ -43,15 +43,12 @@ public function __construct(AuthenticationUtils $authenticationUtils, FormFactor $this->twig = $twig; } - /** - * @Route({ - * "en"="/login", - * "nl"="/inloggen"}, - * name="connectholland_user_login", - * methods={"GET", "POST"} - * ) - * @Route("/api/authenticate", name="connectholland_user_login.api", methods={"GET", "POST"}) - */ + #[Route( + path: ['en' => '/login', 'nl' => '/inloggen'], + name: 'connectholland_user_login', + methods: ['GET', 'POST'] + )] + #[Route(path: '/api/authenticate', name: 'connectholland_user_login.api', methods: ['GET', 'POST'])] public function __invoke(): Response { $form = $this->formFactory->create(LoginType::class); diff --git a/src/EventSubscriber/AuthenticateUserSubscriber.php b/src/EventSubscriber/AuthenticateUserSubscriber.php index 33f714b..a6be978 100644 --- a/src/EventSubscriber/AuthenticateUserSubscriber.php +++ b/src/EventSubscriber/AuthenticateUserSubscriber.php @@ -11,42 +11,31 @@ use ConnectHolland\UserBundle\Event\AuthenticateUserEventInterface; use ConnectHolland\UserBundle\UserBundleEvents; -use Symfony\Component\Security\Guard\AuthenticatorInterface; -use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; final class AuthenticateUserSubscriber implements AuthenticateUserSubscriberInterface { - /** - * @var GuardAuthenticatorHandler - */ - private $guardAuthenticatorHandler; - - /** - * @var AuthenticatorInterface - */ - private $authenticator; - - public function __construct(GuardAuthenticatorHandler $guardAuthenticatorHandler, AuthenticatorInterface $authenticator) - { - $this->guardAuthenticatorHandler = $guardAuthenticatorHandler; - $this->authenticator = $authenticator; - } + public function __construct( + private readonly UserAuthenticatorInterface $userAuthenticator, + private readonly AuthenticatorInterface $authenticator, + ) {} public function onAuthenticateUser(AuthenticateUserEventInterface $event): void { - $providerKey = 'main'; // TODO: Make configurable or read from request - - $token = $this->authenticator->createAuthenticatedToken($event->getUser(), $providerKey); - - $this->guardAuthenticatorHandler->authenticateWithToken($token, $event->getRequest(), $providerKey); + $response = $this->userAuthenticator->authenticateUser( + $event->getUser(), + $this->authenticator, + $event->getRequest() + ); - $event->setResponse($this->guardAuthenticatorHandler->handleAuthenticationSuccess($token, $event->getRequest(), $this->authenticator, $providerKey)); + $event->setResponse($response); } /** * @codeCoverageIgnore No need to test this array 'config' method */ - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ UserBundleEvents::AUTHENTICATE_USER => 'onAuthenticateUser', diff --git a/src/EventSubscriber/CreateUserSubscriber.php b/src/EventSubscriber/CreateUserSubscriber.php index 2e1053f..1adaf47 100644 --- a/src/EventSubscriber/CreateUserSubscriber.php +++ b/src/EventSubscriber/CreateUserSubscriber.php @@ -14,7 +14,7 @@ use ConnectHolland\UserBundle\UserBundleEvents; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; -use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; /** * @codeCoverageIgnore WIP @@ -22,7 +22,7 @@ final class CreateUserSubscriber implements CreateUserSubscriberInterface { /** - * @var UserPasswordEncoderInterface + * @var UserPasswordHasherInterface */ private $passwordEncoder; @@ -31,7 +31,7 @@ final class CreateUserSubscriber implements CreateUserSubscriberInterface */ private $registry; - public function __construct(UserPasswordEncoderInterface $passwordEncoder, ManagerRegistry $registry) + public function __construct(UserPasswordHasherInterface $passwordEncoder, ManagerRegistry $registry) { $this->passwordEncoder = $passwordEncoder; $this->registry = $registry; @@ -41,7 +41,7 @@ public function onCreateUser(CreateUserEventInterface $event): void { $user = $event->getUser(); $user->setPassword( - $this->passwordEncoder->encodePassword($user, $event->getPlainPassword()) + $this->passwordEncoder->hashPassword($user, $event->getPlainPassword()) ); if ($user->isEnabled() === false) { $user->setPasswordRequestToken(bin2hex(random_bytes(32))); diff --git a/src/EventSubscriber/FlashSubscriber.php b/src/EventSubscriber/FlashSubscriber.php index d158852..2ad81bb 100644 --- a/src/EventSubscriber/FlashSubscriber.php +++ b/src/EventSubscriber/FlashSubscriber.php @@ -11,26 +11,26 @@ use ConnectHolland\UserBundle\UserBundleEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Contracts\EventDispatcher\Event; use Symfony\Contracts\Translation\TranslatorInterface; class FlashSubscriber implements EventSubscriberInterface { /** - * @var SessionInterface + * @var RequestStack */ - private $session; + private $requestStack; /** * @var TranslatorInterface */ private $translator; - public function __construct(SessionInterface $session, TranslatorInterface $translator) + public function __construct(RequestStack $requestStack, TranslatorInterface $translator) { - $this->session = $session; - $this->translator = $translator; + $this->requestStack = $requestStack; + $this->translator = $translator; } /** @@ -49,7 +49,7 @@ public static function getSubscribedEvents(): array public function addFlashMessage(Event $event): void { $message = $this->translate(sprintf('connectholland_user.flash_message.%s.flash.%s', $event->getAction(), $event->getState())); - $this->session->getFlashBag()->add($event->getState(), $message); + $this->requestStack->getSession()->getFlashBag()->add($event->getState(), $message); } private function translate(string $message, array $parameters = []): string diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 416550a..e84eea6 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -50,13 +50,10 @@ services: - '@Doctrine\Persistence\ManagerRegistry' - '@router' - '@security.csrf.token_manager' - - '@security.user_password_encoder.generic' - - '@event_dispatcher' ConnectHolland\UserBundle\Controller\RegistrationController: arguments: - '@Doctrine\Persistence\ManagerRegistry' - - '@session' - '@event_dispatcher' - '@router' - '@twig' @@ -72,7 +69,7 @@ services: ConnectHolland\UserBundle\Controller\Account\AccountController: arguments: - - '@security.user_password_encoder.generic' + - '@security.password_hasher' - '@event_dispatcher' - '@twig' - '@Doctrine\Persistence\ManagerRegistry' @@ -83,7 +80,6 @@ services: ConnectHolland\UserBundle\Controller\ResetController: arguments: - '@Doctrine\Persistence\ManagerRegistry' - - '@session' - '@event_dispatcher' - '@router' - '@twig' @@ -115,7 +111,7 @@ services: ConnectHolland\UserBundle\EventSubscriber\AuthenticateUserSubscriber: arguments: - - '@security.authentication.guard_handler' + - '@security.user_authenticator' - '@ConnectHolland\UserBundle\Security\UserBundleAuthenticator' tags: - { name: kernel.event_subscriber } @@ -136,7 +132,7 @@ services: ConnectHolland\UserBundle\EventSubscriber\CreateUserSubscriber: arguments: - - '@security.user_password_encoder.generic' + - '@security.password_hasher' - '@Doctrine\Persistence\ManagerRegistry' tags: - { name: kernel.event_subscriber } @@ -168,7 +164,7 @@ services: ConnectHolland\UserBundle\EventSubscriber\FlashSubscriber: arguments: - - '@session' + - '@request_stack' - '@translator' tags: - { name: kernel.event_subscriber } diff --git a/src/Security/UserBundleAuthenticator.php b/src/Security/UserBundleAuthenticator.php index 1b8d812..63956aa 100644 --- a/src/Security/UserBundleAuthenticator.php +++ b/src/Security/UserBundleAuthenticator.php @@ -9,113 +9,75 @@ namespace ConnectHolland\UserBundle\Security; +use ConnectHolland\UserBundle\Entity\UserInterface; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; use Symfony\Component\Security\Core\Security; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Util\TargetPathTrait; /** * @codeCoverageIgnore WIP */ -final class UserBundleAuthenticator extends AbstractFormLoginAuthenticator +final class UserBundleAuthenticator extends AbstractLoginFormAuthenticator { use TargetPathTrait; - /** - * @var ManagerRegistry - */ - private $registry; + public function __construct( + private readonly ManagerRegistry $registry, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly CsrfTokenManagerInterface $csrfTokenManager, + ) {} - /** - * @var UrlGeneratorInterface - */ - private $urlGenerator; - - /** - * @var CsrfTokenManagerInterface - */ - private $csrfTokenManager; - - /** - * @var UserPasswordEncoderInterface - */ - private $passwordEncoder; - - public function __construct(ManagerRegistry $registry, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) - { - $this->registry = $registry; - $this->urlGenerator = $urlGenerator; - $this->csrfTokenManager = $csrfTokenManager; - $this->passwordEncoder = $passwordEncoder; - } - - public function supports(Request $request) + public function authenticate(Request $request): Passport { - return 'connectholland_user_login' === $request->attributes->get('_route') && $request->isMethod('POST'); - } + $email = $request->request->getString('_username'); + $password = $request->request->getString('_password'); + $csrfToken = $request->request->getString('_token'); - public function getCredentials(Request $request) - { - $credentials = [ - 'username' => $request->request->get('_username'), - 'password' => $request->request->get('_password'), - 'csrf_token' => $request->request->get('_token'), - ]; - - $session = $request->getSession(); - if (is_null($session) === false) { - $session->set(Security::LAST_USERNAME, $credentials['username']); - } + $request->getSession()?->set(Security::LAST_USERNAME, $email); - return $credentials; - } - - public function getUser($credentials, UserProviderInterface $userProvider) - { - $token = new CsrfToken('authenticate', $credentials['csrf_token']); - if (!$this->csrfTokenManager->isTokenValid($token)) { + if (!$this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) { throw new InvalidCsrfTokenException(); } - /** @var UserInterface|null $user */ - $user = $this->registry->getRepository(\ConnectHolland\UserBundle\Entity\UserInterface::class)->findOneBy(['email' => $credentials['username'], 'enabled' => true]); - - if ($user instanceof UserInterface === false) { - // fail authentication with a custom error - throw new CustomUserMessageAuthenticationException('Email could not be found.'); - } - - return $user; - } - - public function checkCredentials($credentials, UserInterface $user) - { - return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + return new Passport( + new UserBadge($email, function (string $userIdentifier) { + $user = $this->registry->getRepository(UserInterface::class) + ->findOneBy(['email' => $userIdentifier, 'enabled' => true]); + if ($user === null) { + throw new CustomUserMessageAuthenticationException('Email could not be found.'); + } + + return $user; + }), + new PasswordCredentials($password), + [new CsrfTokenBadge('authenticate', $csrfToken)] + ); } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { - $session = $request->getSession(); - if ($session instanceof SessionInterface && $targetPath = $this->getTargetPath($session, $providerKey)) { + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { return new RedirectResponse($targetPath); } - return new RedirectResponse('/'); // No target path configured, just go to / + return new RedirectResponse('/'); } - protected function getLoginUrl() + protected function getLoginUrl(Request $request): string { return $this->urlGenerator->generate('connectholland_user_login'); }