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 Symfony\Component\DependencyInjection\ContainerInterface;
14 use Symfony\Component\HttpFoundation\Request;
15 use Symfony\Component\HttpFoundation\Response;
16 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
17 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
18 use Symfony\Component\Serializer\Encoder\JsonEncoder;
19 use Symfony\Component\Serializer\Serializer;
22 * Provides controllers for login, login status and logout via HTTP requests.
24 class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface {
27 * String sent in responses, to describe the user as being logged in.
34 * String sent in responses, to describe the user as being logged out.
41 * The flood controller.
43 * @var \Drupal\Core\Flood\FloodInterface
50 * @var \Drupal\user\UserStorageInterface
52 protected $userStorage;
55 * The CSRF token generator.
57 * @var \Drupal\Core\Access\CsrfTokenGenerator
62 * The user authentication.
64 * @var \Drupal\user\UserAuthInterface
71 * @var \Drupal\Core\Routing\RouteProviderInterface
73 protected $routeProvider;
78 * @var \Symfony\Component\Serializer\Serializer
80 protected $serializer;
83 * The available serialization formats.
87 protected $serializerFormats = [];
90 * Constructs a new UserAuthenticationController object.
92 * @param \Drupal\Core\Flood\FloodInterface $flood
93 * The flood controller.
94 * @param \Drupal\user\UserStorageInterface $user_storage
96 * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
97 * The CSRF token generator.
98 * @param \Drupal\user\UserAuthInterface $user_auth
99 * The user authentication.
100 * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
101 * The route provider.
102 * @param \Symfony\Component\Serializer\Serializer $serializer
104 * @param array $serializer_formats
105 * The available serialization formats.
107 public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats) {
108 $this->flood = $flood;
109 $this->userStorage = $user_storage;
110 $this->csrfToken = $csrf_token;
111 $this->userAuth = $user_auth;
112 $this->serializer = $serializer;
113 $this->serializerFormats = $serializer_formats;
114 $this->routeProvider = $route_provider;
120 public static function create(ContainerInterface $container) {
121 if ($container->hasParameter('serializer.formats') && $container->has('serializer')) {
122 $serializer = $container->get('serializer');
123 $formats = $container->getParameter('serializer.formats');
127 $encoders = [new JsonEncoder()];
128 $serializer = new Serializer([], $encoders);
132 $container->get('flood'),
133 $container->get('entity_type.manager')->getStorage('user'),
134 $container->get('csrf_token'),
135 $container->get('user.auth'),
136 $container->get('router.route_provider'),
145 * @param \Symfony\Component\HttpFoundation\Request $request
148 * @return \Symfony\Component\HttpFoundation\Response
149 * A response which contains the ID and CSRF token.
151 public function login(Request $request) {
152 $format = $this->getRequestFormat($request);
154 $content = $request->getContent();
155 $credentials = $this->serializer->decode($content, $format);
156 if (!isset($credentials['name']) && !isset($credentials['pass'])) {
157 throw new BadRequestHttpException('Missing credentials.');
160 if (!isset($credentials['name'])) {
161 throw new BadRequestHttpException('Missing credentials.name.');
163 if (!isset($credentials['pass'])) {
164 throw new BadRequestHttpException('Missing credentials.pass.');
167 $this->floodControl($request, $credentials['name']);
169 if ($this->userIsBlocked($credentials['name'])) {
170 throw new BadRequestHttpException('The user has not been activated or is blocked.');
173 if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) {
174 $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
175 /** @var \Drupal\user\UserInterface $user */
176 $user = $this->userStorage->load($uid);
177 $this->userLoginFinalize($user);
179 // Send basic metadata about the logged in user.
181 if ($user->get('uid')->access('view', $user)) {
182 $response_data['current_user']['uid'] = $user->id();
184 if ($user->get('roles')->access('view', $user)) {
185 $response_data['current_user']['roles'] = $user->getRoles();
187 if ($user->get('name')->access('view', $user)) {
188 $response_data['current_user']['name'] = $user->getAccountName();
190 $response_data['csrf_token'] = $this->csrfToken->get('rest');
192 $logout_route = $this->routeProvider->getRouteByName('user.logout.http');
193 // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
194 $logout_path = ltrim($logout_route->getPath(), '/');
195 $response_data['logout_token'] = $this->csrfToken->get($logout_path);
197 $encoded_response_data = $this->serializer->encode($response_data, $format);
198 return new Response($encoded_response_data);
201 $flood_config = $this->config('user.flood');
202 if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
203 $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier);
205 // Always register an IP-based failed login event.
206 $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
207 throw new BadRequestHttpException('Sorry, unrecognized username or password.');
211 * Verifies if the user is blocked.
213 * @param string $name
217 * TRUE if the user is blocked, otherwise FALSE.
219 protected function userIsBlocked($name) {
220 return user_is_blocked($name);
224 * Finalizes the user login.
226 * @param \Drupal\user\UserInterface $user
229 protected function userLoginFinalize(UserInterface $user) {
230 user_login_finalize($user);
236 * @return \Symfony\Component\HttpFoundation\Response
237 * The response object.
239 public function logout() {
241 return new Response(NULL, 204);
247 protected function userLogout() {
252 * Checks whether a user is logged in or not.
254 * @return \Symfony\Component\HttpFoundation\Response
257 public function loginStatus() {
258 if ($this->currentUser()->isAuthenticated()) {
259 $response = new Response(self::LOGGED_IN);
262 $response = new Response(self::LOGGED_OUT);
264 $response->headers->set('Content-Type', 'text/plain');
269 * Gets the format of the current request.
271 * @param \Symfony\Component\HttpFoundation\Request $request
272 * The current request.
275 * The format of the request.
277 protected function getRequestFormat(Request $request) {
278 $format = $request->getRequestFormat();
279 if (!in_array($format, $this->serializerFormats)) {
280 throw new BadRequestHttpException("Unrecognized format: $format.");
286 * Enforces flood control for the current login request.
288 * @param \Symfony\Component\HttpFoundation\Request $request
289 * The current request.
290 * @param string $username
291 * The user name sent for login credentials.
293 protected function floodControl(Request $request, $username) {
294 $flood_config = $this->config('user.flood');
295 if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
296 throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
299 if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
300 // Don't allow login if the limit for this user has been reached.
301 // Default is to allow 5 failed attempts every 6 hours.
302 if (!$this->flood->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
303 if ($flood_config->get('uid_only')) {
304 $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'));
307 $error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
309 throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS);
315 * Gets the login identifier for user login flood control.
317 * @param \Symfony\Component\HttpFoundation\Request $request
318 * The current request.
319 * @param string $username
320 * The username supplied in login credentials.
323 * The login identifier or if the user does not exist an empty string.
325 protected function getLoginFloodIdentifier(Request $request, $username) {
326 $flood_config = $this->config('user.flood');
327 $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]);
328 if ($account = reset($accounts)) {
329 if ($flood_config->get('uid_only')) {
330 // Register flood events based on the uid only, so they apply for any
331 // IP address. This is the most secure option.
332 $identifier = $account->id();
335 // The default identifier is a combination of uid and IP address. This
336 // is less secure but more resistant to denial-of-service attacks that
337 // could lock out all users with public user names.
338 $identifier = $account->id() . '-' . $request->getClientIp();