annotate core/modules/user/src/Controller/UserAuthenticationController.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 4c8ae668cc8c
children
rev   line source
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 }