3 namespace Drupal\basic_auth\Authentication\Provider;
5 use Drupal\Component\Render\FormattableMarkup;
6 use Drupal\Core\Authentication\AuthenticationProviderInterface;
7 use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
8 use Drupal\Core\Cache\CacheableMetadata;
9 use Drupal\Core\Config\ConfigFactoryInterface;
10 use Drupal\Core\Entity\EntityManagerInterface;
11 use Drupal\Core\Flood\FloodInterface;
12 use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
13 use Drupal\user\UserAuthInterface;
14 use Symfony\Component\HttpFoundation\Request;
17 * HTTP Basic authentication provider.
19 class BasicAuth implements AuthenticationProviderInterface, AuthenticationProviderChallengeInterface {
24 * @var \Drupal\Core\Config\ConfigFactoryInterface
26 protected $configFactory;
29 * The user auth service.
31 * @var \Drupal\user\UserAuthInterface
38 * @var \Drupal\Core\Flood\FloodInterface
45 * @var \Drupal\Core\Entity\EntityManagerInterface
47 protected $entityManager;
50 * Constructs a HTTP basic authentication provider object.
52 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
54 * @param \Drupal\user\UserAuthInterface $user_auth
55 * The user authentication service.
56 * @param \Drupal\Core\Flood\FloodInterface $flood
58 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
59 * The entity manager service.
61 public function __construct(ConfigFactoryInterface $config_factory, UserAuthInterface $user_auth, FloodInterface $flood, EntityManagerInterface $entity_manager) {
62 $this->configFactory = $config_factory;
63 $this->userAuth = $user_auth;
64 $this->flood = $flood;
65 $this->entityManager = $entity_manager;
71 public function applies(Request $request) {
72 $username = $request->headers->get('PHP_AUTH_USER');
73 $password = $request->headers->get('PHP_AUTH_PW');
74 return isset($username) && isset($password);
80 public function authenticate(Request $request) {
81 $flood_config = $this->configFactory->get('user.flood');
82 $username = $request->headers->get('PHP_AUTH_USER');
83 $password = $request->headers->get('PHP_AUTH_PW');
84 // Flood protection: this is very similar to the user login form code.
85 // @see \Drupal\user\Form\UserLoginForm::validateAuthentication()
86 // Do not allow any login from the current user's IP if the limit has been
87 // reached. Default is 50 failed attempts allowed in one hour. This is
88 // independent of the per-user limit to catch attempts from one IP to log
89 // in to many different user accounts. We have a reasonably high limit
90 // since there may be only one apparent IP for all users at an institution.
91 if ($this->flood->isAllowed('basic_auth.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
92 $accounts = $this->entityManager->getStorage('user')->loadByProperties(['name' => $username, 'status' => 1]);
93 $account = reset($accounts);
95 if ($flood_config->get('uid_only')) {
96 // Register flood events based on the uid only, so they apply for any
97 // IP address. This is the most secure option.
98 $identifier = $account->id();
101 // The default identifier is a combination of uid and IP address. This
102 // is less secure but more resistant to denial-of-service attacks that
103 // could lock out all users with public user names.
104 $identifier = $account->id() . '-' . $request->getClientIP();
106 // Don't allow login if the limit for this user has been reached.
107 // Default is to allow 5 failed attempts every 6 hours.
108 if ($this->flood->isAllowed('basic_auth.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
109 $uid = $this->userAuth->authenticate($username, $password);
111 $this->flood->clear('basic_auth.failed_login_user', $identifier);
112 return $this->entityManager->getStorage('user')->load($uid);
115 // Register a per-user failed login event.
116 $this->flood->register('basic_auth.failed_login_user', $flood_config->get('user_window'), $identifier);
121 // Always register an IP-based failed login event.
122 $this->flood->register('basic_auth.failed_login_ip', $flood_config->get('ip_window'));
129 public function challengeException(Request $request, \Exception $previous) {
130 $site_config = $this->configFactory->get('system.site');
131 $site_name = $site_config->get('name');
132 $challenge = new FormattableMarkup('Basic realm="@realm"', [
133 '@realm' => !empty($site_name) ? $site_name : 'Access restricted',
136 // A 403 is converted to a 401 here, but it doesn't matter what the
137 // cacheability was of the 403 exception: what matters here is that
138 // authentication credentials are missing, i.e. that this request was made
139 // as the anonymous user.
140 // Therefore, all we must do, is make this response:
141 // 1. vary by whether the current user has the 'anonymous' role or not. This
142 // works fine because:
143 // - Thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests,
144 // Page Cache never caches a response whose request has Basic Auth
146 // - Dynamic Page Cache will cache a different result for when the
147 // request is unauthenticated (this 401) versus authenticated (some
149 // 2. have the 'config:user.role.anonymous' cache tag, because the only
150 // reason this 401 would no longer be a 401 is if permissions for the
151 // 'anonymous' role change, causing that cache tag to be invalidated.
152 // @see \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
153 // @see \Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber()
154 // @see \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onAllResponds()
155 $cacheability = CacheableMetadata::createFromObject($site_config)
156 ->addCacheTags(['config:user.role.anonymous'])
157 ->addCacheContexts(['user.roles:anonymous']);
158 return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);