48732bc86dd44be48792cb776a92b37816bdd12a
[yaffs-website] / web / core / modules / basic_auth / src / Authentication / Provider / BasicAuth.php
1 <?php
2
3 namespace Drupal\basic_auth\Authentication\Provider;
4
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;
15
16 /**
17  * HTTP Basic authentication provider.
18  */
19 class BasicAuth implements AuthenticationProviderInterface, AuthenticationProviderChallengeInterface {
20
21   /**
22    * The config factory.
23    *
24    * @var \Drupal\Core\Config\ConfigFactoryInterface
25    */
26   protected $configFactory;
27
28   /**
29    * The user auth service.
30    *
31    * @var \Drupal\user\UserAuthInterface
32    */
33   protected $userAuth;
34
35   /**
36    * The flood service.
37    *
38    * @var \Drupal\Core\Flood\FloodInterface
39    */
40   protected $flood;
41
42   /**
43    * The entity manager.
44    *
45    * @var \Drupal\Core\Entity\EntityManagerInterface
46    */
47   protected $entityManager;
48
49   /**
50    * Constructs a HTTP basic authentication provider object.
51    *
52    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
53    *   The config factory.
54    * @param \Drupal\user\UserAuthInterface $user_auth
55    *   The user authentication service.
56    * @param \Drupal\Core\Flood\FloodInterface $flood
57    *   The flood service.
58    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
59    *   The entity manager service.
60    */
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;
66   }
67
68   /**
69    * {@inheritdoc}
70    */
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);
75   }
76
77   /**
78    * {@inheritdoc}
79    */
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);
94       if ($account) {
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();
99         }
100         else {
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();
105         }
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);
110           if ($uid) {
111             $this->flood->clear('basic_auth.failed_login_user', $identifier);
112             return $this->entityManager->getStorage('user')->load($uid);
113           }
114           else {
115             // Register a per-user failed login event.
116             $this->flood->register('basic_auth.failed_login_user', $flood_config->get('user_window'), $identifier);
117           }
118         }
119       }
120     }
121     // Always register an IP-based failed login event.
122     $this->flood->register('basic_auth.failed_login_ip', $flood_config->get('ip_window'));
123     return [];
124   }
125
126   /**
127    * {@inheritdoc}
128    */
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',
134     ]);
135
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
145     //      credentials.
146     //    - Dynamic Page Cache will cache a different result for when the
147     //      request is unauthenticated (this 401) versus authenticated (some
148     //      other response)
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);
159   }
160
161 }