Mercurial > hg > isophonics-drupal-site
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 } |