]> Raphaël G. Git Repositories - userbundle/blob - Handler/AuthenticationFailureHandler.php
Version 0.5.1
[userbundle] / Handler / AuthenticationFailureHandler.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys UserBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\UserBundle\Handler;
13
14 use Doctrine\Persistence\ManagerRegistry;
15
16 use Psr\Log\LoggerInterface;
17
18 use Rapsys\PackBundle\Util\SluggerUtil;
19
20 use Rapsys\UserBundle\Exception\UnactivatedException;
21 use Rapsys\UserBundle\RapsysUserBundle;
22
23 use Symfony\Bridge\Twig\Mime\TemplatedEmail;
24 use Symfony\Component\DependencyInjection\ContainerInterface;
25 use Symfony\Component\HttpFoundation\RedirectResponse;
26 use Symfony\Component\HttpFoundation\Request;
27 use Symfony\Component\HttpFoundation\RequestStack;
28 use Symfony\Component\HttpFoundation\Response;
29 use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
30 use Symfony\Component\HttpKernel\HttpKernelInterface;
31 use Symfony\Component\Mailer\MailerInterface;
32 use Symfony\Component\Mime\Address;
33 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
34 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
35 use Symfony\Component\Routing\RequestContext;
36 use Symfony\Component\Routing\RouterInterface;
37 use Symfony\Component\Security\Core\Exception\AuthenticationException;
38 use Symfony\Component\Security\Core\Exception\BadCredentialsException;
39 use Symfony\Component\Security\Core\Exception\DisabledException;
40 use Symfony\Component\Security\Core\Exception\UserNotFoundException;
41 use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
42 use Symfony\Component\Security\Http\HttpUtils;
43 use Symfony\Contracts\Translation\TranslatorInterface;
44
45 /**
46 * {@inheritdoc}
47 */
48 class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
49 /**
50 * Alias string
51 */
52 protected string $alias;
53
54 /**
55 * Config array
56 */
57 protected array $config;
58 protected array $defaultOptions = [
59 'failure_path' => null,
60 'failure_forward' => false,
61 'login_path' => '/login',
62 'failure_path_parameter' => '_failure_path',
63 ];
64
65 /**
66 * {@inheritdoc}
67 *
68 * @xxx Second argument will be replaced by security.firewalls.main.logout.target
69 * @see vendor/symfony/security-bundle/DependencyInjection/SecurityExtension.php +360
70 *
71 * @param HttpKernelInterface $httpKernel The http kernel
72 * @param HttpUtils $httpUtils The http utils
73 * @param array $options The options
74 * @param LoggerInterface $logger The logger instance
75 * @param ContainerInterface $container The container instance
76 * @param ManagerRegistry $doctrine The doctrine instance
77 * @param MailerInterface $mailer The mailer instance
78 * @param RouterInterface $router The router instance
79 * @param SluggerUtil $slugger The slugger instance
80 * @param RequestStack $stack The stack instance
81 * @param TranslatorInterface $translator The translator instance
82 */
83 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) {
84 //Call parent constructor
85 parent::__construct($httpKernel, $httpUtils, $options, $logger);
86
87 //Set config
88 $this->config = $container->getParameter($this->alias = RapsysUserBundle::getAlias());
89 }
90
91 /**
92 * Adds a flash message to the current session for type.
93 *
94 * @throws \LogicException
95 */
96 protected function addFlash(string $type, mixed $message): void {
97 try {
98 $session = $this->stack->getSession();
99 } catch (SessionNotFoundException $e) {
100 throw new \LogicException('You cannot use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".', 0, $e);
101 }
102
103 if (!$session instanceof FlashBagAwareSessionInterface) {
104 throw new \LogicException(sprintf('You cannot use the addFlash method because class "%s" doesn\'t implement "%s".', get_debug_type($session), FlashBagAwareSessionInterface::class));
105 }
106
107 $session->getFlashBag()->add($type, $message);
108 }
109
110 /**
111 * {@inheritdoc}
112 *
113 * This is called when an interactive authentication attempt fails
114 */
115 public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response {
116 //With bad credential exception
117 if ($exception instanceof BadCredentialsException) {
118 //With parent exception
119 if (($parent = $exception->getPrevious()) instanceof UserNotFoundException) {
120 /** Disabled to prevent user mail + hash retrieval for each unactivated/locked accounts
121
122 //Get user identifier
123 $mail = $parent->getUserIdentifier();
124
125 //Set extra parameters
126 $extra = ['mail' => $smail = $this->slugger->short($mail), 'hash' => $this->slugger->hash($smail)];*/
127
128 //With failure target path option
129 if (!empty($failurePath = $this->options['failure_path'])) {
130 //With path
131 if ($failurePath[0] == '/') {
132 //Create login path request instance
133 $req = Request::create($failurePath);
134
135 //Get login path pathinfo
136 $path = $req->getPathInfo();
137
138 //Remove script name
139 $path = str_replace($request->getScriptName(), '', $path);
140
141 //Try with login path path
142 try {
143 //Save old context
144 $oldContext = $this->router->getContext();
145
146 //Force clean context
147 //XXX: prevent MethodNotAllowedException on GET only routes because our context method is POST
148 //XXX: see vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php +42
149 $this->router->setContext(new RequestContext());
150
151 //Retrieve route matching path
152 $route = $this->router->match($path);
153
154 //Reset context
155 $this->router->setContext($oldContext);
156
157 //Clear old context
158 unset($oldContext);
159
160 //With route name
161 if ($name = $route['_route']) {
162 //Remove route and controller from route defaults
163 unset($route['_route'], $route['_controller'], $route['_canonical_route']);
164
165 //Generate url
166 $url = $this->router->generate($name, /*$extra+*/$route);
167
168 //Return redirect to url response
169 return new RedirectResponse($url, 302);
170 }
171 //No route matched
172 } catch (ResourceNotFoundException $e) {
173 //Unset default path, name and route
174 unset($failurePath, $name, $route);
175 }
176 //With route name
177 } else {
178 //Try with login path route
179 try {
180 //Retrieve route matching path
181 $url = $this->router->generate($failurePath/*, $extra*/);
182
183 //Return redirect to url response
184 return new RedirectResponse($url, 302);
185 //Route not found, missing parameter or invalid parameter
186 } catch (RouteNotFoundException|MissingMandatoryParametersException|InvalidParameterException $e) {
187 //Unset default path and url
188 unset($failurePath, $url);
189 }
190 }
191 }
192 //With not enabled user
193 } elseif ($parent instanceof DisabledException) {
194 //Add error message account is not enabled
195 $this->addFlash('error', $this->translator->trans('Account not enabled', [], $this->alias));
196
197 //Redirect on the same route with sent=1 to cleanup form
198 return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302);
199 //With not activated user
200 } elseif ($parent instanceof UnactivatedException) {
201 //Set user
202 $user = $parent->getUser();
203
204 //Set context
205 $context = [
206 'recipient_mail' => $user->getMail(),
207 'recipient_name' => $user->getRecipientName()
208 ] + array_replace_recursive(
209 $this->config['context'],
210 $this->config['register']['view']['context'],
211 $this->config['register']['mail']['context']
212 );
213
214 //Generate each route route
215 foreach($this->config['register']['route'] as $route => $tag) {
216 //Only process defined routes
217 if (!empty($this->config['route'][$route])) {
218 //Process for confirm url
219 if ($route == 'confirm') {
220 //Set the url in context
221 $context[$tag] = $this->router->generate(
222 $this->config['route'][$route]['name'],
223 //Prepend confirm context with tag
224 [
225 'mail' => $smail = $this->slugger->short($context['recipient_mail']),
226 'hash' => $this->slugger->hash($smail)
227 ]+$this->config['route'][$route]['context'],
228 UrlGeneratorInterface::ABSOLUTE_URL
229 );
230 }
231 }
232 }
233
234 //Iterate on keys to translate
235 foreach($this->config['translate'] as $translate) {
236 //Extract keys
237 $keys = explode('.', $translate);
238
239 //Set current
240 $current =& $context;
241
242 //Iterate on each subkey
243 do {
244 //Skip unset translation keys
245 if (!isset($current[current($keys)])) {
246 continue(2);
247 }
248
249 //Set current to subkey
250 $current =& $current[current($keys)];
251 } while(next($keys));
252
253 //Set translation
254 $current = $this->translator->trans($current, [], $this->alias);
255
256 //Remove reference
257 unset($current);
258 }
259
260 //Translate subject
261 $context['subject'] = $subject = ucfirst(
262 $this->translator->trans(
263 $this->config['register']['mail']['subject'],
264 $this->slugger->flatten($context, null, '.', '%', '%'),
265 $this->alias
266 )
267 );
268
269 //Create message
270 $message = (new TemplatedEmail())
271 //Set sender
272 ->from(new Address($this->config['contact']['address'], $this->config['contact']['name']))
273 //Set recipient
274 //XXX: remove the debug set in vendor/symfony/mime/Address.php +46
275 ->to(new Address($context['recipient_mail'], $context['recipient_name']))
276 //Set subject
277 ->subject($context['subject'])
278
279 //Set path to twig templates
280 ->htmlTemplate($this->config['register']['mail']['html'])
281 ->textTemplate($this->config['register']['mail']['text'])
282
283 //Set context
284 ->context($context);
285
286 //Try sending message
287 //XXX: mail delivery may silently fail
288 try {
289 //Send message
290 $this->mailer->send($message);
291 //Catch obvious transport exception
292 } catch(TransportExceptionInterface $e) {
293 //Add error message mail unreachable
294 $this->addFlash('error', $this->translator->trans('Unable to reach account', [], $this->alias));
295 }
296
297 //Add notice
298 $this->addFlash('notice', $this->translator->trans('Your verification mail has been sent, to activate your account follow the confirmation link inside', [], $this->alias));
299
300 //Add junk warning
301 $this->addFlash('warning', $this->translator->trans('If you did not receive a verification mail, check your Spam or Junk mail folder', [], $this->alias));
302
303 //Redirect on the same route with sent=1 to cleanup form
304 return new RedirectResponse($this->router->generate($request->get('_route'), $request->get('_route_params')), 302);
305 }
306 }
307
308 //Call parent function
309 return parent::onAuthenticationFailure($request, $exception);
310 }
311 }