class RestAuthenticationController

Same name and namespace in other branches
  1. main core/modules/rest/src/Controller/RestAuthenticationController.php \Drupal\rest\Controller\RestAuthenticationController

Provides controllers for login, login status and logout via HTTP requests.

Hierarchy

Expanded class hierarchy of RestAuthenticationController

1 file declares its use of RestAuthenticationController
RestLoginHttpTest.php in core/modules/rest/tests/src/Functional/RestLoginHttpTest.php

File

core/modules/rest/src/Controller/RestAuthenticationController.php, line 25

Namespace

Drupal\rest\Controller
View source
class RestAuthenticationController 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';
  
  /**
   * @param \Drupal\user\UserFloodControlInterface $userFloodControl
   *   The user flood control service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrfToken
   *   The CSRF token generator.
   * @param \Drupal\user\UserAuthenticationInterface|\Drupal\user\UserAuthInterface $userAuth
   *   The user authentication.
   * @param \Drupal\Core\Routing\RouteProviderInterface $routeProvider
   *   The route provider.
   * @param \Symfony\Component\Serializer\Serializer $serializer
   *   The serializer.
   * @param array $serializerFormats
   *   The available serialization formats.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   */
  public function __construct(protected UserFloodControlInterface $userFloodControl, EntityTypeManagerInterface $entity_type_manager, protected CsrfTokenGenerator $csrfToken, protected UserAuthenticationInterface|UserAuthInterface $userAuth, protected RouteProviderInterface $routeProvider, protected Serializer $serializer, protected array $serializerFormats, protected LoggerInterface $logger) {
    $this->entityTypeManager = $entity_type_manager;
    if (!$userAuth instanceof UserAuthenticationInterface) {
      @trigger_error('The $userAuth parameter implementing UserAuthInterface is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. Implement UserAuthenticationInterface instead. See https://www.drupal.org/node/3411040');
    }
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) : static {
    return new static($container->get('user.flood_control'), $container->get('entity_type.manager'), $container->get('csrf_token'), $container->get('user.auth'), $container->get('router.route_provider'), $container->get('serializer'), $container->getParameter('serializer.formats'), $container->get('logger.factory')
      ->get('rest'));
  }
  
  /**
   * 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) : Response {
    $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']);
    $account = FALSE;
    if ($this->userAuth instanceof UserAuthenticationInterface) {
      $account = $this->userAuth
        ->lookupAccount($credentials['name']);
    }
    else {
      $accounts = $this->entityTypeManager
        ->getStorage('user')
        ->loadByProperties([
        'name' => $credentials['name'],
      ]);
      if ($accounts) {
        $account = reset($accounts);
      }
    }
    if ($account) {
      if ($account->isBlocked()) {
        throw new BadRequestHttpException('The user has not been activated or is blocked.');
      }
      if ($this->userAuth instanceof UserAuthenticationInterface) {
        $authenticated = $this->userAuth
          ->authenticateAccount($account, $credentials['pass']) ? $account->id() : FALSE;
      }
      else {
        $authenticated = $this->userAuth
          ->authenticate($credentials['name'], $credentials['pass']);
      }
      if ($authenticated) {
        $this->userFloodControl
          ->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
        $this->userLoginFinalize($account);
        // Send basic metadata about the logged in user.
        $response_data = [];
        if ($account->get('uid')
          ->access('view', $account)) {
          $response_data['current_user']['uid'] = $account->id();
        }
        if ($account->get('roles')
          ->access('view', $account)) {
          $response_data['current_user']['roles'] = $account->getRoles();
        }
        if ($account->get('name')
          ->access('view', $account)) {
          $response_data['current_user']['name'] = $account->getAccountName();
        }
        $response_data['csrf_token'] = $this->csrfToken
          ->get('rest');
        $logout_route = $this->routeProvider
          ->getRouteByName('rest.logout');
        // 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->userFloodControl
        ->register('user.http_login', $flood_config->get('user_window'), $identifier);
    }
    // Always register an IP-based failed login event.
    $this->userFloodControl
      ->register('user.failed_login_ip', $flood_config->get('ip_window'));
    throw new BadRequestHttpException('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) : Response {
    $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.
    $identifier = '';
    $users = [];
    if (isset($credentials['name'])) {
      $identifier = $credentials['name'];
      $users = $this->entityTypeManager
        ->getStorage('user')
        ->loadByProperties([
        'name' => trim($identifier),
      ]);
    }
    elseif (isset($credentials['mail'])) {
      $identifier = $credentials['mail'];
      $users = $this->entityTypeManager
        ->getStorage('user')
        ->loadByProperties([
        'mail' => trim($identifier),
      ]);
    }
    /** @var \Drupal\user\UserInterface $account */
    $account = reset($users);
    if ($account && $account->id()) {
      if ($account->isBlocked()) {
        $this->logger
          ->error('Unable to send password reset email for blocked or not yet activated user %identifier.', [
          '%identifier' => $identifier,
        ]);
        return new Response();
      }
      // Send the password reset email.
      $mail = _user_mail_notify('password_reset', $account);
      if (empty($mail)) {
        throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
      }
      else {
        $this->logger
          ->info('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.
    $this->logger
      ->error('Unable to send password reset email for unrecognized username or email address %identifier.', [
      '%identifier' => $identifier,
    ]);
    return new Response();
  }
  
  /**
   * Finalizes the user login.
   *
   * @param \Drupal\user\UserInterface $user
   *   The user.
   */
  protected function userLoginFinalize(UserInterface $user) : void {
    user_login_finalize($user);
  }
  
  /**
   * Logs out a user.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   The response object.
   */
  public function logout() : Response {
    $this->userLogout();
    return new Response(NULL, 204);
  }
  
  /**
   * Logs the user out.
   */
  protected function userLogout() : void {
    user_logout();
  }
  
  /**
   * Checks whether a user is logged in or not.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   The response.
   */
  public function loginStatus() : Response {
    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) : string {
    $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, string $username) : void {
    $flood_config = $this->config('user.flood');
    if (!$this->userFloodControl
      ->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->userFloodControl
        ->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, string $username) : string {
    $flood_config = $this->config('user.flood');
    $accounts = $this->entityTypeManager
      ->getStorage('user')
      ->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 '';
  }

}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.