]> Raphaël G. Git Repositories - userbundle/commitdiff
Add user checker
authorRaphaël Gertz <git@rapsys.eu>
Thu, 12 Aug 2021 11:56:41 +0000 (13:56 +0200)
committerRaphaël Gertz <git@rapsys.eu>
Thu, 12 Aug 2021 11:56:41 +0000 (13:56 +0200)
Add authentication failure handler
Add unactivated exception

Checker/UserChecker.php [new file with mode: 0644]
Exception/UnactivatedException.php [new file with mode: 0644]
Handler/AuthenticationFailureHandler.php [new file with mode: 0644]

diff --git a/Checker/UserChecker.php b/Checker/UserChecker.php
new file mode 100644 (file)
index 0000000..1240c8a
--- /dev/null
@@ -0,0 +1,48 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys UserBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\UserBundle\Checker;
+
+use Symfony\Component\Security\Core\User\UserChecker as BaseUserChecker;
+use Symfony\Component\Security\Core\Exception\DisabledException;
+use Symfony\Component\Security\Core\User\UserInterface;
+
+use Rapsys\UserBundle\Entity\User;
+use Rapsys\UserBundle\Exception\UnactivatedException;
+
+/**
+ * {@inheritdoc}
+ */
+class UserChecker extends BaseUserChecker {
+       /**
+        * {@inheritdoc}
+        */
+       public function checkPostAuth(UserInterface $user): void {
+               //Without User instance
+               if (!$user instanceof User) {
+                       return;
+               }
+
+               //With not activated user
+        if (!$user->isActivated()) {
+            $ex = new UnactivatedException('Account is not activated');
+            $ex->setUser($user);
+            throw $ex;
+        }
+
+               //With disabled user
+        if ($user->isDisabled()) {
+            $ex = new DisabledException('Account is disabled');
+            $ex->setUser($user);
+            throw $ex;
+        }
+       }
+}
diff --git a/Exception/UnactivatedException.php b/Exception/UnactivatedException.php
new file mode 100644 (file)
index 0000000..cbf367d
--- /dev/null
@@ -0,0 +1,28 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys UserBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\UserBundle\Exception;
+
+use Symfony\Component\Security\Core\Exception\AccountStatusException;
+
+/**
+ * UnactivatedException is thrown when the user account is unactivated.
+ *
+ * {@inheritdoc}
+ */
+class UnactivatedException extends AccountStatusException {
+    /**
+     * {@inheritdoc}
+     */
+    public function getMessageKey(): string {
+        return 'Account is not activated';
+       }
+}
diff --git a/Handler/AuthenticationFailureHandler.php b/Handler/AuthenticationFailureHandler.php
new file mode 100644 (file)
index 0000000..5945644
--- /dev/null
@@ -0,0 +1,261 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys UserBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\UserBundle\Handler;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+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\Http\Authentication\DefaultAuthenticationFailureHandler;
+use Symfony\Component\Security\Http\HttpUtils;
+use Symfony\Component\Security\Http\ParameterBagUtils;
+
+use Rapsys\PackBundle\Util\SluggerUtil;
+use Rapsys\UserBundle\Exception\UnactivatedException;
+use Rapsys\UserBundle\RapsysUserBundle;
+
+/**
+ * {@inheritdoc}
+ */
+class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
+       /**
+        * Config array
+        */
+       protected $config;
+    protected $options;
+    protected $defaultOptions = [
+        'failure_path' => null,
+        'failure_forward' => false,
+        'login_path' => '/login',
+        'failure_path_parameter' => '_failure_path',
+    ];
+
+       /**
+        * Router instance
+        */
+       protected $router;
+
+       /**
+        * Slugger instance
+        */
+       protected $slugger;
+
+       /**
+        * {@inheritdoc}
+        */
+    public function __construct(HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options = [], LoggerInterface $logger, ContainerInterface $container, RouterInterface $router, SluggerUtil $slugger) {
+               //Set config
+               $this->config = $container->getParameter(self::getAlias());
+
+               //Set router
+               $this->router = $router;
+
+               //Set slugger
+               $this->slugger = $slugger;
+
+               //Call parent constructor
+               parent::__construct($httpKernel, $httpUtils, $options, $logger);
+       }
+
+    /**
+        * This is called when an interactive authentication attempt fails
+        *
+        * User may retrieve mail + field + hash for each unactivated/locked accounts
+        *
+        * {@inheritdoc}
+        */
+       public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response {
+               //With bad credential exception
+               if ($exception instanceof BadCredentialsException) {
+                       //With parent exception
+                       if ($parent = $exception->getPrevious()) {
+                               //Retrieve login
+                               //TODO: check form _token validity ???
+                               if (
+                                       $request->request->has('login') &&
+                                       !empty($login = $request->request->get('login')) &&
+                                       !empty($mail = $login['mail'])
+                               ) {
+                                       //Redirect on register
+                                       if ($parent instanceof UnactivatedException || $parent instanceof DisabledException) {
+                                               //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);
+
+                                                                       //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 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);
+
+                                                               //Return generated route
+                                                               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);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               //Call parent function
+               return parent::onAuthenticationFailure($request, $exception);
+       }
+
+       /**
+        * {@inheritdoc}
+        */
+       public function getAlias(): string {
+               return RapsysUserBundle::getAlias();
+       }
+}