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