3 namespace Drupal\user\Controller;
5 use Drupal\Core\Access\CsrfTokenGenerator;
6 use Drupal\Core\Controller\ControllerBase;
7 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
8 use Drupal\Core\Flood\FloodInterface;
9 use Drupal\Core\Routing\RouteProviderInterface;
10 use Drupal\user\UserAuthInterface;
11 use Drupal\user\UserInterface;
12 use Drupal\user\UserStorageInterface;
13 use Psr\Log\LoggerInterface;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15 use Symfony\Component\HttpFoundation\Request;
16 use Symfony\Component\HttpFoundation\Response;
17 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
18 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
19 use Symfony\Component\Serializer\Encoder\JsonEncoder;
20 use Symfony\Component\Serializer\Serializer;
23 * Provides controllers for login, login status and logout via HTTP requests.
25 class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface {
28 * String sent in responses, to describe the user as being logged in.
35 * String sent in responses, to describe the user as being logged out.
42 * The flood controller.
44 * @var \Drupal\Core\Flood\FloodInterface
51 * @var \Drupal\user\UserStorageInterface
53 protected $userStorage;
56 * The CSRF token generator.
58 * @var \Drupal\Core\Access\CsrfTokenGenerator
63 * The user authentication.
65 * @var \Drupal\user\UserAuthInterface
72 * @var \Drupal\Core\Routing\RouteProviderInterface
74 protected $routeProvider;
79 * @var \Symfony\Component\Serializer\Serializer
81 protected $serializer;
84 * The available serialization formats.
88 protected $serializerFormats = [];
93 * @var \Psr\Log\LoggerInterface
98 * Constructs a new UserAuthenticationController object.
100 * @param \Drupal\Core\Flood\FloodInterface $flood
101 * The flood controller.
102 * @param \Drupal\user\UserStorageInterface $user_storage
104 * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
105 * The CSRF token generator.
106 * @param \Drupal\user\UserAuthInterface $user_auth
107 * The user authentication.
108 * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
109 * The route provider.
110 * @param \Symfony\Component\Serializer\Serializer $serializer
112 * @param array $serializer_formats
113 * The available serialization formats.
114 * @param \Psr\Log\LoggerInterface $logger
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) {
118 $this->flood = $flood;
119 $this->userStorage = $user_storage;
120 $this->csrfToken = $csrf_token;
121 $this->userAuth = $user_auth;
122 $this->serializer = $serializer;
123 $this->serializerFormats = $serializer_formats;
124 $this->routeProvider = $route_provider;
125 $this->logger = $logger;
131 public static function create(ContainerInterface $container) {
132 if ($container->hasParameter('serializer.formats') && $container->has('serializer')) {
133 $serializer = $container->get('serializer');
134 $formats = $container->getParameter('serializer.formats');
138 $encoders = [new JsonEncoder()];
139 $serializer = new Serializer([], $encoders);
143 $container->get('flood'),
144 $container->get('entity_type.manager')->getStorage('user'),
145 $container->get('csrf_token'),
146 $container->get('user.auth'),
147 $container->get('router.route_provider'),
150 $container->get('logger.factory')->get('user')
157 * @param \Symfony\Component\HttpFoundation\Request $request
160 * @return \Symfony\Component\HttpFoundation\Response
161 * A response which contains the ID and CSRF token.
163 public function login(Request $request) {
164 $format = $this->getRequestFormat($request);
166 $content = $request->getContent();
167 $credentials = $this->serializer->decode($content, $format);
168 if (!isset($credentials['name']) && !isset($credentials['pass'])) {
169 throw new BadRequestHttpException('Missing credentials.');
172 if (!isset($credentials['name'])) {
173 throw new BadRequestHttpException('Missing credentials.name.');
175 if (!isset($credentials['pass'])) {
176 throw new BadRequestHttpException('Missing credentials.pass.');
179 $this->floodControl($request, $credentials['name']);
181 if ($this->userIsBlocked($credentials['name'])) {
182 throw new BadRequestHttpException('The user has not been activated or is blocked.');
185 if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) {
186 $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
187 /** @var \Drupal\user\UserInterface $user */
188 $user = $this->userStorage->load($uid);
189 $this->userLoginFinalize($user);
191 // Send basic metadata about the logged in user.
193 if ($user->get('uid')->access('view', $user)) {
194 $response_data['current_user']['uid'] = $user->id();
196 if ($user->get('roles')->access('view', $user)) {
197 $response_data['current_user']['roles'] = $user->getRoles();
199 if ($user->get('name')->access('view', $user)) {
200 $response_data['current_user']['name'] = $user->getAccountName();
202 $response_data['csrf_token'] = $this->csrfToken->get('rest');
204 $logout_route = $this->routeProvider->getRouteByName('user.logout.http');
205 // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
206 $logout_path = ltrim($logout_route->getPath(), '/');
207 $response_data['logout_token'] = $this->csrfToken->get($logout_path);
209 $encoded_response_data = $this->serializer->encode($response_data, $format);
210 return new Response($encoded_response_data);
213 $flood_config = $this->config('user.flood');
214 if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
215 $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier);
217 // Always register an IP-based failed login event.
218 $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
219 throw new BadRequestHttpException('Sorry, unrecognized username or password.');
223 * Resets a user password.
225 * @param \Symfony\Component\HttpFoundation\Request $request
228 * @return \Symfony\Component\HttpFoundation\Response
229 * The response object.
231 public function resetPassword(Request $request) {
232 $format = $this->getRequestFormat($request);
234 $content = $request->getContent();
235 $credentials = $this->serializer->decode($content, $format);
237 // Check if a name or mail is provided.
238 if (!isset($credentials['name']) && !isset($credentials['mail'])) {
239 throw new BadRequestHttpException('Missing credentials.name or credentials.mail');
242 // Load by name if provided.
243 if (isset($credentials['name'])) {
244 $users = $this->userStorage->loadByProperties(['name' => trim($credentials['name'])]);
246 elseif (isset($credentials['mail'])) {
247 $users = $this->userStorage->loadByProperties(['mail' => trim($credentials['mail'])]);
250 /** @var \Drupal\Core\Session\AccountInterface $account */
251 $account = reset($users);
252 if ($account && $account->id()) {
253 if ($this->userIsBlocked($account->getAccountName())) {
254 throw new BadRequestHttpException('The user has not been activated or is blocked.');
257 // Send the password reset email.
258 $mail = _user_mail_notify('password_reset', $account, $account->getPreferredLangcode());
260 throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
263 $this->logger->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
264 return new Response();
268 // Error if no users found with provided name or mail.
269 throw new BadRequestHttpException('Unrecognized username or email address.');
273 * Verifies if the user is blocked.
275 * @param string $name
279 * TRUE if the user is blocked, otherwise FALSE.
281 protected function userIsBlocked($name) {
282 return user_is_blocked($name);
286 * Finalizes the user login.
288 * @param \Drupal\user\UserInterface $user
291 protected function userLoginFinalize(UserInterface $user) {
292 user_login_finalize($user);
298 * @return \Symfony\Component\HttpFoundation\Response
299 * The response object.
301 public function logout() {
303 return new Response(NULL, 204);
309 protected function userLogout() {
314 * Checks whether a user is logged in or not.
316 * @return \Symfony\Component\HttpFoundation\Response
319 public function loginStatus() {
320 if ($this->currentUser()->isAuthenticated()) {
321 $response = new Response(self::LOGGED_IN);
324 $response = new Response(self::LOGGED_OUT);
326 $response->headers->set('Content-Type', 'text/plain');
331 * Gets the format of the current request.
333 * @param \Symfony\Component\HttpFoundation\Request $request
334 * The current request.
337 * The format of the request.
339 protected function getRequestFormat(Request $request) {
340 $format = $request->getRequestFormat();
341 if (!in_array($format, $this->serializerFormats)) {
342 throw new BadRequestHttpException("Unrecognized format: $format.");
348 * Enforces flood control for the current login request.
350 * @param \Symfony\Component\HttpFoundation\Request $request
351 * The current request.
352 * @param string $username
353 * The user name sent for login credentials.
355 protected function floodControl(Request $request, $username) {
356 $flood_config = $this->config('user.flood');
357 if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
358 throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
361 if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
362 // Don't allow login if the limit for this user has been reached.
363 // Default is to allow 5 failed attempts every 6 hours.
364 if (!$this->flood->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
365 if ($flood_config->get('uid_only')) {
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'));
369 $error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
371 throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS);
377 * Gets the login identifier for user login flood control.
379 * @param \Symfony\Component\HttpFoundation\Request $request
380 * The current request.
381 * @param string $username
382 * The username supplied in login credentials.
385 * The login identifier or if the user does not exist an empty string.
387 protected function getLoginFloodIdentifier(Request $request, $username) {
388 $flood_config = $this->config('user.flood');
389 $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]);
390 if ($account = reset($accounts)) {
391 if ($flood_config->get('uid_only')) {
392 // Register flood events based on the uid only, so they apply for any
393 // IP address. This is the most secure option.
394 $identifier = $account->id();
397 // The default identifier is a combination of uid and IP address. This
398 // is less secure but more resistant to denial-of-service attacks that
399 // could lock out all users with public user names.
400 $identifier = $account->id() . '-' . $request->getClientIp();