diff --git a/core/core.services.yml b/core/core.services.yml index 9dca742..e391815 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -419,3 +419,23 @@ services: token: class: Drupal\Core\Utility\Token arguments: ['@module_handler'] + authentication: + class: Drupal\Core\Authentication\AuthenticationManager + authentication.cookie: + class: Drupal\Core\Authentication\Provider\Cookie + tags: + - { name: authentication_provider } + authentication.http_basic: + class: Drupal\Core\Authentication\Provider\HttpBasic + tags: + - { name: authentication_provider } + authentication_subscriber: + class: Drupal\Core\EventSubscriber\AuthenticationSubscriber + tags: + - { name: event_subscriber } + calls: + - [setContainer, ['@service_container']] + access_check.authentication_provider: + class: Drupal\Core\Access\AuthenticationProviderAccessCheck + tags: + - { name: access_check } \ No newline at end of file diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 7dc75f5..95cff3f 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -156,11 +156,6 @@ const DRUPAL_BOOTSTRAP_VARIABLES = 4; /** - * Sixth bootstrap phase: initialize session handling. - */ -const DRUPAL_BOOTSTRAP_SESSION = 5; - -/** * Eighth bootstrap phase: load code for subsystems and modules. */ const DRUPAL_BOOTSTRAP_CODE = 6; @@ -1871,7 +1866,6 @@ function drupal_anonymous_user() { * - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page. * - DRUPAL_BOOTSTRAP_DATABASE: Initializes the database layer. * - DRUPAL_BOOTSTRAP_VARIABLES: Initializes the variable system. - * - DRUPAL_BOOTSTRAP_SESSION: Initializes session handling. * - DRUPAL_BOOTSTRAP_CODE: Loads code for subsystems and modules. * - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input * data. @@ -1890,7 +1884,6 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { DRUPAL_BOOTSTRAP_PAGE_CACHE, DRUPAL_BOOTSTRAP_DATABASE, DRUPAL_BOOTSTRAP_VARIABLES, - DRUPAL_BOOTSTRAP_SESSION, DRUPAL_BOOTSTRAP_CODE, DRUPAL_BOOTSTRAP_FULL, ); @@ -1940,11 +1933,6 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { _drupal_bootstrap_variables(); break; - case DRUPAL_BOOTSTRAP_SESSION: - require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); - drupal_session_initialize(); - break; - case DRUPAL_BOOTSTRAP_CODE: require_once __DIR__ . '/common.inc'; _drupal_bootstrap_code(); diff --git a/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php b/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php new file mode 100644 index 0000000..9897f83 --- /dev/null +++ b/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php @@ -0,0 +1,38 @@ +getOption('_auth'); + if (empty($allowed_auth_providers)) { + $allowed_auth_providers = array('cookie'); + } + + $auth_provider_triggered = $request->attributes->get('_authentication_provider'); + return in_array($auth_provider_triggered, $allowed_auth_providers); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Authentication/AuthenticationManager.php b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php new file mode 100644 index 0000000..0dcbc91 --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php @@ -0,0 +1,58 @@ +providers[$provider_id] = $provider; + } + + public function authenticate(Request $request) { + $authentication_happened = FALSE; + + foreach ($this->providers as $provider) { + $result = $provider->authenticate($request); + + if ($result !== NULL) { + // We do not allow request to have information to authenticate + // with two methods at the same time. + if ($authentication_happened) { + throw new BadRequestHttpException(t('Multiple authentication methods are not allowed.')); + } + $authentication_happened = TRUE; + + // Save id of provider to request so it is checked in AuthenticationProviderAccessCheck. + $provider_container_service_id = $this->getProviderContainerServiceId($provider); + $request->attributes->set('_authentication_provider', $provider_container_service_id); + } + } + } + + /** + * Retrieves key of authentication provider how it is registered in DIC. + * + * For example Drupal\Core\Authentication\Provider\Cookie is registered as authentication.cookie + * so this function will return 'cookie'. + * + * @param $provider + * + * @return string $service_id. + */ + protected function getProviderContainerServiceId($provider_to_find) { + + $provider_class = get_class($provider_to_find); + foreach ($this->providers as $provider_id => $provider) { + if (get_class($provider) == $provider_class) { + break; + } + } + + return substr($provider_id, strlen('authentication.')); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Authentication/AuthenticationManagerInterface.php b/core/lib/Drupal/Core/Authentication/AuthenticationManagerInterface.php new file mode 100644 index 0000000..3aab4b7 --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManagerInterface.php @@ -0,0 +1,9 @@ +get('session_inc', 'core/includes/session.inc'); + drupal_session_initialize(); + if (user_is_logged_in()) { + return TRUE; + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php b/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php new file mode 100644 index 0000000..874543d --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php @@ -0,0 +1,22 @@ +server->get('PHP_AUTH_USER'); + $password = $request->server->get('PHP_AUTH_PW'); + if ($username && $password) { + $uid = user_authenticate($username, $password); + if ($uid) { + global $user; + $user = user_load($uid); + return TRUE; + } + return FALSE; + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 2d85f6a..360c655 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -16,6 +16,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterAuthenticationPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -60,6 +61,8 @@ public function build(ContainerBuilder $container) { // Add the compiler pass that will process the tagged services. $container->addCompilerPass(new RegisterPathProcessorsPass()); $container->addCompilerPass(new ListCacheBinsPass()); + // Addd the compiler pass that will process tagged authentication services. + $container->addCompilerPass(new RegisterAuthenticationPass()); } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAuthenticationPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAuthenticationPass.php new file mode 100644 index 0000000..6a0728d --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAuthenticationPass.php @@ -0,0 +1,28 @@ +hasDefinition('authentication')) { + return; + } + $matcher = $container->getDefinition('authentication'); + foreach ($container->findTaggedServiceIds('authentication_provider') as $id => $attributes) { + $matcher->addMethodCall('addProvider', array($id, new Reference($id))); + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php new file mode 100644 index 0000000..afacbfa --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php @@ -0,0 +1,51 @@ +getRequestType() == HttpKernelInterface::MASTER_REQUEST) { + $authentication_manager = $this->container->get('authentication'); + $request = $event->getRequest(); + + // Include sessions.inc file because + // core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php + // has drupal_session_commit(). + require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); + + $authentication_manager->authenticate($request); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events[KernelEvents::REQUEST][] = array('onKernelRequestAuthenticate', 100); + return $events; + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Authentication/HttpBasicTest.php b/core/modules/system/lib/Drupal/system/Tests/Authentication/HttpBasicTest.php new file mode 100644 index 0000000..a6901c7 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Authentication/HttpBasicTest.php @@ -0,0 +1,68 @@ + 'HttpBasic authentication', + 'description' => 'Tests for HttpBasic authentication provider.', + 'group' => 'Authentication', + ); + } + + public function testHttpBasic() { + $account = $this->drupalCreateUser(); + + $this->basicAuthGet('router_test/test11', $account->name, $account->pass_raw); + $this->assertText($account->name, 'Account name should be displayed.'); + $this->curlClose(); + + $this->basicAuthGet('router_test/test11', $account->name, $this->randomName()); + $this->assertNoText($account->name, 'Bad basic auth credentials do not authenticate the user.'); + $this->curlClose(); + + // Login to have a valid session. + $this->drupalLogin($account); + // This call should have both session cookies and basic authentication. So it should fail. + $this->basicAuthGet('router_test/test11', $account->name, $account->pass_raw); + $this->assertResponse('400', 'Multiple authentication methods in request lead to 400.'); + $this->curlClose(); + + $this->drupalGet('router_test/test11'); + $this->assertResponse('403', 'Not authenticated on the route that allows only http_basic.'); + } + + /** + * We do not use drupalGet because we need to set curl settings for basic authentication. + * + * @param string $username + * @param string $password + * + * @return string + * Curl output. + */ + protected function basicAuthGet($path, $username, $password) { + $out = $this->curlExec( + array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_URL => url($path, array('absolute' => TRUE)), + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_USERPWD => $username . ':' . $password, + ) + ); + + $this->verbose('GET request to: ' . $path . + '
' . $out); + + return $out; + } +} \ No newline at end of file diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php index 56d567b..f7deee8 100644 --- a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php +++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php @@ -19,4 +19,12 @@ public function test1() { return 'abcde'; } + /** + * Provides example content for route specific authentication. + */ + public function test11() { + global $user; + return $user->name; + } + } diff --git a/core/modules/system/tests/modules/router_test/router_test.routing.yml b/core/modules/system/tests/modules/router_test/router_test.routing.yml index 2a989df..3ba8902 100644 --- a/core/modules/system/tests/modules/router_test/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test/router_test.routing.yml @@ -60,3 +60,10 @@ router_test_10: _content: '\Drupal\router_test\TestContent::test1' requirements: _access: 'TRUE' + +router_test_11: + pattern: '/router_test/test11' + options: + _auth: [ 'http_basic' ] + defaults: + _content: '\Drupal\router_test\TestContent::test11' \ No newline at end of file