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