345 lines
11 KiB
PHP
345 lines
11 KiB
PHP
<?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 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 = [];
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats) {
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* {@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
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.');
|
|
}
|
|
|
|
/**
|
|
* 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 '';
|
|
}
|
|
|
|
}
|