diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/user/src/Controller/UserAuthenticationController.php	Wed Nov 29 16:09:58 2017 +0000
@@ -0,0 +1,407 @@
+<?php
+
+namespace Drupal\user\Controller;
+
+use Drupal\Core\Access\CsrfTokenGenerator;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\user\UserAuthInterface;
+use Drupal\user\UserInterface;
+use Drupal\user\UserStorageInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Serializer\Encoder\JsonEncoder;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * Provides controllers for login, login status and logout via HTTP requests.
+ */
+class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * String sent in responses, to describe the user as being logged in.
+   *
+   * @var string
+   */
+  const LOGGED_IN = 1;
+
+  /**
+   * String sent in responses, to describe the user as being logged out.
+   *
+   * @var string
+   */
+  const LOGGED_OUT = 0;
+
+  /**
+   * The flood controller.
+   *
+   * @var \Drupal\Core\Flood\FloodInterface
+   */
+  protected $flood;
+
+  /**
+   * The user storage.
+   *
+   * @var \Drupal\user\UserStorageInterface
+   */
+  protected $userStorage;
+
+  /**
+   * The CSRF token generator.
+   *
+   * @var \Drupal\Core\Access\CsrfTokenGenerator
+   */
+  protected $csrfToken;
+
+  /**
+   * The user authentication.
+   *
+   * @var \Drupal\user\UserAuthInterface
+   */
+  protected $userAuth;
+
+  /**
+   * The route provider.
+   *
+   * @var \Drupal\Core\Routing\RouteProviderInterface
+   */
+  protected $routeProvider;
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The available serialization formats.
+   *
+   * @var array
+   */
+  protected $serializerFormats = [];
+
+  /**
+   * A logger instance.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new UserAuthenticationController object.
+   *
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood controller.
+   * @param \Drupal\user\UserStorageInterface $user_storage
+   *   The user storage.
+   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
+   *   The CSRF token generator.
+   * @param \Drupal\user\UserAuthInterface $user_auth
+   *   The user authentication.
+   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+   *   The route provider.
+   * @param \Symfony\Component\Serializer\Serializer $serializer
+   *   The serializer.
+   * @param array $serializer_formats
+   *   The available serialization formats.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   A logger instance.
+   */
+  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats, LoggerInterface $logger) {
+    $this->flood = $flood;
+    $this->userStorage = $user_storage;
+    $this->csrfToken = $csrf_token;
+    $this->userAuth = $user_auth;
+    $this->serializer = $serializer;
+    $this->serializerFormats = $serializer_formats;
+    $this->routeProvider = $route_provider;
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    if ($container->hasParameter('serializer.formats') && $container->has('serializer')) {
+      $serializer = $container->get('serializer');
+      $formats = $container->getParameter('serializer.formats');
+    }
+    else {
+      $formats = ['json'];
+      $encoders = [new JsonEncoder()];
+      $serializer = new Serializer([], $encoders);
+    }
+
+    return new static(
+      $container->get('flood'),
+      $container->get('entity_type.manager')->getStorage('user'),
+      $container->get('csrf_token'),
+      $container->get('user.auth'),
+      $container->get('router.route_provider'),
+      $serializer,
+      $formats,
+      $container->get('logger.factory')->get('user')
+    );
+  }
+
+  /**
+   * Logs in a user.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   A response which contains the ID and CSRF token.
+   */
+  public function login(Request $request) {
+    $format = $this->getRequestFormat($request);
+
+    $content = $request->getContent();
+    $credentials = $this->serializer->decode($content, $format);
+    if (!isset($credentials['name']) && !isset($credentials['pass'])) {
+      throw new BadRequestHttpException('Missing credentials.');
+    }
+
+    if (!isset($credentials['name'])) {
+      throw new BadRequestHttpException('Missing credentials.name.');
+    }
+    if (!isset($credentials['pass'])) {
+      throw new BadRequestHttpException('Missing credentials.pass.');
+    }
+
+    $this->floodControl($request, $credentials['name']);
+
+    if ($this->userIsBlocked($credentials['name'])) {
+      throw new BadRequestHttpException('The user has not been activated or is blocked.');
+    }
+
+    if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) {
+      $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
+      /** @var \Drupal\user\UserInterface $user */
+      $user = $this->userStorage->load($uid);
+      $this->userLoginFinalize($user);
+
+      // Send basic metadata about the logged in user.
+      $response_data = [];
+      if ($user->get('uid')->access('view', $user)) {
+        $response_data['current_user']['uid'] = $user->id();
+      }
+      if ($user->get('roles')->access('view', $user)) {
+        $response_data['current_user']['roles'] = $user->getRoles();
+      }
+      if ($user->get('name')->access('view', $user)) {
+        $response_data['current_user']['name'] = $user->getAccountName();
+      }
+      $response_data['csrf_token'] = $this->csrfToken->get('rest');
+
+      $logout_route = $this->routeProvider->getRouteByName('user.logout.http');
+      // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
+      $logout_path = ltrim($logout_route->getPath(), '/');
+      $response_data['logout_token'] = $this->csrfToken->get($logout_path);
+
+      $encoded_response_data = $this->serializer->encode($response_data, $format);
+      return new Response($encoded_response_data);
+    }
+
+    $flood_config = $this->config('user.flood');
+    if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
+      $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier);
+    }
+    // Always register an IP-based failed login event.
+    $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
+    throw new BadRequestHttpException('Sorry, unrecognized username or password.');
+  }
+
+  /**
+   * Resets a user password.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response object.
+   */
+  public function resetPassword(Request $request) {
+    $format = $this->getRequestFormat($request);
+
+    $content = $request->getContent();
+    $credentials = $this->serializer->decode($content, $format);
+
+    // Check if a name or mail is provided.
+    if (!isset($credentials['name']) && !isset($credentials['mail'])) {
+      throw new BadRequestHttpException('Missing credentials.name or credentials.mail');
+    }
+
+    // Load by name if provided.
+    if (isset($credentials['name'])) {
+      $users = $this->userStorage->loadByProperties(['name' => trim($credentials['name'])]);
+    }
+    elseif (isset($credentials['mail'])) {
+      $users = $this->userStorage->loadByProperties(['mail' => trim($credentials['mail'])]);
+    }
+
+    /** @var \Drupal\Core\Session\AccountInterface $account */
+    $account = reset($users);
+    if ($account && $account->id()) {
+      if ($this->userIsBlocked($account->getAccountName())) {
+        throw new BadRequestHttpException('The user has not been activated or is blocked.');
+      }
+
+      // Send the password reset email.
+      $mail = _user_mail_notify('password_reset', $account, $account->getPreferredLangcode());
+      if (empty($mail)) {
+        throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
+      }
+      else {
+        $this->logger->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
+        return new Response();
+      }
+    }
+
+    // Error if no users found with provided name or mail.
+    throw new BadRequestHttpException('Unrecognized username or email address.');
+  }
+
+  /**
+   * Verifies if the user is blocked.
+   *
+   * @param string $name
+   *   The username.
+   *
+   * @return bool
+   *   TRUE if the user is blocked, otherwise FALSE.
+   */
+  protected function userIsBlocked($name) {
+    return user_is_blocked($name);
+  }
+
+  /**
+   * Finalizes the user login.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The user.
+   */
+  protected function userLoginFinalize(UserInterface $user) {
+    user_login_finalize($user);
+  }
+
+  /**
+   * Logs out a user.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response object.
+   */
+  public function logout() {
+    $this->userLogout();
+    return new Response(NULL, 204);
+  }
+
+  /**
+   * Logs the user out.
+   */
+  protected function userLogout() {
+    user_logout();
+  }
+
+  /**
+   * Checks whether a user is logged in or not.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response.
+   */
+  public function loginStatus() {
+    if ($this->currentUser()->isAuthenticated()) {
+      $response = new Response(self::LOGGED_IN);
+    }
+    else {
+      $response = new Response(self::LOGGED_OUT);
+    }
+    $response->headers->set('Content-Type', 'text/plain');
+    return $response;
+  }
+
+  /**
+   * Gets the format of the current request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return string
+   *   The format of the request.
+   */
+  protected function getRequestFormat(Request $request) {
+    $format = $request->getRequestFormat();
+    if (!in_array($format, $this->serializerFormats)) {
+      throw new BadRequestHttpException("Unrecognized format: $format.");
+    }
+    return $format;
+  }
+
+  /**
+   * Enforces flood control for the current login request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param string $username
+   *   The user name sent for login credentials.
+   */
+  protected function floodControl(Request $request, $username) {
+    $flood_config = $this->config('user.flood');
+    if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
+      throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
+    }
+
+    if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
+      // Don't allow login if the limit for this user has been reached.
+      // Default is to allow 5 failed attempts every 6 hours.
+      if (!$this->flood->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
+        if ($flood_config->get('uid_only')) {
+          $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'));
+        }
+        else {
+          $error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
+        }
+        throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS);
+      }
+    }
+  }
+
+  /**
+   * Gets the login identifier for user login flood control.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param string $username
+   *   The username supplied in login credentials.
+   *
+   * @return string
+   *   The login identifier or if the user does not exist an empty string.
+   */
+  protected function getLoginFloodIdentifier(Request $request, $username) {
+    $flood_config = $this->config('user.flood');
+    $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]);
+    if ($account = reset($accounts)) {
+      if ($flood_config->get('uid_only')) {
+        // Register flood events based on the uid only, so they apply for any
+        // IP address. This is the most secure option.
+        $identifier = $account->id();
+      }
+      else {
+        // The default identifier is a combination of uid and IP address. This
+        // is less secure but more resistant to denial-of-service attacks that
+        // could lock out all users with public user names.
+        $identifier = $account->id() . '-' . $request->getClientIp();
+      }
+      return $identifier;
+    }
+    return '';
+  }
+
+}