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