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