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