comparison core/modules/user/src/Controller/UserAuthenticationController.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
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 }