X-Git-Url: https://git.rapsys.eu/userbundle/blobdiff_plain/15c15d969c9161e0a10515ff91857e687ab6f60a..5478fe44867886d37c941ec81c94fb9afc3d7c39:/Handler/AuthenticationFailureHandler.php diff --git a/Handler/AuthenticationFailureHandler.php b/Handler/AuthenticationFailureHandler.php index a8ac5a4..7e7c132 100644 --- a/Handler/AuthenticationFailureHandler.php +++ b/Handler/AuthenticationFailureHandler.php @@ -12,7 +12,14 @@ namespace Rapsys\UserBundle\Handler; use Doctrine\Persistence\ManagerRegistry; + use Psr\Log\LoggerInterface; + +use Rapsys\PackBundle\Util\SluggerUtil; + +use Rapsys\UserBundle\Exception\UnactivatedException; +use Rapsys\UserBundle\RapsysUserBundle; + use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -29,14 +36,12 @@ use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\DisabledException; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler; use Symfony\Component\Security\Http\HttpUtils; use Symfony\Contracts\Translation\TranslatorInterface; -use Rapsys\PackBundle\Util\SluggerUtil; -use Rapsys\UserBundle\RapsysUserBundle; - /** * {@inheritdoc} */ @@ -45,7 +50,6 @@ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler { * Config array */ protected array $config; - protected array $options; protected array $defaultOptions = [ 'failure_path' => null, 'failure_forward' => false, @@ -54,36 +58,8 @@ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler { ]; /** - * Doctrine instance - */ - protected ManagerRegistry $doctrine; - - /** - * MailerInterface - */ - protected MailerInterface $mailer; - - /** - * Router instance - */ - protected RouterInterface $router; - - /** - * Slugger instance - */ - protected SluggerUtil $slugger; - - /** - * RequestStack instance - */ - protected RequestStack $stack; - - /** - * Translator instance - */ - protected TranslatorInterface $translator; - - /** + * {@inheritdoc} + * * @xxx Second argument will be replaced by security.firewalls.main.logout.target * @see vendor/symfony/security-bundle/DependencyInjection/SecurityExtension.php +360 * @@ -98,33 +74,13 @@ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler { * @param SluggerUtil $slugger The slugger instance * @param RequestStack $stack The stack instance * @param TranslatorInterface $translator The translator instance - * - * {@inheritdoc} */ - public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options, LoggerInterface $logger, ContainerInterface $container, ManagerRegistry $doctrine, MailerInterface $mailer, RouterInterface $router, SluggerUtil $slugger, RequestStack $stack, TranslatorInterface $translator) { - //Set config - $this->config = $container->getParameter(self::getAlias()); - - //Set doctrine - $this->doctrine = $doctrine; - - //Set mailer - $this->mailer = $mailer; - - //Set router - $this->router = $router; - - //Set slugger - $this->slugger = $slugger; - - //Set stack - $this->stack = $stack; - - //Set translator - $this->translator = $translator; - + public function __construct(protected HttpKernelInterface $httpKernel, protected HttpUtils $httpUtils, protected array $options, protected ?LoggerInterface $logger, protected ContainerInterface $container, protected ManagerRegistry $doctrine, protected MailerInterface $mailer, protected RouterInterface $router, protected SluggerUtil $slugger, protected RequestStack $stack, protected TranslatorInterface $translator) { //Call parent constructor parent::__construct($httpKernel, $httpUtils, $options, $logger); + + //Set config + $this->config = $container->getParameter(RapsysUserBundle::getAlias()); } /** @@ -147,295 +103,203 @@ class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler { } /** - * This is called when an interactive authentication attempt fails - * - * User may retrieve mail + field + hash for each unactivated/locked accounts - * * {@inheritdoc} + * + * This is called when an interactive authentication attempt fails */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { //With bad credential exception if ($exception instanceof BadCredentialsException) { //With parent exception if (($parent = $exception->getPrevious()) instanceof UserNotFoundException) { - //Retrieve login - //TODO: check form _token validity ??? - if ( - !empty($login = $request->get('login')) && - !empty($mail = $login['mail']) - ) { - //Set extra parameters - $extra = ['mail' => $smail = $this->slugger->short($mail), 'field' => $sfield = $this->slugger->serialize([]), 'hash' => $this->slugger->hash($smail.$sfield)]; - - //With failure target path option - if (!empty($failurePath = $this->options['failure_path'])) { - //With path - if ($failurePath[0] == '/') { - //Create login path request instance - $req = Request::create($failurePath); - - //Get login path pathinfo - $path = $req->getPathInfo(); - - //Remove script name - $path = str_replace($request->getScriptName(), '', $path); - - //Try with login path path - try { - //Save old context - $oldContext = $this->router->getContext(); - - //Force clean context - //XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST - //XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42 - $this->router->setContext(new RequestContext()); - - //Retrieve route matching path - $route = $this->router->match($path); - - //Reset context - $this->router->setContext($oldContext); - - //Clear old context - unset($oldContext); - - //With route name - if ($name = $route['_route']) { - //Remove route and controller from route defaults - unset($route['_route'], $route['_controller'], $route['_canonical_route']); - - //Generate url - $url = $this->router->generate($name, $extra+$route); - - //Return redirect to url response - return new RedirectResponse($url, 302); - } - //No route matched - } catch (ResourceNotFoundException $e) { - //Unset default path, name and route - unset($failurePath, $name, $route); - } - //With route name - } else { - //Try with login path route - try { - //Retrieve route matching path - $url = $this->router->generate($failurePath, $extra); + /** Disabled to prevent user mail + hash retrieval for each unactivated/locked accounts - //Return redirect to url response - return new RedirectResponse($url, 302); - //Route not found, missing parameter or invalid parameter - } catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) { - //Unset default path and url - unset($failurePath, $url); - } - } - } + //Get user identifier + $mail = $parent->getUserIdentifier(); + + //Set extra parameters + $extra = ['mail' => $smail = $this->slugger->short($mail), 'hash' => $this->slugger->hash($smail)];*/ + + //With failure target path option + if (!empty($failurePath = $this->options['failure_path'])) { + //With path + if ($failurePath[0] == '/') { + //Create login path request instance + $req = Request::create($failurePath); + + //Get login path pathinfo + $path = $req->getPathInfo(); + + //Remove script name + $path = str_replace($request->getScriptName(), '', $path); + + //Try with login path path + try { + //Save old context + $oldContext = $this->router->getContext(); + + //Force clean context + //XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST + //XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42 + $this->router->setContext(new RequestContext()); + + //Retrieve route matching path + $route = $this->router->match($path); + + //Reset context + $this->router->setContext($oldContext); + + //Clear old context + unset($oldContext); + + //With route name + if ($name = $route['_route']) { + //Remove route and controller from route defaults + unset($route['_route'], $route['_controller'], $route['_canonical_route']); - //Without user - if (($user = $this->doctrine->getRepository($this->config['class']['user'])->findOneByMail($mail)) === null) { - //With index route from config - if (!empty($name = $this->config['route']['register']['name']) && is_array($context = $this->config['route']['register']['context'])) { - //Try index route - try { //Generate url - $url = $this->router->generate($name, $extra+$context); + $url = $this->router->generate($name, /*$extra+*/$route); - //Return generated route + //Return redirect to url response return new RedirectResponse($url, 302); - //No route matched - } catch (ResourceNotFoundException $e) { - //Unset name and context - unset($name, $context); - } - } - - //With login target path option - if (!empty($loginPath = $this->options['login_path'])) { - //With path - if ($loginPath[0] == '/') { - //Create login path request instance - $req = Request::create($loginPath); - - //Get login path pathinfo - $path = $req->getPathInfo(); - - //Remove script name - $path = str_replace($request->getScriptName(), '', $path); - - //Try with login path path - try { - //Save old context - $oldContext = $this->router->getContext(); - - //Force clean context - //XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST - //XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42 - $this->router->setContext(new RequestContext()); - - //Retrieve route matching path - $route = $this->router->match($path); - - //Reset context - $this->router->setContext($oldContext); - - //Clear old context - unset($oldContext); - - //With route name - if ($name = $route['_route']) { - //Remove route and controller from route defaults - unset($route['_route'], $route['_controller'], $route['_canonical_route']); - - //Generate url - $url = $this->router->generate($name, $extra+$route); - - //Return redirect to url response - return new RedirectResponse($url, 302); - } - //No route matched - } catch (ResourceNotFoundException $e) { - //Unset default path, name and route - unset($loginPath, $name, $route); - } - //With route name - } else { - //Try with login path route - try { - //Retrieve route matching path - $url = $this->router->generate($loginPath, $extra); - - //Return redirect to url response - return new RedirectResponse($url, 302); - //Route not found, missing parameter or invalid parameter - } catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) { - //Unset default path and url - unset($loginPath, $url); - } } + //No route matched + } catch (ResourceNotFoundException $e) { + //Unset default path, name and route + unset($failurePath, $name, $route); } - //With not enabled user - } elseif (empty($user->isEnabled())) { - //Add error message account is not enabled - $this->addFlash('error', $this->translator->trans('Your account is not enable')); - - //Redirect on the same route with sent=1 to cleanup form - return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302); - //With unactivated user - } elseif (empty($user->isActivated())) { - //Set context - $context = [ - 'recipient_mail' => $user->getMail(), - 'recipient_name' => $user->getRecipientName() - ] + array_replace_recursive( - $this->config['context'], - $this->config['register']['view']['context'], - $this->config['register']['mail']['context'] - ); - - //Generate each route route - foreach($this->config['register']['route'] as $route => $tag) { - //Only process defined routes - if (!empty($this->config['route'][$route])) { - //Process for confirm url - if ($route == 'confirm') { - //Set the url in context - $context[$tag] = $this->router->generate( - $this->config['route'][$route]['name'], - //Prepend subscribe context with tag - [ - 'mail' => $smail = $this->slugger->short($context['recipient_mail']), - 'hash' => $this->slugger->hash($smail) - ]+$this->config['route'][$route]['context'], - UrlGeneratorInterface::ABSOLUTE_URL - ); - } - } + //With route name + } else { + //Try with login path route + try { + //Retrieve route matching path + $url = $this->router->generate($failurePath/*, $extra*/); + + //Return redirect to url response + return new RedirectResponse($url, 302); + //Route not found, missing parameter or invalid parameter + } catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) { + //Unset default path and url + unset($failurePath, $url); + } + } + } + //With not enabled user + } elseif ($parent instanceof DisabledException) { + //Add error message account is not enabled + $this->addFlash('error', $this->translator->trans('Account not enabled')); + + //Redirect on the same route with sent=1 to cleanup form + return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302); + //With not activated user + } elseif ($parent instanceof UnactivatedException) { + //Set user + $user = $parent->getUser(); + + //Set context + $context = [ + 'recipient_mail' => $user->getMail(), + 'recipient_name' => $user->getRecipientName() + ] + array_replace_recursive( + $this->config['context'], + $this->config['register']['view']['context'], + $this->config['register']['mail']['context'] + ); + + //Generate each route route + foreach($this->config['register']['route'] as $route => $tag) { + //Only process defined routes + if (!empty($this->config['route'][$route])) { + //Process for confirm url + if ($route == 'confirm') { + //Set the url in context + $context[$tag] = $this->router->generate( + $this->config['route'][$route]['name'], + //Prepend confirm context with tag + [ + 'mail' => $smail = $this->slugger->short($context['recipient_mail']), + 'hash' => $this->slugger->hash($smail) + ]+$this->config['route'][$route]['context'], + UrlGeneratorInterface::ABSOLUTE_URL + ); } + } + } - //Iterate on keys to translate - foreach($this->config['translate'] as $translate) { - //Extract keys - $keys = explode('.', $translate); + //Iterate on keys to translate + foreach($this->config['translate'] as $translate) { + //Extract keys + $keys = explode('.', $translate); - //Set current - $current =& $context; + //Set current + $current =& $context; - //Iterate on each subkey - do { - //Skip unset translation keys - if (!isset($current[current($keys)])) { - continue(2); - } + //Iterate on each subkey + do { + //Skip unset translation keys + if (!isset($current[current($keys)])) { + continue(2); + } - //Set current to subkey - $current =& $current[current($keys)]; - } while(next($keys)); + //Set current to subkey + $current =& $current[current($keys)]; + } while(next($keys)); - //Set translation - $current = $this->translator->trans($current); + //Set translation + $current = $this->translator->trans($current); - //Remove reference - unset($current); - } + //Remove reference + unset($current); + } - //Translate subject - $context['subject'] = $subject = ucfirst( - $this->translator->trans( - $this->config['register']['mail']['subject'], - $this->slugger->flatten($context, null, '.', '%', '%') - ) - ); - - //Create message - $message = (new TemplatedEmail()) - //Set sender - ->from(new Address($this->config['contact']['address'], $this->config['contact']['name'])) - //Set recipient - //XXX: remove the debug set in vendor/symfony/mime/Address.php +46 - ->to(new Address($context['recipient_mail'], $context['recipient_name'])) - //Set subject - ->subject($context['subject']) - - //Set path to twig templates - ->htmlTemplate($this->config['register']['mail']['html']) - ->textTemplate($this->config['register']['mail']['text']) - - //Set context - ->context($context); - - //Try sending message - //XXX: mail delivery may silently fail - try { - //Send message - $this->mailer->send($message); - //Catch obvious transport exception - } catch(TransportExceptionInterface $e) { - //Add error message mail unreachable - $this->addFlash('error', $this->translator->trans('Unable to reach account')); - } + //Translate subject + $context['subject'] = $subject = ucfirst( + $this->translator->trans( + $this->config['register']['mail']['subject'], + $this->slugger->flatten($context, null, '.', '%', '%') + ) + ); + + //Create message + $message = (new TemplatedEmail()) + //Set sender + ->from(new Address($this->config['contact']['address'], $this->config['contact']['name'])) + //Set recipient + //XXX: remove the debug set in vendor/symfony/mime/Address.php +46 + ->to(new Address($context['recipient_mail'], $context['recipient_name'])) + //Set subject + ->subject($context['subject']) + + //Set path to twig templates + ->htmlTemplate($this->config['register']['mail']['html']) + ->textTemplate($this->config['register']['mail']['text']) + + //Set context + ->context($context); + + //Try sending message + //XXX: mail delivery may silently fail + try { + //Send message + $this->mailer->send($message); + //Catch obvious transport exception + } catch(TransportExceptionInterface $e) { + //Add error message mail unreachable + $this->addFlash('error', $this->translator->trans('Unable to reach account')); + } - //Add notice - $this->addFlash('notice', $this->translator->trans('Your verification mail has been sent, to activate your account you must follow the confirmation link inside')); + //Add notice + $this->addFlash('notice', $this->translator->trans('Your verification mail has been sent, to activate your account follow the confirmation link inside')); - //Add junk warning - $this->addFlash('warning', $this->translator->trans('If you did not receive a verification mail, check your Spam or Junk mail folders')); + //Add junk warning + $this->addFlash('warning', $this->translator->trans('If you did not receive a verification mail, check your Spam or Junk mail folder')); - //Redirect on the same route with sent=1 to cleanup form - return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302); - } - } + //Redirect on the same route with sent=1 to cleanup form + return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302); } } //Call parent function return parent::onAuthenticationFailure($request, $exception); } - - /** - * {@inheritdoc} - */ - public function getAlias(): string { - return RapsysUserBundle::getAlias(); - } }