f45ec3a7ac7ba42c67e225b29128821da9df5cd6
[yaffs-website] / web / core / modules / user / src / Controller / UserAuthenticationController.php
1 <?php
2
3 namespace Drupal\user\Controller;
4
5 use Drupal\Core\Access\CsrfTokenGenerator;
6 use Drupal\Core\Controller\ControllerBase;
7 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
8 use Drupal\Core\Flood\FloodInterface;
9 use Drupal\Core\Routing\RouteProviderInterface;
10 use Drupal\user\UserAuthInterface;
11 use Drupal\user\UserInterface;
12 use Drupal\user\UserStorageInterface;
13 use Psr\Log\LoggerInterface;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15 use Symfony\Component\HttpFoundation\Request;
16 use Symfony\Component\HttpFoundation\Response;
17 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
18 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
19 use Symfony\Component\Serializer\Encoder\JsonEncoder;
20 use Symfony\Component\Serializer\Serializer;
21
22 /**
23  * Provides controllers for login, login status and logout via HTTP requests.
24  */
25 class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface {
26
27   /**
28    * String sent in responses, to describe the user as being logged in.
29    *
30    * @var string
31    */
32   const LOGGED_IN = 1;
33
34   /**
35    * String sent in responses, to describe the user as being logged out.
36    *
37    * @var string
38    */
39   const LOGGED_OUT = 0;
40
41   /**
42    * The flood controller.
43    *
44    * @var \Drupal\Core\Flood\FloodInterface
45    */
46   protected $flood;
47
48   /**
49    * The user storage.
50    *
51    * @var \Drupal\user\UserStorageInterface
52    */
53   protected $userStorage;
54
55   /**
56    * The CSRF token generator.
57    *
58    * @var \Drupal\Core\Access\CsrfTokenGenerator
59    */
60   protected $csrfToken;
61
62   /**
63    * The user authentication.
64    *
65    * @var \Drupal\user\UserAuthInterface
66    */
67   protected $userAuth;
68
69   /**
70    * The route provider.
71    *
72    * @var \Drupal\Core\Routing\RouteProviderInterface
73    */
74   protected $routeProvider;
75
76   /**
77    * The serializer.
78    *
79    * @var \Symfony\Component\Serializer\Serializer
80    */
81   protected $serializer;
82
83   /**
84    * The available serialization formats.
85    *
86    * @var array
87    */
88   protected $serializerFormats = [];
89
90   /**
91    * A logger instance.
92    *
93    * @var \Psr\Log\LoggerInterface
94    */
95   protected $logger;
96
97   /**
98    * Constructs a new UserAuthenticationController object.
99    *
100    * @param \Drupal\Core\Flood\FloodInterface $flood
101    *   The flood controller.
102    * @param \Drupal\user\UserStorageInterface $user_storage
103    *   The user storage.
104    * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
105    *   The CSRF token generator.
106    * @param \Drupal\user\UserAuthInterface $user_auth
107    *   The user authentication.
108    * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
109    *   The route provider.
110    * @param \Symfony\Component\Serializer\Serializer $serializer
111    *   The serializer.
112    * @param array $serializer_formats
113    *   The available serialization formats.
114    * @param \Psr\Log\LoggerInterface $logger
115    *   A logger instance.
116    */
117   public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats, LoggerInterface $logger) {
118     $this->flood = $flood;
119     $this->userStorage = $user_storage;
120     $this->csrfToken = $csrf_token;
121     $this->userAuth = $user_auth;
122     $this->serializer = $serializer;
123     $this->serializerFormats = $serializer_formats;
124     $this->routeProvider = $route_provider;
125     $this->logger = $logger;
126   }
127
128   /**
129    * {@inheritdoc}
130    */
131   public static function create(ContainerInterface $container) {
132     if ($container->hasParameter('serializer.formats') && $container->has('serializer')) {
133       $serializer = $container->get('serializer');
134       $formats = $container->getParameter('serializer.formats');
135     }
136     else {
137       $formats = ['json'];
138       $encoders = [new JsonEncoder()];
139       $serializer = new Serializer([], $encoders);
140     }
141
142     return new static(
143       $container->get('flood'),
144       $container->get('entity_type.manager')->getStorage('user'),
145       $container->get('csrf_token'),
146       $container->get('user.auth'),
147       $container->get('router.route_provider'),
148       $serializer,
149       $formats,
150       $container->get('logger.factory')->get('user')
151     );
152   }
153
154   /**
155    * Logs in a user.
156    *
157    * @param \Symfony\Component\HttpFoundation\Request $request
158    *   The request.
159    *
160    * @return \Symfony\Component\HttpFoundation\Response
161    *   A response which contains the ID and CSRF token.
162    */
163   public function login(Request $request) {
164     $format = $this->getRequestFormat($request);
165
166     $content = $request->getContent();
167     $credentials = $this->serializer->decode($content, $format);
168     if (!isset($credentials['name']) && !isset($credentials['pass'])) {
169       throw new BadRequestHttpException('Missing credentials.');
170     }
171
172     if (!isset($credentials['name'])) {
173       throw new BadRequestHttpException('Missing credentials.name.');
174     }
175     if (!isset($credentials['pass'])) {
176       throw new BadRequestHttpException('Missing credentials.pass.');
177     }
178
179     $this->floodControl($request, $credentials['name']);
180
181     if ($this->userIsBlocked($credentials['name'])) {
182       throw new BadRequestHttpException('The user has not been activated or is blocked.');
183     }
184
185     if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) {
186       $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
187       /** @var \Drupal\user\UserInterface $user */
188       $user = $this->userStorage->load($uid);
189       $this->userLoginFinalize($user);
190
191       // Send basic metadata about the logged in user.
192       $response_data = [];
193       if ($user->get('uid')->access('view', $user)) {
194         $response_data['current_user']['uid'] = $user->id();
195       }
196       if ($user->get('roles')->access('view', $user)) {
197         $response_data['current_user']['roles'] = $user->getRoles();
198       }
199       if ($user->get('name')->access('view', $user)) {
200         $response_data['current_user']['name'] = $user->getAccountName();
201       }
202       $response_data['csrf_token'] = $this->csrfToken->get('rest');
203
204       $logout_route = $this->routeProvider->getRouteByName('user.logout.http');
205       // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
206       $logout_path = ltrim($logout_route->getPath(), '/');
207       $response_data['logout_token'] = $this->csrfToken->get($logout_path);
208
209       $encoded_response_data = $this->serializer->encode($response_data, $format);
210       return new Response($encoded_response_data);
211     }
212
213     $flood_config = $this->config('user.flood');
214     if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
215       $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier);
216     }
217     // Always register an IP-based failed login event.
218     $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
219     throw new BadRequestHttpException('Sorry, unrecognized username or password.');
220   }
221
222   /**
223    * Resets a user password.
224    *
225    * @param \Symfony\Component\HttpFoundation\Request $request
226    *   The request.
227    *
228    * @return \Symfony\Component\HttpFoundation\Response
229    *   The response object.
230    */
231   public function resetPassword(Request $request) {
232     $format = $this->getRequestFormat($request);
233
234     $content = $request->getContent();
235     $credentials = $this->serializer->decode($content, $format);
236
237     // Check if a name or mail is provided.
238     if (!isset($credentials['name']) && !isset($credentials['mail'])) {
239       throw new BadRequestHttpException('Missing credentials.name or credentials.mail');
240     }
241
242     // Load by name if provided.
243     if (isset($credentials['name'])) {
244       $users = $this->userStorage->loadByProperties(['name' => trim($credentials['name'])]);
245     }
246     elseif (isset($credentials['mail'])) {
247       $users = $this->userStorage->loadByProperties(['mail' => trim($credentials['mail'])]);
248     }
249
250     /** @var \Drupal\Core\Session\AccountInterface $account */
251     $account = reset($users);
252     if ($account && $account->id()) {
253       if ($this->userIsBlocked($account->getAccountName())) {
254         throw new BadRequestHttpException('The user has not been activated or is blocked.');
255       }
256
257       // Send the password reset email.
258       $mail = _user_mail_notify('password_reset', $account, $account->getPreferredLangcode());
259       if (empty($mail)) {
260         throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
261       }
262       else {
263         $this->logger->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
264         return new Response();
265       }
266     }
267
268     // Error if no users found with provided name or mail.
269     throw new BadRequestHttpException('Unrecognized username or email address.');
270   }
271
272   /**
273    * Verifies if the user is blocked.
274    *
275    * @param string $name
276    *   The username.
277    *
278    * @return bool
279    *   TRUE if the user is blocked, otherwise FALSE.
280    */
281   protected function userIsBlocked($name) {
282     return user_is_blocked($name);
283   }
284
285   /**
286    * Finalizes the user login.
287    *
288    * @param \Drupal\user\UserInterface $user
289    *   The user.
290    */
291   protected function userLoginFinalize(UserInterface $user) {
292     user_login_finalize($user);
293   }
294
295   /**
296    * Logs out a user.
297    *
298    * @return \Symfony\Component\HttpFoundation\Response
299    *   The response object.
300    */
301   public function logout() {
302     $this->userLogout();
303     return new Response(NULL, 204);
304   }
305
306   /**
307    * Logs the user out.
308    */
309   protected function userLogout() {
310     user_logout();
311   }
312
313   /**
314    * Checks whether a user is logged in or not.
315    *
316    * @return \Symfony\Component\HttpFoundation\Response
317    *   The response.
318    */
319   public function loginStatus() {
320     if ($this->currentUser()->isAuthenticated()) {
321       $response = new Response(self::LOGGED_IN);
322     }
323     else {
324       $response = new Response(self::LOGGED_OUT);
325     }
326     $response->headers->set('Content-Type', 'text/plain');
327     return $response;
328   }
329
330   /**
331    * Gets the format of the current request.
332    *
333    * @param \Symfony\Component\HttpFoundation\Request $request
334    *   The current request.
335    *
336    * @return string
337    *   The format of the request.
338    */
339   protected function getRequestFormat(Request $request) {
340     $format = $request->getRequestFormat();
341     if (!in_array($format, $this->serializerFormats)) {
342       throw new BadRequestHttpException("Unrecognized format: $format.");
343     }
344     return $format;
345   }
346
347   /**
348    * Enforces flood control for the current login request.
349    *
350    * @param \Symfony\Component\HttpFoundation\Request $request
351    *   The current request.
352    * @param string $username
353    *   The user name sent for login credentials.
354    */
355   protected function floodControl(Request $request, $username) {
356     $flood_config = $this->config('user.flood');
357     if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
358       throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
359     }
360
361     if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
362       // Don't allow login if the limit for this user has been reached.
363       // Default is to allow 5 failed attempts every 6 hours.
364       if (!$this->flood->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
365         if ($flood_config->get('uid_only')) {
366           $error_message = sprintf('There have been more than %s failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', $flood_config->get('user_limit'));
367         }
368         else {
369           $error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
370         }
371         throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS);
372       }
373     }
374   }
375
376   /**
377    * Gets the login identifier for user login flood control.
378    *
379    * @param \Symfony\Component\HttpFoundation\Request $request
380    *   The current request.
381    * @param string $username
382    *   The username supplied in login credentials.
383    *
384    * @return string
385    *   The login identifier or if the user does not exist an empty string.
386    */
387   protected function getLoginFloodIdentifier(Request $request, $username) {
388     $flood_config = $this->config('user.flood');
389     $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]);
390     if ($account = reset($accounts)) {
391       if ($flood_config->get('uid_only')) {
392         // Register flood events based on the uid only, so they apply for any
393         // IP address. This is the most secure option.
394         $identifier = $account->id();
395       }
396       else {
397         // The default identifier is a combination of uid and IP address. This
398         // is less secure but more resistant to denial-of-service attacks that
399         // could lock out all users with public user names.
400         $identifier = $account->id() . '-' . $request->getClientIp();
401       }
402       return $identifier;
403     }
404     return '';
405   }
406
407 }