diff --git a/core/modules/basic_auth/basic_auth.services.yml b/core/modules/basic_auth/basic_auth.services.yml index 13a6cb15b90b8b7691f0b970d3870d52e3436207..4c7a4493d2a937f15db78b381f85aa8bdce08fee 100644 --- a/core/modules/basic_auth/basic_auth.services.yml +++ b/core/modules/basic_auth/basic_auth.services.yml @@ -1,6 +1,6 @@ services: authentication.basic_auth: class: Drupal\basic_auth\Authentication\Provider\BasicAuth - arguments: ['@config.factory'] + arguments: ['@config.factory', '@user.auth'] tags: - { name: authentication_provider, priority: 100 } diff --git a/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php index 282b89102dcd2fe077fa9ad08b6bbcd071c57623..579d8a0236f1e363da37d6c55b1e130572aea913 100644 --- a/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php +++ b/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php @@ -11,6 +11,7 @@ use Drupal\Core\Authentication\AuthenticationProviderInterface; use Drupal\Core\Config\Config; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\user\UserAuthInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; @@ -28,14 +29,22 @@ class BasicAuth implements AuthenticationProviderInterface { */ protected $configFactory; + /** + * The user auth service. + * + * @var \Drupal\user\UserAuthInterface + */ + protected $userAuth; + /** * Constructs a HTTP basic authentication provider object. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. */ - public function __construct(ConfigFactoryInterface $config_factory) { + public function __construct(ConfigFactoryInterface $config_factory, UserAuthInterface $user_auth) { $this->configFactory = $config_factory; + $this->userAuth = $user_auth; } /** @@ -53,7 +62,7 @@ public function applies(Request $request) { public function authenticate(Request $request) { $username = $request->headers->get('PHP_AUTH_USER'); $password = $request->headers->get('PHP_AUTH_PW'); - $uid = user_authenticate($username, $password); + $uid = $this->userAuth->authenticate($username, $password); if ($uid) { return user_load($uid); } diff --git a/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php b/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php index 46744a4dbbddd4fcdb8efcfb4335fa5f4e493a5b..dcc6845d5cb2a778793f4ff7c83abb2a1f1ab260 100644 --- a/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php +++ b/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php @@ -9,6 +9,7 @@ use Drupal\Core\Flood\FloodInterface; use Drupal\Core\Form\FormBase; +use Drupal\user\UserAuthInterface; use Drupal\user\UserStorageControllerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,6 +32,13 @@ class UserLoginForm extends FormBase { */ protected $userStorage; + /** + * The user authentication object. + * + * @var \Drupal\user\UserAuthInterface + */ + protected $userAuth; + /** * Constructs a new UserLoginForm. * @@ -38,10 +46,13 @@ class UserLoginForm extends FormBase { * The flood service. * @param \Drupal\user\UserStorageControllerInterface $user_storage * The user storage controller. + * @param \Drupal\user\UserAuthInterface $user_auth + * The user authentication object. */ - public function __construct(FloodInterface $flood, UserStorageControllerInterface $user_storage) { + public function __construct(FloodInterface $flood, UserStorageControllerInterface $user_storage, UserAuthInterface $user_auth) { $this->flood = $flood; $this->userStorage = $user_storage; + $this->userAuth = $user_auth; } /** @@ -50,7 +61,8 @@ public function __construct(FloodInterface $flood, UserStorageControllerInterfac public static function create(ContainerInterface $container) { return new static( $container->get('flood'), - $container->get('entity.manager')->getStorageController('user') + $container->get('entity.manager')->getStorageController('user'), + $container->get('user.auth') ); } @@ -165,7 +177,7 @@ public function validateAuthentication(array &$form, array &$form_state) { } // We are not limited by flood control, so try to authenticate. // Set $form_state['uid'] as a flag for self::validateFinal(). - $form_state['uid'] = user_authenticate($form_state['values']['name'], $password); + $form_state['uid'] = $this->userAuth->authenticate($form_state['values']['name'], $password); } } diff --git a/core/modules/user/lib/Drupal/user/UserAuth.php b/core/modules/user/lib/Drupal/user/UserAuth.php new file mode 100644 index 0000000000000000000000000000000000000000..3ce2683a4387d7d737b26db24ea34287cc6846c5 --- /dev/null +++ b/core/modules/user/lib/Drupal/user/UserAuth.php @@ -0,0 +1,72 @@ +<?php + +/** + * @file + * Contains \Drupal\user\UserAuth. + */ + +namespace Drupal\user; + +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityStorageControllerInterface; +use Drupal\Core\Password\PasswordInterface; + +/** + * Validates user authentication credentials. + */ +class UserAuth implements UserAuthInterface { + + /** + * The user storage. + * + * @var \Drupal\Core\Entity\EntityStorageControllerInterface + */ + protected $storage; + + /** + * The password service. + * + * @var \Drupal\Core\Password\PasswordInterface + */ + protected $passwordChecker; + + /** + * Constructs a UserAuth object. + * + * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage + * The user storage. + * @param \Drupal\Core\Password\PasswordInterface $password_checker + * The password service. + */ + public function __construct(EntityManagerInterface $entity_manager, PasswordInterface $password_checker) { + $this->storage = $entity_manager->getStorageController('user'); + $this->passwordChecker = $password_checker; + } + + /** + * {@inheritdoc} + */ + public function authenticate($username, $password) { + $uid = FALSE; + + if (!empty($username) && !empty($password)) { + $account_search = $this->storage->loadByProperties(array('name' => $username)); + + if ($account = reset($account_search)) { + if ($this->passwordChecker->check($password, $account)) { + // Successful authentication. + $uid = $account->id(); + + // Update user to new password scheme if needed. + if ($this->passwordChecker->userNeedsNewHash($account)) { + $account->setPassword($password); + $account->save(); + } + } + } + } + + return $uid; + } + +} diff --git a/core/modules/user/lib/Drupal/user/UserAuthInterface.php b/core/modules/user/lib/Drupal/user/UserAuthInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..1044ec18e9507b4a89b9303d4b11da3ae0f1fb6f --- /dev/null +++ b/core/modules/user/lib/Drupal/user/UserAuthInterface.php @@ -0,0 +1,27 @@ +<?php + +/** + * @file + * Contains \Drupal\user\UserAuthInterface. + */ + +namespace Drupal\user; + +/** + * An interface for validating user authentication credentials. + */ +interface UserAuthInterface { + + /** + * Validates user authentication credentials. + * + * @param string $username + * The user name to authenticate. + * @param string $password + * A plain-text password, such as trimmed text from form values. + * @return int|bool + * The user's uid on success, or FALSE on failure to authenticate. + */ + public function authenticate($username, $password); + +} diff --git a/core/modules/user/tests/Drupal/user/Tests/UserAuthTest.php b/core/modules/user/tests/Drupal/user/Tests/UserAuthTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c658e608f7d90d4ed4700137f23bf1f19744e1e0 --- /dev/null +++ b/core/modules/user/tests/Drupal/user/Tests/UserAuthTest.php @@ -0,0 +1,215 @@ +<?php + +/** + * @file + * Contains \Drupal\user\Tests\UserAuthTest. + */ + +namespace Drupal\user\Tests; + +use Drupal\Tests\UnitTestCase; +use Drupal\user\UserAuth; + +/** + * Tests the UserAuth class. + * + * @group Drupal + * @group User + * + * @coverDefaultClass \Drupal\user\UserAuth + */ +class UserAuthTest extends UnitTestCase { + + /** + * The mock user storage controller. + * + * @var \Drupal\Core\Entity\EntityStorageControllerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $userStorage; + + /** + * The mocked password service. + * + * @var \Drupal\Core\Password\PasswordInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $passwordService; + + /** + * The mock user. + * + * @var \Drupal\user\Entity\User|\PHPUnit_Framework_MockObject_MockObject + */ + protected $testUser; + + /** + * The user auth object under test. + * + * @var \Drupal\user\UserAuth + */ + protected $userAuth; + + /** + * The test username. + * + * @var string + */ + protected $username = 'test_user'; + + /** + * The test password + * + * @var string + */ + protected $password = 'password'; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'User auth service', + 'description' => 'Tests the user auth service', + 'group' => 'User', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + $this->userStorage = $this->getMock('Drupal\Core\Entity\EntityStorageControllerInterface'); + + $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + // Getting the user storage controller should only happen once per test. + $entity_manager->expects($this->once()) + ->method('getStorageController') + ->with('user') + ->will($this->returnValue($this->userStorage)); + + $this->passwordService = $this->getMock('Drupal\Core\Password\PasswordInterface'); + + $this->testUser = $this->getMockBuilder('Drupal\user\Entity\User') + ->disableOriginalConstructor() + ->setMethods(array('id', 'setPassword', 'save')) + ->getMock(); + + $this->userAuth = new UserAuth($entity_manager, $this->passwordService); + } + + /** + * Tests failing authentication with missing credential parameters. + * + * @covers ::authenticate() + * + * @dataProvider providerTestAuthenticateWithMissingCredentials + */ + public function testAuthenticateWithMissingCredentials($username, $password) { + $this->userStorage->expects($this->never()) + ->method('loadByProperties'); + + $this->assertFalse($this->userAuth->authenticate($username, $password)); + } + + /** + * Data provider for testAuthenticateWithMissingCredentials(). + * + * @return array + */ + public function providerTestAuthenticateWithMissingCredentials() { + return array( + array(NULL, NULL), + array(NULL, ''), + array('', NULL), + array('', ''), + ); + } + + /** + * Tests the authenticate method with no account returned. + * + * @covers ::authenticate() + */ + public function testAuthenticateWithNoAccountReturned() { + $this->userStorage->expects($this->once()) + ->method('loadByProperties') + ->with(array('name' => $this->username)) + ->will($this->returnValue(array())); + + $this->assertFalse($this->userAuth->authenticate($this->username, $this->password)); + } + + /** + * Tests the authenticate method with an incorrect password. + * + * @covers ::authenticate() + */ + public function testAuthenticateWithIncorrectPassword() { + $this->userStorage->expects($this->once()) + ->method('loadByProperties') + ->with(array('name' => $this->username)) + ->will($this->returnValue(array($this->testUser))); + + $this->passwordService->expects($this->once()) + ->method('check') + ->with($this->password, $this->testUser) + ->will($this->returnValue(FALSE)); + + $this->assertFalse($this->userAuth->authenticate($this->username, $this->password)); + } + + /** + * Tests the authenticate method with a correct password. + * + * @covers ::authenticate() + */ + public function testAuthenticateWithCorrectPassword() { + $this->testUser->expects($this->once()) + ->method('id') + ->will($this->returnValue(1)); + + $this->userStorage->expects($this->once()) + ->method('loadByProperties') + ->with(array('name' => $this->username)) + ->will($this->returnValue(array($this->testUser))); + + $this->passwordService->expects($this->once()) + ->method('check') + ->with($this->password, $this->testUser) + ->will($this->returnValue(TRUE)); + + $this->assertsame(1, $this->userAuth->authenticate($this->username, $this->password)); + } + + /** + * Tests the authenticate method with a correct password and new password hash. + * + * @covers ::authenticate() + */ + public function testAuthenticateWithCorrectPasswordAndNewPasswordHash() { + $this->testUser->expects($this->once()) + ->method('id') + ->will($this->returnValue(1)); + $this->testUser->expects($this->once()) + ->method('setPassword') + ->with($this->password); + $this->testUser->expects($this->once()) + ->method('save'); + + $this->userStorage->expects($this->once()) + ->method('loadByProperties') + ->with(array('name' => $this->username)) + ->will($this->returnValue(array($this->testUser))); + + $this->passwordService->expects($this->once()) + ->method('check') + ->with($this->password, $this->testUser) + ->will($this->returnValue(TRUE)); + $this->passwordService->expects($this->once()) + ->method('userNeedsNewHash') + ->with($this->testUser) + ->will($this->returnValue(TRUE)); + + $this->assertsame(1, $this->userAuth->authenticate($this->username, $this->password)); + } + +} diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 8c83ae591007250b8ee19af3595c95a9b3d0bf2f..ab9a163e86572174067ce2cca49d4f80321275ac 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -796,26 +796,12 @@ function user_admin_paths() { * A plain-text password, such as trimmed text from form values. * @return * The user's uid on success, or FALSE on failure to authenticate. + * + * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. + * Use \Drupal\user\UserAuth::authenticate() instead. */ function user_authenticate($name, $password) { - $uid = FALSE; - if (!empty($name) && !empty($password)) { - $account = user_load_by_name($name); - if ($account) { - $password_hasher = \Drupal::service('password'); - if ($password_hasher->check($password, $account)) { - // Successful authentication. - $uid = $account->id(); - - // Update user to new password scheme if needed. - if ($password_hasher->userNeedsNewHash($account)) { - $account->setPassword($password); - $account->save(); - } - } - } - } - return $uid; + return \Drupal::service('user.auth')->authenticate($name, $password); } /** diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml index 23d870699f359788a03fe8f78a8cb0e121bf400a..36d1e79a6e528af2f7adbdf12c8ba6e17364209d 100644 --- a/core/modules/user/user.services.yml +++ b/core/modules/user/user.services.yml @@ -33,3 +33,6 @@ services: user.permissions_hash: class: Drupal\user\PermissionsHash arguments: ['@private_key', '@cache.cache'] + user.auth: + class: Drupal\user\UserAuth + arguments: ['@entity.manager', '@password']