diff --git a/docs/en/authenticators.rst b/docs/en/authenticators.rst index dd4f5d76..36b6bca0 100644 --- a/docs/en/authenticators.rst +++ b/docs/en/authenticators.rst @@ -139,8 +139,8 @@ Add the following to your ``Application`` class:: { $service = new AuthenticationService(); // ... - $service->loadIdentifier('Authentication.JwtSubject'); $service->loadAuthenticator('Authentication.Jwt', [ + 'identifier' => 'Authentication.JwtSubject', 'secretKey' => file_get_contents(CONFIG . '/jwt.key'), 'algorithm' => 'RS256', 'returnPayload' => false @@ -180,7 +180,6 @@ Using a JWKS fetched from an external JWKS endpoint is supported as well:: { $service = new AuthenticationService(); // ... - $service->loadIdentifier('Authentication.JwtSubject'); $jwksUrl = 'https://appleid.apple.com/auth/keys'; @@ -193,6 +192,7 @@ Using a JWKS fetched from an external JWKS endpoint is supported as well:: }); $service->loadAuthenticator('Authentication.Jwt', [ + 'identifier' => 'Authentication.JwtSubject', 'jwks' => $jsonWebKeySet, 'returnPayload' => false ]); @@ -335,14 +335,18 @@ authentication cookie is **also destroyed**. An example configuration would be:: // Put form authentication first so that users can re-login via // the login form if necessary. $service->loadAuthenticator('Authentication.Form', [ + 'identifier' => 'Authentication.Password', 'fields' => $fields, 'loginUrl' => '/users/login', ]); // Then use sessions if they are active. - $service->loadAuthenticator('Authentication.Session'); + $service->loadAuthenticator('Authentication.Session', [ + 'identifier' => 'Authentication.Password', + ]); // If the user is on the login page, check for a cookie as well. $service->loadAuthenticator('Authentication.Cookie', [ + 'identifier' => 'Authentication.Password', 'fields' => $fields, 'loginUrl' => '/users/login', ]); @@ -366,12 +370,15 @@ and similar SAML 1.1 implementations. An example configuration is:: // Configure a token identifier that maps `USER_ID` to the // username column - $service->loadIdentifier('Authentication.Token', [ - 'tokenField' => 'username', - 'dataField' => 'USER_NAME', - ]); + $identifier = [ + 'Authentication.Token' => [ + 'tokenField' => 'username', + 'dataField' => 'USER_NAME', + ], + ]; $service->loadAuthenticator('Authentication.Environment', [ + 'identifier' => $identifier, 'loginUrl' => '/sso', 'fields' => [ // Choose which environment variables exposed by your @@ -477,19 +484,26 @@ authenticators must send specific challenge headers in the response:: // Instantiate the service $service = new AuthenticationService(); - // Load identifiers - $service->loadIdentifier('Authentication.Password', [ - 'fields' => [ - 'username' => 'email', - 'password' => 'password' - ] - ]); - $service->loadIdentifier('Authentication.Token'); + // Define identifiers + $passwordIdentifier = [ + 'Authentication.Password' => [ + 'fields' => [ + 'username' => 'email', + 'password' => 'password' + ] + ], + ]; // Load the authenticators leaving Basic as the last one. - $service->loadAuthenticator('Authentication.Session'); - $service->loadAuthenticator('Authentication.Form'); - $service->loadAuthenticator('Authentication.HttpBasic'); + $service->loadAuthenticator('Authentication.Session', [ + 'identifier' => $passwordIdentifier, + ]); + $service->loadAuthenticator('Authentication.Form', [ + 'identifier' => $passwordIdentifier, + ]); + $service->loadAuthenticator('Authentication.HttpBasic', [ + 'identifier' => 'Authentication.Token', + ]); If you want to combine ``HttpBasic`` or ``HttpDigest`` with other authenticators, be aware that these authenticators will abort the request and diff --git a/docs/en/identifiers.rst b/docs/en/identifiers.rst index 86a0e179..5c673fa9 100644 --- a/docs/en/identifiers.rst +++ b/docs/en/identifiers.rst @@ -6,27 +6,28 @@ that was extracted from the request by the authenticators. Identifiers can take options in the ``loadIdentifier`` method. A holistic example of using the Password Identifier looks like:: - $service->loadIdentifier('Authentication.Password', [ - 'fields' => [ - 'username' => 'email', - 'password' => 'passwd', - ], - 'resolver' => [ - 'className' => 'Authentication.Orm', - 'userModel' => 'Users', - 'finder' => 'active', // default: 'all' - ], - 'passwordHasher' => [ - 'className' => 'Authentication.Fallback', - 'hashers' => [ - 'Authentication.Default', - [ - 'className' => 'Authentication.Legacy', - 'hashType' => 'md5', - ], + $identifier = [ + 'Authentication.Password' => [ + 'fields' => [ + 'username' => 'email', + 'password' => 'passwd', ], - ], - ]); + 'resolver' => [ + 'className' => 'Authentication.Orm', + 'userModel' => 'Users', + 'finder' => 'active', // default: 'all' + ], + 'passwordHasher' => [ + 'className' => 'Authentication.Fallback', + 'hashers' => [ + 'Authentication.Default' => [ + 'className' => 'Authentication.Legacy', + 'hashType' => 'md5', + ], + ], + ], + ], + ]; Password ======== @@ -60,7 +61,7 @@ Configuration options: - **resolver**: The identity resolver. Default is ``Authentication.Orm`` which uses CakePHP ORM. - **hashAlgorithm**: The algorithm used to hash the incoming token - with before compairing it to the ``tokenField``. Recommended value is + with before comparing it to the ``tokenField``. Recommended value is ``sha256``. Default is ``null``. JWT Subject @@ -119,36 +120,39 @@ or an ``Authentication\Authenticator\Result`` if you want to forward error messages:: // A simple callback identifier - $authenticationService->loadIdentifier('Authentication.Callback', [ - 'callback' => function($data) { - // do identifier logic + $identifier = [ + 'Authentication.Callback' => [ + 'callback' => function($data) { + // do identifier logic - // Return an array of the identified user or null for failure. - if ($result) { - return $result; - } + // Return an array of the identified user or null for failure. + if ($result) { + return $result; + } - return null; - }, - ]); + return null; + }, + ] + ]; // Using a result object to return error messages. - $authenticationService->loadIdentifier('Authentication.Callback', [ - 'callback' => function($data) { - // do identifier logic - - if ($result) { - return new Result($result, Result::SUCCESS); - } - - return new Result( - null, - Result::FAILURE_OTHER, - ['message' => 'Removed user.'] - ); - }, - ]); - + $identifier = [ + 'Authentication.Callback' => [ + 'callback' => function($data) { + // do identifier logic + + if ($result) { + return new Result($result, Result::SUCCESS); + } + + return new Result( + null, + Result::FAILURE_OTHER, + ['message' => 'Removed user.'] + ); + }, + ]; + ]; Identity resolvers ================== @@ -183,17 +187,27 @@ reside under ``App\Identifier\Resolver`` namespace. Resolver can be configured using ``resolver`` config option:: - $service->loadIdentifier('Authentication.Password', [ - 'resolver' => [ - // can be a full class name: \Some\Other\Custom\Resolver::class - 'className' => 'MyResolver', - // Pass additional options to the resolver constructor. - 'option' => 'value', - ], - ]); + $identifier = [ + 'Authentication.Password' => [ + 'resolver' => [ + // can be a full class name: \Some\Other\Custom\Resolver::class + 'className' => 'MyResolver', + // Pass additional options to the resolver constructor. + 'option' => 'value', + ], + ]; + ]; Or injected using a setter:: $resolver = new \App\Identifier\Resolver\CustomResolver(); $identifier = $service->loadIdentifier('Authentication.Password'); $identifier->setResolver($resolver); + +As of 3.3.0, you should pass the constructed resolver into the identifier:: + + $resolver = new \App\Identifier\Resolver\CustomResolver(); + $identifier = [ + 'Authentication.Password' => [ + 'resolver' => $resolver; + ]; diff --git a/docs/en/index.rst b/docs/en/index.rst index dd65787c..3339df70 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -94,13 +94,23 @@ define the ``AuthenticationService`` it wants to use. Add the following method t 'queryParam' => 'redirect', ]); + // Define identifiers $fields = [ AbstractIdentifier::CREDENTIAL_USERNAME => 'email', AbstractIdentifier::CREDENTIAL_PASSWORD => 'password' ]; + $passwordIdentifier = [ + 'Authentication.Password' => [ + 'fields' => $fields, + ], + ]; + // Load the authenticators. Session should be first. - $service->loadAuthenticator('Authentication.Session'); + $service->loadAuthenticator('Authentication.Session', [ + 'identifier' => $passwordIdentifier, + ]); $service->loadAuthenticator('Authentication.Form', [ + 'identifier' => $passwordIdentifier, 'fields' => $fields, 'loginUrl' => Router::url([ 'prefix' => false, @@ -110,9 +120,6 @@ define the ``AuthenticationService`` it wants to use. Add the following method t ]), ]); - // Load identifiers - $service->loadIdentifier('Authentication.Password', compact('fields')); - return $service; } diff --git a/docs/en/middleware.rst b/docs/en/middleware.rst index 3844e60b..2e8cb70f 100644 --- a/docs/en/middleware.rst +++ b/docs/en/middleware.rst @@ -42,18 +42,21 @@ inspecting the request object you can configure authentication appropriately:: $service = new AuthenticationService(); if (strpos($path, '/api') === 0) { // Accept API tokens only - $service->loadAuthenticator('Authentication.Token'); - $service->loadIdentifier('Authentication.Token'); + $service->loadAuthenticator('Authentication.Token', [ + 'identifier' => 'Authentication.Token', + ]); return $service; } // Web authentication // Support sessions and form login. - $service->loadAuthenticator('Authentication.Session'); - $service->loadAuthenticator('Authentication.Form'); - - $service->loadIdentifier('Authentication.Password'); + $service->loadAuthenticator('Authentication.Session', [ + 'identifier' => 'Authentication.Password', + ]); + $service->loadAuthenticator('Authentication.Form', [ + 'identifier' => 'Authentication.Password', + ]); return $service; } diff --git a/docs/en/migration-from-the-authcomponent.rst b/docs/en/migration-from-the-authcomponent.rst index 26b6276b..5d75c9fc 100644 --- a/docs/en/migration-from-the-authcomponent.rst +++ b/docs/en/migration-from-the-authcomponent.rst @@ -134,35 +134,43 @@ You’ll now have to configure it this way:: // Instantiate the service $service = new AuthenticationService(); - // Load identifiers - $service->loadIdentifier('Authentication.Password', [ - 'fields' => [ - 'username' => 'email', - 'password' => 'password', - ] - ]); - - // Load the authenticators - $service->loadAuthenticator('Authentication.Session'); - $service->loadAuthenticator('Authentication.Form'); + // Define identifier + $passwordIdentifier = [ + 'Authentication.Password' => [ + 'fields' => [ + 'username' => 'email', + 'password' => 'password' + ] + ], + ]; + + // Load the authenticators + $service->loadAuthenticator('Authentication.Session', [ + 'identifier' => $passwordIdentifier, + ]); + $service->loadAuthenticator('Authentication.Form', [ + 'identifier' => $passwordIdentifier, + ]); If you have customized the ``userModel`` you can use the following configuration:: - // Instantiate the service - $service = new AuthenticationService(); - - // Load identifiers - $service->loadIdentifier('Authentication.Password', [ - 'resolver' => [ - 'className' => 'Authentication.Orm', - 'userModel' => 'Employees', - ], - 'fields' => [ - 'username' => 'email', - 'password' => 'password', - ] - ]); + // Instantiate the service + $service = new AuthenticationService(); + + // Define identifier + $passwordIdentifier = [ + 'Authentication.Password' => [ + 'resolver' => [ + 'className' => 'Authentication.Orm', + 'userModel' => 'Employees', + ], + 'fields' => [ + 'username' => 'email', + 'password' => 'password' + ] + ], + ]; While there is a bit more code than before, you have more flexibility in how your authentication is handled. diff --git a/docs/en/password-hashers.rst b/docs/en/password-hashers.rst index e73212ac..e5f44aa9 100644 --- a/docs/en/password-hashers.rst +++ b/docs/en/password-hashers.rst @@ -41,20 +41,21 @@ algorithm to another, this is achieved through the Legacy password to the Default bcrypt hasher, you can configure the fallback hasher as follows:: - $service->loadIdentifier('Authentication.Password', [ - // Other config options - 'passwordHasher' => [ - 'className' => 'Authentication.Fallback', - 'hashers' => [ - 'Authentication.Default', - [ - 'className' => 'Authentication.Legacy', - 'hashType' => 'md5', - 'salt' => false // turn off default usage of salt - ], - ] - ] - ]); + $passwordIdentifier = [ + 'Authentication.Password' => [ + // Other config options + 'passwordHasher' => [ + 'className' => 'Authentication.Fallback', + 'hashers' => [ + 'Authentication.Default' => [ + 'className' => 'Authentication.Legacy', + 'hashType' => 'md5', + 'salt' => false, // turn off default usage of salt + ], + ] + ] + ], + ]; Then in your login action you can use the authentication service to access the ``Password`` identifier and check if the current user’s @@ -72,7 +73,7 @@ password needs to be upgraded:: // Rehash happens on save. $user = $this->Users->get($authentication->getIdentity()->getIdentifier()); $user->password = $this->request->getData('password'); - $this->Users->save($user); + $this->Users->saveOrFail($user); } // Redirect or display a template. diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index da88ee40..ff99acac 100644 --- a/src/AuthenticationService.php +++ b/src/AuthenticationService.php @@ -30,6 +30,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; +use function Cake\Core\deprecationWarning; /** * Authentication Service @@ -164,9 +165,15 @@ public function loadAuthenticator(string $name, array $config = []): Authenticat * @param string $name Name or class name. * @param array $config Identifier configuration. * @return \Authentication\Identifier\IdentifierInterface Identifier instance + * @deprecated 3.3.0: loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator. */ public function loadIdentifier(string $name, array $config = []): IdentifierInterface { + deprecationWarning( + '3.3.0', + 'loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator.', + ); + return $this->identifiers()->load($name, $config); } @@ -280,7 +287,16 @@ public function getAuthenticationProvider(): ?AuthenticatorInterface */ public function getIdentificationProvider(): ?IdentifierInterface { - return $this->identifiers()->getIdentificationProvider(); + if ($this->_successfulAuthenticator === null) { + return null; + } + + $identifier = $this->_successfulAuthenticator->getIdentifier(); + if ($identifier instanceof IdentifierCollection) { + return $identifier->getIdentificationProvider(); + } + + return $identifier; } /** diff --git a/src/AuthenticationServiceInterface.php b/src/AuthenticationServiceInterface.php index e2aef27f..304147a0 100644 --- a/src/AuthenticationServiceInterface.php +++ b/src/AuthenticationServiceInterface.php @@ -39,6 +39,7 @@ public function loadAuthenticator(string $name, array $config = []): Authenticat * @param string $name Name or class name. * @param array $config Identifier configuration. * @return \Authentication\Identifier\IdentifierInterface + * @deprecated 3.3.0: loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator. */ public function loadIdentifier(string $name, array $config = []): IdentifierInterface; diff --git a/src/Authenticator/AuthenticatorCollection.php b/src/Authenticator/AuthenticatorCollection.php index 0a5f8f06..8036eee3 100644 --- a/src/Authenticator/AuthenticatorCollection.php +++ b/src/Authenticator/AuthenticatorCollection.php @@ -20,6 +20,7 @@ use Authentication\Identifier\IdentifierCollection; use Cake\Core\App; use RuntimeException; +use function Cake\Core\deprecationWarning; /** * @extends \Authentication\AbstractCollection<\Authentication\Authenticator\AuthenticatorInterface> @@ -42,6 +43,12 @@ class AuthenticatorCollection extends AbstractCollection public function __construct(IdentifierCollection $identifiers, array $config = []) { $this->_identifiers = $identifiers; + if ($identifiers->count() > 0) { + deprecationWarning( + '3.3.0', + 'loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator.', + ); + } parent::__construct($config); } @@ -58,6 +65,15 @@ public function __construct(IdentifierCollection $identifiers, array $config = [ protected function _create(object|string $class, string $alias, array $config): AuthenticatorInterface { if (is_string($class)) { + if (!empty($config['identifier'])) { + $this->_identifiers = new IdentifierCollection((array)$config['identifier']); + } else { + deprecationWarning( + '3.3.0', + 'loadIdentifier() usage is deprecated. Directly pass `\'identifier\'` config to Authenticator.', + ); + } + return new $class($this->_identifiers, $config); } @@ -78,7 +94,7 @@ protected function _resolveClassName(string $class): ?string /** * @param string $class Missing class. - * @param string $plugin Class plugin. + * @param string|null $plugin Class plugin. * @return void * @throws \RuntimeException */ diff --git a/src/Authenticator/AuthenticatorInterface.php b/src/Authenticator/AuthenticatorInterface.php index ec6e626e..603ddf7e 100644 --- a/src/Authenticator/AuthenticatorInterface.php +++ b/src/Authenticator/AuthenticatorInterface.php @@ -18,6 +18,9 @@ use Psr\Http\Message\ServerRequestInterface; +/** + * @method \Authentication\Identifier\IdentifierInterface getIdentifier() + */ interface AuthenticatorInterface { /** diff --git a/src/Authenticator/SessionAuthenticator.php b/src/Authenticator/SessionAuthenticator.php index cad0b9b0..8b11f728 100644 --- a/src/Authenticator/SessionAuthenticator.php +++ b/src/Authenticator/SessionAuthenticator.php @@ -31,7 +31,7 @@ class SessionAuthenticator extends AbstractAuthenticator implements PersistenceI * Default config for this object. * - `fields` The fields to use to verify a user by. * - `sessionKey` Session key. - * - `identify` Whether or not to identify user data stored in a session. This is + * - `identify` Whether to identify user data stored in a session. This is * useful if you want to remotely end sessions that have a different password stored, * or if your identification logic needs additional conditions before a user can login. * diff --git a/tests/TestCase/AuthenticationServiceTest.php b/tests/TestCase/AuthenticationServiceTest.php index 4fa45df7..02921a63 100644 --- a/tests/TestCase/AuthenticationServiceTest.php +++ b/tests/TestCase/AuthenticationServiceTest.php @@ -45,7 +45,7 @@ class AuthenticationServiceTest extends TestCase * * @return void */ - public function testAuthenticate() + public function testAuthenticateDeprecated() { $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -73,6 +73,38 @@ public function testAuthenticate() $this->assertInstanceOf(PasswordIdentifier::class, $identifier); } + /** + * testAuthenticate + * + * @return void + */ + public function testAuthenticate() + { + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/testpath'], + [], + ['username' => 'mariano', 'password' => 'password'], + ); + + $service = new AuthenticationService([ + 'authenticators' => [ + 'Authentication.Form' => [ + 'identifier' => 'Authentication.Password', + ], + ], + ]); + + $result = $service->authenticate($request); + $this->assertInstanceOf(Result::class, $result); + $this->assertTrue($result->isValid()); + + $result = $service->getAuthenticationProvider(); + $this->assertInstanceOf(FormAuthenticator::class, $result); + + $identifier = $service->getIdentificationProvider(); + $this->assertInstanceOf(PasswordIdentifier::class, $identifier); + } + /** * test authenticate() with a challenger authenticator * diff --git a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php index 6362cd97..bd22d5ea 100644 --- a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php @@ -20,6 +20,7 @@ use Authentication\Authenticator\Result; use Authentication\Authenticator\SessionAuthenticator; use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\PasswordIdentifier; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; @@ -91,6 +92,90 @@ public function testAuthenticateSuccess() $this->assertSame(Result::SUCCESS, $result->getStatus()); } + /** + * Test authentication + * + * @return void + */ + public function testAuthenticateSuccessWithoutCollection() + { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); + + $this->sessionMock->expects($this->once()) + ->method('read') + ->with('Auth') + ->willReturn([ + 'username' => 'mariano', + 'password' => 'password', + ]); + + $request = $request->withAttribute('session', $this->sessionMock); + + $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ + 'identifier' => 'Authentication.Password', + ]); + $result = $authenticator->authenticate($request); + + $this->assertInstanceOf(Result::class, $result); + $this->assertSame(Result::SUCCESS, $result->getStatus()); + } + + /** + * Test authentication + * + * @return void + */ + public function testAuthenticateSuccessWithoutCollectionButObject() + { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); + + $this->sessionMock->expects($this->once()) + ->method('read') + ->with('Auth') + ->willReturn([ + 'username' => 'mariano', + 'password' => 'password', + ]); + + $request = $request->withAttribute('session', $this->sessionMock); + + $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ + 'identifier' => new PasswordIdentifier(), + ]); + $result = $authenticator->authenticate($request); + + $this->assertInstanceOf(Result::class, $result); + $this->assertSame(Result::SUCCESS, $result->getStatus()); + } + + /** + * Test authentication + * + * @return void + */ + public function testAuthenticateSuccessWithDirectCollection() + { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); + + $this->sessionMock->expects($this->once()) + ->method('read') + ->with('Auth') + ->willReturn([ + 'username' => 'mariano', + 'password' => 'password', + ]); + + $request = $request->withAttribute('session', $this->sessionMock); + + $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ + 'identifier' => new IdentifierCollection(['Authentication.Password']), + ]); + $result = $authenticator->authenticate($request); + + $this->assertInstanceOf(Result::class, $result); + $this->assertSame(Result::SUCCESS, $result->getStatus()); + } + /** * Test authentication *