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