From 7f8277e40d21bd32394ff796301d931604c1dbc1 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 6 Nov 2025 05:44:28 +0100 Subject: [PATCH 01/30] Remove deprecations, fix up MultiChecker. --- src/AuthenticationService.php | 57 +---- src/AuthenticationServiceInterface.php | 11 - src/Authenticator/AbstractAuthenticator.php | 20 +- src/Authenticator/AuthenticatorCollection.php | 34 +-- src/Authenticator/CookieAuthenticator.php | 19 +- .../EnvironmentAuthenticator.php | 1 + src/Authenticator/FormAuthenticator.php | 19 +- src/Authenticator/HttpBasicAuthenticator.php | 23 +- src/Authenticator/HttpDigestAuthenticator.php | 11 +- src/Authenticator/JwtAuthenticator.php | 11 +- .../PrimaryKeySessionAuthenticator.php | 5 +- src/Authenticator/SessionAuthenticator.php | 5 +- src/Authenticator/TokenAuthenticator.php | 11 +- src/Identifier/AbstractIdentifier.php | 4 - src/Identifier/IdentifierCollection.php | 132 ----------- src/Identifier/IdentifierFactory.php | 66 ++++++ src/Identifier/IdentifierInterface.php | 2 +- src/Identifier/LdapIdentifier.php | 4 + src/Identifier/PasswordIdentifier.php | 4 + src/UrlChecker/CakeRouterUrlChecker.php | 2 +- src/UrlChecker/DefaultUrlChecker.php | 2 +- src/UrlChecker/MultiUrlChecker.php | 100 +++++++++ src/UrlChecker/UrlCheckerTrait.php | 15 +- tests/TestCase/AuthenticationServiceTest.php | 33 --- .../AuthenticatorCollectionTest.php | 19 +- .../Authenticator/CookieAuthenticatorTest.php | 76 +++---- .../EnvironmentAuthenticatorTest.php | 81 ++++--- .../Authenticator/FormAuthenticatorTest.php | 207 ++++-------------- .../HttpBasicAuthenticatorTest.php | 14 +- .../HttpDigestAuthenticatorTest.php | 14 +- .../Authenticator/JwtAuthenticatorTest.php | 28 +-- .../PrimaryKeySessionAuthenticatorTest.php | 63 +++--- .../SessionAuthenticatorTest.php | 40 ++-- .../Authenticator/TokenAuthenticatorTest.php | 34 ++- .../Identifier/IdentifierCollectionTest.php | 121 ---------- 35 files changed, 460 insertions(+), 828 deletions(-) delete mode 100644 src/Identifier/IdentifierCollection.php create mode 100644 src/Identifier/IdentifierFactory.php create mode 100644 src/UrlChecker/MultiUrlChecker.php delete mode 100644 tests/TestCase/Identifier/IdentifierCollectionTest.php diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index 71c498c7..9b84a5a9 100644 --- a/src/AuthenticationService.php +++ b/src/AuthenticationService.php @@ -23,7 +23,6 @@ use Authentication\Authenticator\PersistenceInterface; use Authentication\Authenticator\ResultInterface; use Authentication\Authenticator\StatelessInterface; -use Authentication\Identifier\IdentifierCollection; use Authentication\Identifier\IdentifierInterface; use Cake\Core\InstanceConfigTrait; use Cake\Routing\Router; @@ -31,7 +30,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; -use function Cake\Core\deprecationWarning; /** * Authentication Service @@ -47,13 +45,6 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona */ protected ?AuthenticatorCollection $_authenticators = null; - /** - * Identifier collection - * - * @var \Authentication\Identifier\IdentifierCollection|null - */ - protected ?IdentifierCollection $_identifiers = null; - /** * Authenticator that successfully authenticated the identity. * @@ -73,10 +64,7 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona * * - `authenticators` - An array of authentication objects to use for authenticating users. * You can configure multiple adapters and they will be checked sequentially - * when users are identified. - * - `identifiers` - An array of identifiers. The identifiers are constructed by the service - * and then passed to the authenticators that will pass the credentials to them and get the - * user data. + * when users are identified. Each authenticator config can specify its own `identifier`. * - `identityClass` - The class name of identity or a callable identity builder. * - `identityAttribute` - The request attribute used to store the identity. Default to `identity`. * - `unauthenticatedRedirect` - The URL to redirect unauthenticated errors to. See @@ -100,7 +88,6 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona */ protected array $_defaultConfig = [ 'authenticators' => [], - 'identifiers' => [], 'identityClass' => Identity::class, 'identityAttribute' => 'identity', 'queryParam' => null, @@ -117,20 +104,6 @@ public function __construct(array $config = []) $this->setConfig($config); } - /** - * Access the identifier collection - * - * @return \Authentication\Identifier\IdentifierCollection - */ - public function identifiers(): IdentifierCollection - { - if ($this->_identifiers === null) { - $this->_identifiers = new IdentifierCollection($this->getConfig('identifiers')); - } - - return $this->_identifiers; - } - /** * Access the authenticator collection * @@ -139,9 +112,8 @@ public function identifiers(): IdentifierCollection public function authenticators(): AuthenticatorCollection { if ($this->_authenticators === null) { - $identifiers = $this->identifiers(); $authenticators = $this->getConfig('authenticators'); - $this->_authenticators = new AuthenticatorCollection($identifiers, $authenticators); + $this->_authenticators = new AuthenticatorCollection($authenticators); } return $this->_authenticators; @@ -159,24 +131,6 @@ public function loadAuthenticator(string $name, array $config = []): Authenticat return $this->authenticators()->load($name, $config); } - /** - * Loads an identifier. - * - * @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\'` config to the Authenticator.', - ); - - return $this->identifiers()->load($name, $config); - } - /** * {@inheritDoc} * @@ -291,12 +245,7 @@ public function getIdentificationProvider(): ?IdentifierInterface return null; } - $identifier = $this->_successfulAuthenticator->getIdentifier(); - if ($identifier instanceof IdentifierCollection) { - return $identifier->getIdentificationProvider(); - } - - return $identifier; + return $this->_successfulAuthenticator->getIdentifier(); } /** diff --git a/src/AuthenticationServiceInterface.php b/src/AuthenticationServiceInterface.php index ade15c28..570253ae 100644 --- a/src/AuthenticationServiceInterface.php +++ b/src/AuthenticationServiceInterface.php @@ -19,7 +19,6 @@ use Authentication\Authenticator\AuthenticatorInterface; use Authentication\Authenticator\PersistenceInterface; use Authentication\Authenticator\ResultInterface; -use Authentication\Identifier\IdentifierInterface; use Psr\Http\Message\ServerRequestInterface; interface AuthenticationServiceInterface extends PersistenceInterface @@ -33,16 +32,6 @@ interface AuthenticationServiceInterface extends PersistenceInterface */ public function loadAuthenticator(string $name, array $config = []): AuthenticatorInterface; - /** - * Loads an identifier. - * - * @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; - /** * Authenticate the request against the configured authentication adapters. * diff --git a/src/Authenticator/AbstractAuthenticator.php b/src/Authenticator/AbstractAuthenticator.php index ad53b741..9d54a50c 100644 --- a/src/Authenticator/AbstractAuthenticator.php +++ b/src/Authenticator/AbstractAuthenticator.php @@ -16,8 +16,8 @@ */ namespace Authentication\Authenticator; -use Authentication\Identifier\AbstractIdentifier; use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Cake\Core\InstanceConfigTrait; use Psr\Http\Message\ServerRequestInterface; @@ -33,25 +33,25 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface */ protected array $_defaultConfig = [ 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', - AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + PasswordIdentifier::CREDENTIAL_USERNAME => 'username', + PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', ], ]; /** - * Identifier or identifiers collection. + * Identifier instance. * - * @var \Authentication\Identifier\IdentifierInterface + * @var \Authentication\Identifier\IdentifierInterface|null */ - protected IdentifierInterface $_identifier; + protected ?IdentifierInterface $_identifier = null; /** * Constructor * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { $this->_identifier = $identifier; $this->setConfig($config); @@ -60,9 +60,9 @@ public function __construct(IdentifierInterface $identifier, array $config = []) /** * Gets the identifier. * - * @return \Authentication\Identifier\IdentifierInterface + * @return \Authentication\Identifier\IdentifierInterface|null */ - public function getIdentifier(): IdentifierInterface + public function getIdentifier(): ?IdentifierInterface { return $this->_identifier; } diff --git a/src/Authenticator/AuthenticatorCollection.php b/src/Authenticator/AuthenticatorCollection.php index 5e4249d0..461a6600 100644 --- a/src/Authenticator/AuthenticatorCollection.php +++ b/src/Authenticator/AuthenticatorCollection.php @@ -17,42 +17,15 @@ namespace Authentication\Authenticator; use Authentication\AbstractCollection; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Cake\Core\App; use RuntimeException; -use function Cake\Core\deprecationWarning; /** * @extends \Authentication\AbstractCollection<\Authentication\Authenticator\AuthenticatorInterface> */ class AuthenticatorCollection extends AbstractCollection { - /** - * Identifier collection. - * - * @var \Authentication\Identifier\IdentifierCollection - */ - protected IdentifierCollection $_identifiers; - - /** - * Constructor. - * - * @param \Authentication\Identifier\IdentifierCollection $identifiers Identifiers collection. - * @param array $config Config array. - */ - 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\'` config to the Authenticator.', - ); - } - - parent::__construct($config); - } - /** * Creates authenticator instance. * @@ -65,11 +38,12 @@ public function __construct(IdentifierCollection $identifiers, array $config = [ protected function _create(object|string $class, string $alias, array $config): AuthenticatorInterface { if (is_string($class)) { + $identifier = null; if (!empty($config['identifier'])) { - $this->_identifiers = new IdentifierCollection((array)$config['identifier']); + $identifier = IdentifierFactory::create($config['identifier']); } - return new $class($this->_identifiers, $config); + return new $class($identifier, $config); } return $class; diff --git a/src/Authenticator/CookieAuthenticator.php b/src/Authenticator/CookieAuthenticator.php index 8e2836ef..ea218cdf 100644 --- a/src/Authenticator/CookieAuthenticator.php +++ b/src/Authenticator/CookieAuthenticator.php @@ -17,9 +17,9 @@ namespace Authentication\Authenticator; use ArrayAccess; -use Authentication\Identifier\AbstractIdentifier; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Authentication\PasswordHasher\PasswordHasherTrait; use Authentication\UrlChecker\UrlCheckerTrait; use Cake\Http\Cookie\Cookie; @@ -47,8 +47,8 @@ class CookieAuthenticator extends AbstractAuthenticator implements PersistenceIn 'urlChecker' => 'Authentication.Default', 'rememberMeField' => 'remember_me', 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', - AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + PasswordIdentifier::CREDENTIAL_USERNAME => 'username', + PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', ], 'cookie' => [ 'name' => 'CookieAuth', @@ -60,21 +60,19 @@ class CookieAuthenticator extends AbstractAuthenticator implements PersistenceIn /** * Constructor * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { // If no identifier is configured, set up a default Password identifier - if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) { + if ($identifier === null) { // Pass the authenticator's fields configuration to the identifier $identifierConfig = []; if (isset($config['fields'])) { $identifierConfig['fields'] = $config['fields']; } - $identifier = new IdentifierCollection([ - 'Authentication.Password' => $identifierConfig, - ]); + $identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); } parent::__construct($identifier, $config); @@ -107,6 +105,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface [$username, $tokenHash] = $token; + assert($this->_identifier !== null); $identity = $this->_identifier->identify(compact('username')); if (!$identity) { diff --git a/src/Authenticator/EnvironmentAuthenticator.php b/src/Authenticator/EnvironmentAuthenticator.php index 40959a00..3cb95393 100644 --- a/src/Authenticator/EnvironmentAuthenticator.php +++ b/src/Authenticator/EnvironmentAuthenticator.php @@ -153,6 +153,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface $data = array_merge($this->_getOptionalData($request), $data); + assert($this->_identifier !== null); $user = $this->_identifier->identify($data); if (!$user) { diff --git a/src/Authenticator/FormAuthenticator.php b/src/Authenticator/FormAuthenticator.php index f784c483..caca2d98 100644 --- a/src/Authenticator/FormAuthenticator.php +++ b/src/Authenticator/FormAuthenticator.php @@ -16,9 +16,9 @@ */ namespace Authentication\Authenticator; -use Authentication\Identifier\AbstractIdentifier; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Authentication\UrlChecker\UrlCheckerTrait; use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; @@ -44,29 +44,27 @@ class FormAuthenticator extends AbstractAuthenticator 'loginUrl' => null, 'urlChecker' => 'Authentication.Default', 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', - AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + PasswordIdentifier::CREDENTIAL_USERNAME => 'username', + PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', ], ]; /** * Constructor * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { // If no identifier is configured, set up a default Password identifier - if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) { + if ($identifier === null) { // Pass the authenticator's fields configuration to the identifier $identifierConfig = []; if (isset($config['fields'])) { $identifierConfig['fields'] = $config['fields']; } - $identifier = new IdentifierCollection([ - 'Authentication.Password' => $identifierConfig, - ]); + $identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); } parent::__construct($identifier, $config); @@ -161,6 +159,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface ]); } + assert($this->_identifier !== null); $user = $this->_identifier->identify($data); if (!$user) { diff --git a/src/Authenticator/HttpBasicAuthenticator.php b/src/Authenticator/HttpBasicAuthenticator.php index 715e002f..1a5ea6a9 100644 --- a/src/Authenticator/HttpBasicAuthenticator.php +++ b/src/Authenticator/HttpBasicAuthenticator.php @@ -15,9 +15,9 @@ */ namespace Authentication\Authenticator; -use Authentication\Identifier\AbstractIdentifier; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Psr\Http\Message\ServerRequestInterface; /** @@ -37,8 +37,8 @@ class HttpBasicAuthenticator extends AbstractAuthenticator implements StatelessI */ protected array $_defaultConfig = [ 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', - AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + PasswordIdentifier::CREDENTIAL_USERNAME => 'username', + PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', ], 'skipChallenge' => false, ]; @@ -46,21 +46,19 @@ class HttpBasicAuthenticator extends AbstractAuthenticator implements StatelessI /** * Constructor * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { // If no identifier is configured, set up a default Password identifier - if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) { + if ($identifier === null) { // Pass the authenticator's fields configuration to the identifier $identifierConfig = []; if (isset($config['fields'])) { $identifierConfig['fields'] = $config['fields']; } - $identifier = new IdentifierCollection([ - 'Authentication.Password' => $identifierConfig, - ]); + $identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); } parent::__construct($identifier, $config); @@ -83,9 +81,10 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); } + assert($this->_identifier !== null); $user = $this->_identifier->identify([ - AbstractIdentifier::CREDENTIAL_USERNAME => $username, - AbstractIdentifier::CREDENTIAL_PASSWORD => $password, + PasswordIdentifier::CREDENTIAL_USERNAME => $username, + PasswordIdentifier::CREDENTIAL_PASSWORD => $password, ]); if ($user === null) { diff --git a/src/Authenticator/HttpDigestAuthenticator.php b/src/Authenticator/HttpDigestAuthenticator.php index 0647b8ea..a6153959 100644 --- a/src/Authenticator/HttpDigestAuthenticator.php +++ b/src/Authenticator/HttpDigestAuthenticator.php @@ -15,8 +15,8 @@ */ namespace Authentication\Authenticator; -use Authentication\Identifier\AbstractIdentifier; use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Cake\Utility\Security; use InvalidArgumentException; use Psr\Http\Message\ServerRequestInterface; @@ -55,10 +55,10 @@ class HttpDigestAuthenticator extends HttpBasicAuthenticator * - `opaque` A string that must be returned unchanged by clients. * Defaults to `md5($config['realm'])` * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier instance. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { $secret = ''; if (class_exists(Security::class)) { @@ -94,8 +94,9 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); } + assert($this->_identifier !== null); $user = $this->_identifier->identify([ - AbstractIdentifier::CREDENTIAL_USERNAME => $digest['username'], + PasswordIdentifier::CREDENTIAL_USERNAME => $digest['username'], ]); if (!$user) { @@ -106,7 +107,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); } - $field = $this->_config['fields'][AbstractIdentifier::CREDENTIAL_PASSWORD]; + $field = $this->_config['fields'][PasswordIdentifier::CREDENTIAL_PASSWORD]; $password = $user[$field]; $server = $request->getServerParams(); diff --git a/src/Authenticator/JwtAuthenticator.php b/src/Authenticator/JwtAuthenticator.php index ff1acd89..ad2ed150 100644 --- a/src/Authenticator/JwtAuthenticator.php +++ b/src/Authenticator/JwtAuthenticator.php @@ -17,7 +17,7 @@ namespace Authentication\Authenticator; use ArrayObject; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\IdentifierInterface; use Authentication\Identifier\JwtSubjectIdentifier; use Cake\Utility\Security; @@ -55,14 +55,14 @@ class JwtAuthenticator extends TokenAuthenticator /** * @inheritDoc */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { // Override parent's default - JWT should use JwtSubject identifier - if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) { - $identifier = new IdentifierCollection(['Authentication.JwtSubject']); + if ($identifier === null) { + $identifier = IdentifierFactory::create('Authentication.JwtSubject'); } - // Call TokenAuthenticator's constructor but skip its default + // Call AbstractAuthenticator's constructor directly to skip parent's default AbstractAuthenticator::__construct($identifier, $config); if (empty($this->_config['secretKey'])) { @@ -113,6 +113,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result($user, Result::SUCCESS); } + assert($this->_identifier !== null); $user = $this->_identifier->identify([ $subjectKey => $result[$subjectKey], ]); diff --git a/src/Authenticator/PrimaryKeySessionAuthenticator.php b/src/Authenticator/PrimaryKeySessionAuthenticator.php index 0de92fbd..3f379424 100644 --- a/src/Authenticator/PrimaryKeySessionAuthenticator.php +++ b/src/Authenticator/PrimaryKeySessionAuthenticator.php @@ -15,10 +15,10 @@ class PrimaryKeySessionAuthenticator extends SessionAuthenticator { /** - * @param \Authentication\Identifier\IdentifierInterface $identifier + * @param \Authentication\Identifier\IdentifierInterface|null $identifier * @param array $config */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { $config += [ 'identifierKey' => 'key', @@ -45,6 +45,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); } + assert($this->_identifier !== null); $user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]); if (!$user) { return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); diff --git a/src/Authenticator/SessionAuthenticator.php b/src/Authenticator/SessionAuthenticator.php index a6c98dea..88f8e0e0 100644 --- a/src/Authenticator/SessionAuthenticator.php +++ b/src/Authenticator/SessionAuthenticator.php @@ -17,7 +17,7 @@ use ArrayAccess; use ArrayObject; -use Authentication\Identifier\AbstractIdentifier; +use Authentication\Identifier\PasswordIdentifier; use Cake\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -39,7 +39,7 @@ class SessionAuthenticator extends AbstractAuthenticator implements PersistenceI */ protected array $_defaultConfig = [ 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', + PasswordIdentifier::CREDENTIAL_USERNAME => 'username', ], 'sessionKey' => 'Auth', 'impersonateSessionKey' => 'AuthImpersonate', @@ -69,6 +69,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface foreach ($this->getConfig('fields') as $key => $field) { $credentials[$key] = $user[$field]; } + assert($this->_identifier !== null); $user = $this->_identifier->identify($credentials); if (!$user) { diff --git a/src/Authenticator/TokenAuthenticator.php b/src/Authenticator/TokenAuthenticator.php index d02e26b5..217c0d99 100644 --- a/src/Authenticator/TokenAuthenticator.php +++ b/src/Authenticator/TokenAuthenticator.php @@ -16,7 +16,7 @@ */ namespace Authentication\Authenticator; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\IdentifierInterface; use Authentication\Identifier\TokenIdentifier; use Psr\Http\Message\ServerRequestInterface; @@ -40,14 +40,14 @@ class TokenAuthenticator extends AbstractAuthenticator implements StatelessInter /** * Constructor * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { // If no identifier is configured, set up a default Token identifier - if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) { - $identifier = new IdentifierCollection(['Authentication.Token']); + if ($identifier === null) { + $identifier = IdentifierFactory::create('Authentication.Token'); } parent::__construct($identifier, $config); @@ -142,6 +142,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); } + assert($this->_identifier !== null); $user = $this->_identifier->identify([ TokenIdentifier::CREDENTIAL_TOKEN => $token, ]); diff --git a/src/Identifier/AbstractIdentifier.php b/src/Identifier/AbstractIdentifier.php index e2e36e36..f8aa9c79 100644 --- a/src/Identifier/AbstractIdentifier.php +++ b/src/Identifier/AbstractIdentifier.php @@ -22,10 +22,6 @@ abstract class AbstractIdentifier implements IdentifierInterface { use InstanceConfigTrait; - public const CREDENTIAL_USERNAME = 'username'; - - public const CREDENTIAL_PASSWORD = 'password'; - /** * Default configuration * diff --git a/src/Identifier/IdentifierCollection.php b/src/Identifier/IdentifierCollection.php deleted file mode 100644 index 257b25d8..00000000 --- a/src/Identifier/IdentifierCollection.php +++ /dev/null @@ -1,132 +0,0 @@ - - */ -class IdentifierCollection extends AbstractCollection implements IdentifierInterface -{ - /** - * Errors - * - * @var array - */ - protected array $_errors = []; - - /** - * Identifier that successfully Identified the identity. - * - * @var \Authentication\Identifier\IdentifierInterface|null - */ - protected ?IdentifierInterface $_successfulIdentifier = null; - - /** - * Identifies an user or service by the passed credentials - * - * @param array $credentials Authentication credentials - * @return \ArrayAccess|array|null - */ - public function identify(array $credentials): ArrayAccess|array|null - { - /** @var \Authentication\Identifier\IdentifierInterface $identifier */ - foreach ($this->_loaded as $name => $identifier) { - $result = $identifier->identify($credentials); - if ($result) { - $this->_successfulIdentifier = $identifier; - - return $result; - } - - $errors = $identifier->getErrors(); - if ($errors) { - $this->_errors[$name] = $identifier->getErrors(); - } - } - - $this->_successfulIdentifier = null; - - return null; - } - - /** - * Creates identifier instance. - * - * @param \Authentication\Identifier\IdentifierInterface|class-string<\Authentication\Identifier\IdentifierInterface> $class Identifier class. - * @param string $alias Identifier alias. - * @param array $config Config array. - * @return \Authentication\Identifier\IdentifierInterface - * @throws \RuntimeException - */ - protected function _create(object|string $class, string $alias, array $config): IdentifierInterface - { - if (is_object($class)) { - return $class; - } - - return new $class($config); - } - - /** - * Get errors - * - * @return array - */ - public function getErrors(): array - { - return $this->_errors; - } - - /** - * Resolves identifier class name. - * - * @param string $class Class name to be resolved. - * @return class-string<\Authentication\Identifier\IdentifierInterface>|null - */ - protected function _resolveClassName(string $class): ?string - { - /** @var class-string<\Authentication\Identifier\IdentifierInterface>|null */ - return App::className($class, 'Identifier', 'Identifier'); - } - - /** - * @param string $class Missing class. - * @param string $plugin Class plugin. - * @return void - * @throws \RuntimeException - */ - protected function _throwMissingClassError(string $class, ?string $plugin): void - { - $message = sprintf('Identifier class `%s` was not found.', $class); - throw new RuntimeException($message); - } - - /** - * Gets the successful identifier instance if one was successful after calling identify. - * - * @return \Authentication\Identifier\IdentifierInterface|null - */ - public function getIdentificationProvider(): ?IdentifierInterface - { - return $this->_successfulIdentifier; - } -} diff --git a/src/Identifier/IdentifierFactory.php b/src/Identifier/IdentifierFactory.php new file mode 100644 index 00000000..83161dfc --- /dev/null +++ b/src/Identifier/IdentifierFactory.php @@ -0,0 +1,66 @@ +|string $config Identifier configuration. + * Can be a class name string, an instance, or an array with 'className' key. + * @param array $defaultConfig Default configuration to merge. + * @return \Authentication\Identifier\IdentifierInterface + * @throws \RuntimeException When the identifier class cannot be found or created. + */ + public static function create( + string|array|IdentifierInterface $config, + array $defaultConfig = [], + ): IdentifierInterface { + if ($config instanceof IdentifierInterface) { + return $config; + } + + if (is_string($config)) { + $className = $config; + $config = []; + } else { + $className = $config['className'] ?? ''; + unset($config['className']); + } + + if (empty($className)) { + throw new RuntimeException('Identifier configuration must specify a class name.'); + } + + $config += $defaultConfig; + + /** @var class-string<\Authentication\Identifier\IdentifierInterface>|null $class */ + $class = App::className($className, 'Identifier', 'Identifier'); + if ($class === null) { + throw new RuntimeException(sprintf('Identifier class `%s` was not found.', $className)); + } + + return new $class($config); + } +} diff --git a/src/Identifier/IdentifierInterface.php b/src/Identifier/IdentifierInterface.php index adf29c22..7883fb72 100644 --- a/src/Identifier/IdentifierInterface.php +++ b/src/Identifier/IdentifierInterface.php @@ -21,7 +21,7 @@ interface IdentifierInterface { /** - * Identifies an user or service by the passed credentials + * Identifies a user or service by the passed credentials * * @param array $credentials Authentication credentials * @return \ArrayAccess|array|null diff --git a/src/Identifier/LdapIdentifier.php b/src/Identifier/LdapIdentifier.php index 70398149..c80076d2 100644 --- a/src/Identifier/LdapIdentifier.php +++ b/src/Identifier/LdapIdentifier.php @@ -46,6 +46,10 @@ */ class LdapIdentifier extends AbstractIdentifier { + public const CREDENTIAL_USERNAME = 'username'; + + public const CREDENTIAL_PASSWORD = 'password'; + /** * Default configuration * diff --git a/src/Identifier/PasswordIdentifier.php b/src/Identifier/PasswordIdentifier.php index 45c39353..964f352e 100644 --- a/src/Identifier/PasswordIdentifier.php +++ b/src/Identifier/PasswordIdentifier.php @@ -47,6 +47,10 @@ class PasswordIdentifier extends AbstractIdentifier } use ResolverAwareTrait; + public const CREDENTIAL_USERNAME = 'username'; + + public const CREDENTIAL_PASSWORD = 'password'; + /** * Default configuration. * - `fields` The fields to use to identify a user by: diff --git a/src/UrlChecker/CakeRouterUrlChecker.php b/src/UrlChecker/CakeRouterUrlChecker.php index 401c85f2..078b0dfd 100644 --- a/src/UrlChecker/CakeRouterUrlChecker.php +++ b/src/UrlChecker/CakeRouterUrlChecker.php @@ -39,7 +39,7 @@ class CakeRouterUrlChecker extends DefaultUrlChecker /** * @inheritDoc */ - public function check(ServerRequestInterface $request, $loginUrls, array $options = []): bool + public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool { $options = $this->_mergeDefaultOptions($options); $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); diff --git a/src/UrlChecker/DefaultUrlChecker.php b/src/UrlChecker/DefaultUrlChecker.php index f148a02f..7515ea8f 100644 --- a/src/UrlChecker/DefaultUrlChecker.php +++ b/src/UrlChecker/DefaultUrlChecker.php @@ -39,7 +39,7 @@ class DefaultUrlChecker implements UrlCheckerInterface /** * @inheritDoc */ - public function check(ServerRequestInterface $request, $loginUrls, array $options = []): bool + public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool { $options = $this->_mergeDefaultOptions($options); diff --git a/src/UrlChecker/MultiUrlChecker.php b/src/UrlChecker/MultiUrlChecker.php new file mode 100644 index 00000000..fa223fa6 --- /dev/null +++ b/src/UrlChecker/MultiUrlChecker.php @@ -0,0 +1,100 @@ + + */ + protected array $_defaultOptions = [ + 'useRegex' => false, + 'checkFullUrl' => false, + ]; + + /** + * @inheritDoc + */ + public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool + { + $options = $this->_mergeDefaultOptions($options); + $urls = (array)$loginUrls; + + if (!$urls) { + return true; + } + + foreach ($urls as $url) { + if ($this->_checkSingleUrl($request, $url, $options)) { + return true; + } + } + + return false; + } + + /** + * Check a single URL + * + * @param \Psr\Http\Message\ServerRequestInterface $request The request. + * @param array|string $url The URL to check (can be string or array). + * @param array $options Options array. + * @return bool + */ + protected function _checkSingleUrl(ServerRequestInterface $request, array|string $url, array $options): bool + { + // Use CakeRouterUrlChecker for array URLs + if (is_array($url) && class_exists(Router::class)) { + $checker = new CakeRouterUrlChecker(); + + return $checker->check($request, [$url], $options); + } + + // Use DefaultUrlChecker for string URLs + $checker = new DefaultUrlChecker(); + + return $checker->check($request, $url, $options); + } + + /** + * Merge default options with provided options + * + * @param array $options The options to merge. + * @return array + */ + protected function _mergeDefaultOptions(array $options): array + { + return $options + $this->_defaultOptions; + } +} diff --git a/src/UrlChecker/UrlCheckerTrait.php b/src/UrlChecker/UrlCheckerTrait.php index 36fe7be8..3dc0a42f 100644 --- a/src/UrlChecker/UrlCheckerTrait.php +++ b/src/UrlChecker/UrlCheckerTrait.php @@ -17,6 +17,7 @@ namespace Authentication\UrlChecker; use Cake\Core\App; +use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; @@ -33,9 +34,14 @@ trait UrlCheckerTrait */ protected function _checkUrl(ServerRequestInterface $request): bool { + $loginUrl = $this->getConfig('loginUrl'); + if ($loginUrl === null) { + return true; + } + return $this->_getUrlChecker()->check( $request, - $this->getConfig('loginUrl'), + $loginUrl, (array)$this->getConfig('urlChecker'), ); } @@ -54,7 +60,12 @@ protected function _getUrlChecker(): UrlCheckerInterface ]; } if (!isset($options['className'])) { - $options['className'] = DefaultUrlChecker::class; + // Auto-detect CakePHP context + if (class_exists(Router::class)) { + $options['className'] = CakeRouterUrlChecker::class; + } else { + $options['className'] = DefaultUrlChecker::class; + } } $className = App::className($options['className'], 'UrlChecker', 'UrlChecker'); diff --git a/tests/TestCase/AuthenticationServiceTest.php b/tests/TestCase/AuthenticationServiceTest.php index 39849630..c25b2d90 100644 --- a/tests/TestCase/AuthenticationServiceTest.php +++ b/tests/TestCase/AuthenticationServiceTest.php @@ -22,7 +22,6 @@ use Authentication\Authenticator\AuthenticatorInterface; use Authentication\Authenticator\FormAuthenticator; use Authentication\Authenticator\Result; -use Authentication\Identifier\IdentifierCollection; use Authentication\Identifier\PasswordIdentifier; use Authentication\Identity; use Authentication\IdentityInterface; @@ -34,7 +33,6 @@ use Cake\I18n\DateTime; use Cake\Routing\Router; use InvalidArgumentException; -use PHPUnit\Runner\Version; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -226,37 +224,6 @@ public function testLoadAuthenticatorException() $service->loadAuthenticator('does-not-exist'); } - /** - * testLoadIdentifier - * - * @return void - */ - public function testLoadIdentifier() - { - $this->skipIf( - version_compare(Version::id(), '11.0', '<'), - 'For some reason PHPUnit doesn\'t pick up the deprecation on v10', - ); - - $this->deprecated(function () { - $service = new AuthenticationService(); - $result = $service->loadIdentifier('Authentication.Password'); - $this->assertInstanceOf(PasswordIdentifier::class, $result); - }); - } - - /** - * testIdentifiers - * - * @return void - */ - public function testIdentifiers() - { - $service = new AuthenticationService(); - $result = $service->identifiers(); - $this->assertInstanceOf(IdentifierCollection::class, $result); - } - /** * testClearIdentity * diff --git a/tests/TestCase/Authenticator/AuthenticatorCollectionTest.php b/tests/TestCase/Authenticator/AuthenticatorCollectionTest.php index c1565955..cadafabb 100644 --- a/tests/TestCase/Authenticator/AuthenticatorCollectionTest.php +++ b/tests/TestCase/Authenticator/AuthenticatorCollectionTest.php @@ -19,7 +19,6 @@ use Authentication\Authenticator\AuthenticatorCollection; use Authentication\Authenticator\AuthenticatorInterface; use Authentication\Authenticator\FormAuthenticator; -use Authentication\Identifier\IdentifierCollection; use Cake\TestSuite\TestCase; class AuthenticatorCollectionTest extends TestCase @@ -31,8 +30,7 @@ class AuthenticatorCollectionTest extends TestCase */ public function testConstruct() { - $identifiers = $this->createMock(IdentifierCollection::class); - $collection = new AuthenticatorCollection($identifiers, [ + $collection = new AuthenticatorCollection([ 'Authentication.Form' => [ 'identifier' => 'Authentication.Password', ], @@ -48,8 +46,7 @@ public function testConstruct() */ public function testLoad() { - $identifiers = $this->createMock(IdentifierCollection::class); - $collection = new AuthenticatorCollection($identifiers); + $collection = new AuthenticatorCollection(); $result = $collection->load('Authentication.Form', [ 'identifier' => 'Authentication.Password', ]); @@ -63,10 +60,9 @@ public function testLoad() */ public function testSet() { - $identifiers = $this->createMock(IdentifierCollection::class); $authenticator = $this->createMock(AuthenticatorInterface::class); - $collection = new AuthenticatorCollection($identifiers); + $collection = new AuthenticatorCollection(); $collection->set('Form', $authenticator); $this->assertSame($authenticator, $collection->get('Form')); } @@ -75,8 +71,7 @@ public function testLoadException() { $this->expectException('RuntimeException'); $this->expectExceptionMessage('Authenticator class `Does-not-exist` was not found.'); - $identifiers = $this->createMock(IdentifierCollection::class); - $collection = new AuthenticatorCollection($identifiers); + $collection = new AuthenticatorCollection(); $collection->load('Does-not-exist'); } @@ -87,8 +82,7 @@ public function testLoadException() */ public function testIsEmpty() { - $identifiers = $this->createMock(IdentifierCollection::class); - $collection = new AuthenticatorCollection($identifiers); + $collection = new AuthenticatorCollection(); $this->assertTrue($collection->isEmpty()); $collection->load('Authentication.Form', [ @@ -104,10 +98,9 @@ public function testIsEmpty() */ public function testIterator() { - $identifiers = $this->createMock(IdentifierCollection::class); $authenticator = $this->createMock(AuthenticatorInterface::class); - $collection = new AuthenticatorCollection($identifiers); + $collection = new AuthenticatorCollection(); $collection->set('Form', $authenticator); $this->assertContains($authenticator, $collection); diff --git a/tests/TestCase/Authenticator/CookieAuthenticatorTest.php b/tests/TestCase/Authenticator/CookieAuthenticatorTest.php index a6e468e8..92cc5223 100644 --- a/tests/TestCase/Authenticator/CookieAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/CookieAuthenticatorTest.php @@ -18,7 +18,7 @@ use ArrayObject; use Authentication\Authenticator\CookieAuthenticator; use Authentication\Authenticator\Result; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Cake\Core\Configure; use Cake\Http\Cookie\Cookie; use Cake\Http\Response; @@ -59,9 +59,7 @@ public function setUp(): void */ public function testAuthenticateInvalidTokenMissingUsername() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -72,7 +70,7 @@ public function testAuthenticateInvalidTokenMissingUsername() ], ); - $authenticator = new CookieAuthenticator($identifiers); + $authenticator = new CookieAuthenticator($identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -86,9 +84,7 @@ public function testAuthenticateInvalidTokenMissingUsername() */ public function testAuthenticateSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -100,7 +96,7 @@ public function testAuthenticateSuccess() ], ); - $authenticator = new CookieAuthenticator($identifiers); + $authenticator = new CookieAuthenticator($identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -114,9 +110,7 @@ public function testAuthenticateSuccess() */ public function testAuthenticateExpandedCookie() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -127,7 +121,7 @@ public function testAuthenticateExpandedCookie() ], ); - $authenticator = new CookieAuthenticator($identifiers); + $authenticator = new CookieAuthenticator($identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -143,9 +137,7 @@ public function testAuthenticateNoSalt() { Configure::delete('Security.salt'); - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -157,7 +149,7 @@ public function testAuthenticateNoSalt() ], ); - $authenticator = new CookieAuthenticator($identifiers, ['salt' => false]); + $authenticator = new CookieAuthenticator($identifier, ['salt' => false]); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -171,9 +163,7 @@ public function testAuthenticateNoSalt() */ public function testAuthenticateInvalidSalt() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -184,7 +174,7 @@ public function testAuthenticateInvalidSalt() ], ); - $authenticator = new CookieAuthenticator($identifiers, ['salt' => '']); + $authenticator = new CookieAuthenticator($identifier, ['salt' => '']); $this->expectException(InvalidArgumentException::class); $authenticator->authenticate($request); @@ -197,9 +187,7 @@ public function testAuthenticateInvalidSalt() */ public function testAuthenticateUnknownUser() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -210,7 +198,7 @@ public function testAuthenticateUnknownUser() ], ); - $authenticator = new CookieAuthenticator($identifiers); + $authenticator = new CookieAuthenticator($identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -224,15 +212,13 @@ public function testAuthenticateUnknownUser() */ public function testCredentialsNotPresent() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], ); - $authenticator = new CookieAuthenticator($identifiers); + $authenticator = new CookieAuthenticator($identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -246,9 +232,7 @@ public function testCredentialsNotPresent() */ public function testAuthenticateInvalidToken() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -259,7 +243,7 @@ public function testAuthenticateInvalidToken() ], ); - $authenticator = new CookieAuthenticator($identifiers); + $authenticator = new CookieAuthenticator($identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -273,9 +257,7 @@ public function testAuthenticateInvalidToken() */ public function testPersistIdentity() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -286,7 +268,7 @@ public function testPersistIdentity() $response = new Response(); Cookie::setDefaults(['samesite' => 'None']); - $authenticator = new CookieAuthenticator($identifiers, [ + $authenticator = new CookieAuthenticator($identifier, [ 'cookie' => ['expires' => '2030-01-01 00:00:00'], ]); @@ -332,7 +314,7 @@ public function testPersistIdentity() $request = $request->withParsedBody([ 'other_field' => 1, ]); - $authenticator = new CookieAuthenticator($identifiers, [ + $authenticator = new CookieAuthenticator($identifier, [ 'rememberMeField' => 'other_field', ]); $result = $authenticator->persistIdentity($request, $response, $identity); @@ -349,9 +331,7 @@ public function testPersistIdentity() */ public function testPersistIdentityLoginUrlMismatch() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -361,7 +341,7 @@ public function testPersistIdentityLoginUrlMismatch() ]); $response = new Response(); - $authenticator = new CookieAuthenticator($identifiers, [ + $authenticator = new CookieAuthenticator($identifier, [ 'loginUrl' => '/users/login', ]); @@ -387,9 +367,7 @@ public function testPersistIdentityLoginUrlMismatch() */ public function testPersistIdentityInvalidConfig() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -399,7 +377,7 @@ public function testPersistIdentityInvalidConfig() ]); $response = new Response(); - $authenticator = new CookieAuthenticator($identifiers, [ + $authenticator = new CookieAuthenticator($identifier, [ 'loginUrl' => '/users/login', ]); @@ -420,16 +398,14 @@ public function testPersistIdentityInvalidConfig() */ public function testClearIdentity() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], ); $response = new Response(); - $authenticator = new CookieAuthenticator($identifiers); + $authenticator = new CookieAuthenticator($identifier); $result = $authenticator->clearIdentity($request, $response); $this->assertIsArray($result); diff --git a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php index a5eb2d4d..a5e6e9b5 100644 --- a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php @@ -18,7 +18,7 @@ use Authentication\Authenticator\EnvironmentAuthenticator; use Authentication\Authenticator\Result; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\ServerRequestFactory; use Cake\Routing\Router; @@ -39,9 +39,9 @@ class EnvironmentAuthenticatorTest extends TestCase /** * Identifiers * - * @var \Authentication\Identifier\IdentifierCollection + * @var \Authentication\Identifier\IdentifierInterface */ - public $identifiers; + public $identifier; /** * @inheritDoc @@ -50,11 +50,9 @@ public function setUp(): void { parent::setUp(); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Token' => [ - 'tokenField' => 'username', - 'dataField' => 'USER_ID', - ], + $this->identifier = IdentifierFactory::create('Authentication.Token', [ + 'tokenField' => 'username', + 'dataField' => 'USER_ID', ]); } @@ -65,18 +63,16 @@ public function setUp(): void */ public function testAuthenticate() { - $identifiers = new IdentifierCollection([ - 'Authentication.Callback' => [ - 'callback' => function ($data) { - if (isset($data['USER_ID']) && isset($data['ATTRIBUTE'])) { - return new Result($data, RESULT::SUCCESS); - } - - return null; - }, - ], + $identifier = IdentifierFactory::create('Authentication.Callback', [ + 'callback' => function ($data) { + if (isset($data['USER_ID']) && isset($data['ATTRIBUTE'])) { + return new Result($data, RESULT::SUCCESS); + } + + return null; + }, ]); - $envAuth = new EnvironmentAuthenticator($identifiers, [ + $envAuth = new EnvironmentAuthenticator($identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -102,7 +98,7 @@ public function testAuthenticate() */ public function testFailedAuthentication() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -128,7 +124,7 @@ public function testFailedAuthentication() */ public function testWithoutFieldConfig() { - $envAuth = new EnvironmentAuthenticator($this->identifiers); + $envAuth = new EnvironmentAuthenticator($this->identifier); $result = $envAuth->authenticate(ServerRequestFactory::fromGlobals()); $this->assertInstanceOf(Result::class, $result); @@ -142,7 +138,7 @@ public function testWithoutFieldConfig() */ public function testWithIncorrectFieldConfig() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'INCORRECT_USER_ID', @@ -168,7 +164,7 @@ public function testWithIncorrectFieldConfig() */ public function testCredentialsEmpty() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -194,18 +190,16 @@ public function testCredentialsEmpty() */ public function testOptionalFields() { - $identifiers = new IdentifierCollection([ - 'Authentication.Callback' => [ - 'callback' => function ($data) { + $identifier = IdentifierFactory::create('Authentication.Callback', [ + 'callback' => function ($data) { if (isset($data['USER_ID']) && isset($data['OPTIONAL_FIELD'])) { return new Result($data, RESULT::SUCCESS); } - return null; - }, - ], + return null; + }, ]); - $envAuth = new EnvironmentAuthenticator($identifiers, [ + $envAuth = new EnvironmentAuthenticator($identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -235,7 +229,7 @@ public function testOptionalFields() */ public function testSingleLoginUrlMismatch() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -265,7 +259,7 @@ public function testMultipleLoginUrlMismatch() Router::createRouteBuilder('/') ->connect('/{lang}/secure', ['controller' => 'Users', 'action' => 'login']); - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => [ ['lang' => 'en', 'controller' => 'Users', 'action' => 'login'], ['lang' => 'de', 'controller' => 'Users', 'action' => 'login'], @@ -295,7 +289,7 @@ public function testMultipleLoginUrlMismatch() */ public function testSingleLoginUrlSuccess() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/en/secure', 'fields' => [ 'USER_ID', @@ -321,7 +315,7 @@ public function testSingleLoginUrlSuccess() */ public function testMultipleLoginUrlSuccess() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => [ '/en/secure', '/de/secure', @@ -351,7 +345,7 @@ public function testMultipleLoginUrlSuccess() */ public function testLoginUrlSuccessWithBase() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/base/fr/secure', 'fields' => [ 'USER_ID', @@ -379,9 +373,10 @@ public function testLoginUrlSuccessWithBase() */ public function testRegexLoginUrlSuccess() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%^/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ + 'className' => 'Authentication.Default', 'useRegex' => true, ], 'fields' => [ @@ -409,9 +404,10 @@ public function testRegexLoginUrlSuccess() */ public function testFullRegexLoginUrlFailure() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ + 'className' => 'Authentication.Default', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -440,9 +436,10 @@ public function testFullRegexLoginUrlFailure() */ public function testFullRegexLoginUrlSuccess() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ + 'className' => 'Authentication.Default', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -472,7 +469,7 @@ public function testFullRegexLoginUrlSuccess() */ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => 'http://localhost/secure', 'fields' => [ 'USER_ID', @@ -499,8 +496,7 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() */ public function testAuthenticateMissingChecker() { - $this->createMock(IdentifierCollection::class); - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -527,8 +523,7 @@ public function testAuthenticateMissingChecker() */ public function testAuthenticateInvalidChecker() { - $this->createMock(IdentifierCollection::class); - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', diff --git a/tests/TestCase/Authenticator/FormAuthenticatorTest.php b/tests/TestCase/Authenticator/FormAuthenticatorTest.php index 13b72414..db14ab42 100644 --- a/tests/TestCase/Authenticator/FormAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/FormAuthenticatorTest.php @@ -18,7 +18,8 @@ use Authentication\Authenticator\FormAuthenticator; use Authentication\Authenticator\Result; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; +use Authentication\Identifier\IdentifierInterface; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\ServerRequestFactory; use Cake\Routing\Router; @@ -43,9 +44,7 @@ class FormAuthenticatorTest extends TestCase */ public function testAuthenticate() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -53,7 +52,7 @@ public function testAuthenticate() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers); + $form = new FormAuthenticator($identifier); $result = $form->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -67,9 +66,7 @@ public function testAuthenticate() */ public function testCredentialsNotPresent() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/does-not-match'], @@ -77,7 +74,7 @@ public function testCredentialsNotPresent() [], ); - $form = new FormAuthenticator($identifiers); + $form = new FormAuthenticator($identifier); $result = $form->authenticate($request); @@ -93,9 +90,7 @@ public function testCredentialsNotPresent() */ public function testCredentialsEmpty() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/does-not-match'], @@ -103,7 +98,7 @@ public function testCredentialsEmpty() ['username' => '', 'password' => ''], ); - $form = new FormAuthenticator($identifiers); + $form = new FormAuthenticator($identifier); $result = $form->authenticate($request); @@ -114,9 +109,7 @@ public function testCredentialsEmpty() public function testIdentityNotFound() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/does-not-match'], @@ -124,7 +117,7 @@ public function testIdentityNotFound() ['username' => 'non-existent', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers); + $form = new FormAuthenticator($identifier); $result = $form->authenticate($request); @@ -140,9 +133,7 @@ public function testIdentityNotFound() */ public function testSingleLoginUrlMismatch() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/does-not-match'], @@ -150,7 +141,7 @@ public function testSingleLoginUrlMismatch() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', ]); @@ -168,9 +159,7 @@ public function testSingleLoginUrlMismatch() */ public function testMultipleLoginUrlMismatch() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/does-not-match'], @@ -181,7 +170,7 @@ public function testMultipleLoginUrlMismatch() Router::createRouteBuilder('/') ->connect('/{lang}/users/login', ['controller' => 'Users', 'action' => 'login']); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'urlChecker' => 'Authentication.CakeRouter', 'loginUrl' => [ ['lang' => 'en', 'controller' => 'Users', 'action' => 'login'], @@ -203,9 +192,7 @@ public function testMultipleLoginUrlMismatch() */ public function testLoginUrlMismatchWithBase() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -214,7 +201,7 @@ public function testLoginUrlMismatchWithBase() ); $request = $request->withAttribute('base', '/base'); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', ]); @@ -232,9 +219,7 @@ public function testLoginUrlMismatchWithBase() */ public function testSingleLoginUrlSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/Users/login'], @@ -242,7 +227,7 @@ public function testSingleLoginUrlSuccess() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/Users/login', ]); @@ -260,9 +245,7 @@ public function testSingleLoginUrlSuccess() */ public function testMultipleLoginUrlSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/de/users/login'], @@ -270,7 +253,7 @@ public function testMultipleLoginUrlSuccess() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => [ '/en/users/login', '/de/users/login', @@ -291,9 +274,7 @@ public function testMultipleLoginUrlSuccess() */ public function testLoginUrlSuccessWithBase() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -302,7 +283,7 @@ public function testLoginUrlSuccessWithBase() ); $request = $request->withAttribute('base', '/base'); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/base/users/login', ]); @@ -320,9 +301,7 @@ public function testLoginUrlSuccessWithBase() */ public function testRegexLoginUrlSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/de/users/login'], @@ -330,9 +309,10 @@ public function testRegexLoginUrlSuccess() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%^/[a-z]{2}/users/login/?$%', 'urlChecker' => [ + 'className' => 'Authentication.Default', 'useRegex' => true, ], ]); @@ -351,9 +331,7 @@ public function testRegexLoginUrlSuccess() */ public function testFullRegexLoginUrlFailure() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( [ @@ -363,9 +341,10 @@ public function testFullRegexLoginUrlFailure() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 'urlChecker' => [ + 'className' => 'Authentication.Default', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -385,9 +364,7 @@ public function testFullRegexLoginUrlFailure() */ public function testFullRegexLoginUrlSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( [ @@ -398,9 +375,10 @@ public function testFullRegexLoginUrlSuccess() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 'urlChecker' => [ + 'className' => 'Authentication.Default', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -420,9 +398,7 @@ public function testFullRegexLoginUrlSuccess() */ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -430,7 +406,7 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => 'http://localhost/users/login', ]); @@ -448,7 +424,7 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() */ public function testAuthenticateCustomFields() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -456,7 +432,7 @@ public function testAuthenticateCustomFields() ['email' => 'mariano@cakephp.org', 'secret' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', 'fields' => [ 'username' => 'email', @@ -464,7 +440,7 @@ public function testAuthenticateCustomFields() ], ]); - $identifiers->expects($this->once()) + $identifier->expects($this->once()) ->method('identify') ->with([ 'username' => 'mariano@cakephp.org', @@ -485,7 +461,7 @@ public function testAuthenticateCustomFields() */ public function testAuthenticateValidData() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -493,11 +469,11 @@ public function testAuthenticateValidData() ['id' => 1, 'username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', ]); - $identifiers->expects($this->once()) + $identifier->expects($this->once()) ->method('identify') ->with([ 'username' => 'mariano', @@ -518,7 +494,7 @@ public function testAuthenticateValidData() */ public function testAuthenticateMissingChecker() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -526,7 +502,7 @@ public function testAuthenticateMissingChecker() ['id' => 1, 'username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', 'urlChecker' => 'Foo', ]); @@ -544,7 +520,7 @@ public function testAuthenticateMissingChecker() */ public function testAuthenticateInvalidChecker() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -552,7 +528,7 @@ public function testAuthenticateInvalidChecker() ['id' => 1, 'username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', 'urlChecker' => self::class, ]); @@ -565,101 +541,4 @@ public function testAuthenticateInvalidChecker() $form->authenticate($request); } - - /** - * Test that FormAuthenticator uses default Password identifier when none is provided. - * - * @return void - */ - public function testDefaultPasswordIdentifier() - { - // Create an empty IdentifierCollection (simulating no explicit identifier configuration) - $identifiers = new IdentifierCollection(); - - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/testpath'], - [], - ['username' => 'mariano', 'password' => 'password'], - ); - - // FormAuthenticator should automatically configure a Password identifier - $form = new FormAuthenticator($identifiers); - $result = $form->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::SUCCESS, $result->getStatus()); - - // Verify the identifier collection now has the Password identifier - $identifier = $form->getIdentifier(); - $this->assertInstanceOf(IdentifierCollection::class, $identifier); - $this->assertFalse($identifier->isEmpty()); - } - - /** - * Test that FormAuthenticator respects explicitly configured identifier. - * - * @return void - */ - public function testExplicitIdentifierNotOverridden() - { - // Create an IdentifierCollection with a specific identifier - $identifiers = new IdentifierCollection([ - 'Password' => [ - 'className' => 'Authentication.Password', - 'fields' => [ - 'username' => 'email', - 'password' => 'password', - ], - ], - ]); - - ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/testpath'], - [], - ['email' => 'mariano@example.com', 'password' => 'password'], - ); - - // FormAuthenticator should use the provided identifier - $form = new FormAuthenticator($identifiers); - - // The identifier should remain as configured - $identifier = $form->getIdentifier(); - $this->assertInstanceOf(IdentifierCollection::class, $identifier); - $this->assertFalse($identifier->isEmpty()); - $this->assertSame($identifiers, $identifier, 'Identifier collection should be the same.'); - $this->assertSame($identifiers->get('Password'), $identifier->get('Password'), 'Identifier should be the same.'); - } - - /** - * Test that default identifier inherits fields configuration from authenticator. - * - * @return void - */ - public function testDefaultIdentifierInheritsFieldsConfig() - { - // Create an empty IdentifierCollection - $identifiers = new IdentifierCollection(); - - // Configure authenticator with custom fields mapping - $config = [ - 'fields' => [ - 'username' => 'user_name', - 'password' => 'pass_word', - ], - ]; - - // FormAuthenticator should create default identifier with inherited fields - $form = new FormAuthenticator($identifiers, $config); - - // Verify the identifier was created with the correct configuration - $identifier = $form->getIdentifier(); - $this->assertInstanceOf(IdentifierCollection::class, $identifier); - $this->assertFalse($identifier->isEmpty()); - - // Verify the fields are properly configured - // We can't directly access the internal configuration, but we can verify - // the FormAuthenticator has the expected configuration - $this->assertEquals('user_name', $form->getConfig('fields.username')); - $this->assertEquals('pass_word', $form->getConfig('fields.password')); - } } diff --git a/tests/TestCase/Authenticator/HttpBasicAuthenticatorTest.php b/tests/TestCase/Authenticator/HttpBasicAuthenticatorTest.php index dff7e06e..71ddd053 100644 --- a/tests/TestCase/Authenticator/HttpBasicAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/HttpBasicAuthenticatorTest.php @@ -19,7 +19,7 @@ use Authentication\Authenticator\AuthenticationRequiredException; use Authentication\Authenticator\HttpBasicAuthenticator; use Authentication\Authenticator\ResultInterface; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\ServerRequestFactory; use Cake\I18n\DateTime; @@ -38,9 +38,9 @@ class HttpBasicAuthenticatorTest extends TestCase ]; /** - * @var \Authentication\Identifier\IdentifierCollection + * @var \Authentication\Identifier\IdentifierInterface */ - protected $identifiers; + protected $identifier; /** * @var \Authentication\Authenticator\HttpBasicAuthenticator @@ -54,11 +54,9 @@ public function setUp(): void { parent::setUp(); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $this->identifier = IdentifierFactory::create('Authentication.Password'); - $this->auth = new HttpBasicAuthenticator($this->identifiers); + $this->auth = new HttpBasicAuthenticator($this->identifier); } /** @@ -68,7 +66,7 @@ public function setUp(): void */ public function testConstructor() { - $object = new HttpBasicAuthenticator($this->identifiers, [ + $object = new HttpBasicAuthenticator($this->identifier, [ 'userModel' => 'AuthUser', 'fields' => [ 'username' => 'user', diff --git a/tests/TestCase/Authenticator/HttpDigestAuthenticatorTest.php b/tests/TestCase/Authenticator/HttpDigestAuthenticatorTest.php index b5224d9c..2eedcb77 100644 --- a/tests/TestCase/Authenticator/HttpDigestAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/HttpDigestAuthenticatorTest.php @@ -21,7 +21,7 @@ use Authentication\Authenticator\HttpDigestAuthenticator; use Authentication\Authenticator\Result; use Authentication\Authenticator\StatelessInterface; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Cake\Http\ServerRequestFactory; use Cake\I18n\DateTime; use Cake\ORM\TableRegistry; @@ -44,9 +44,9 @@ class HttpDigestAuthenticatorTest extends TestCase ]; /** - * @var \Authentication\Identifier\IdentifierCollection + * @var \Authentication\Identifier\IdentifierInterface */ - protected $identifiers; + protected $identifier; /** * @var \Authentication\Authenticator\HttpDigestAuthenticator @@ -62,11 +62,9 @@ public function setUp(): void { parent::setUp(); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $this->identifier = IdentifierFactory::create('Authentication.Password'); - $this->auth = new HttpDigestAuthenticator($this->identifiers, [ + $this->auth = new HttpDigestAuthenticator($this->identifier, [ 'realm' => 'localhost', 'nonce' => 123, 'opaque' => '123abc', @@ -85,7 +83,7 @@ public function setUp(): void */ public function testConstructor() { - $object = new HttpDigestAuthenticator($this->identifiers, [ + $object = new HttpDigestAuthenticator($this->identifier, [ 'userModel' => 'AuthUser', 'fields' => ['username' => 'user', 'password' => 'pass'], 'nonce' => 123456, diff --git a/tests/TestCase/Authenticator/JwtAuthenticatorTest.php b/tests/TestCase/Authenticator/JwtAuthenticatorTest.php index df8531a4..c2b36c4c 100644 --- a/tests/TestCase/Authenticator/JwtAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/JwtAuthenticatorTest.php @@ -20,7 +20,7 @@ use ArrayObject; use Authentication\Authenticator\JwtAuthenticator; use Authentication\Authenticator\Result; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierInterface; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\ServerRequestFactory; use Exception; @@ -56,9 +56,9 @@ class JwtAuthenticatorTest extends TestCase /** * Identifier Collection * - * @var \Authentication\Identifier\IdentifierCollection; + * @var \Authentication\Identifier\IdentifierInterface; */ - public $identifiers; + public $identifier; /** * @var \Cake\Http\ServerRequest @@ -84,7 +84,7 @@ public function setUp(): void $privKey1 = file_get_contents(__DIR__ . '/../../data/rsa1-private.pem'); $this->tokenRS256 = JWT::encode($data, $privKey1, 'RS256', 'jwk1'); - $this->identifiers = new IdentifierCollection([]); + $this->identifier = null; } /** @@ -99,7 +99,7 @@ public function testAuthenticateViaHeaderToken() ); $this->request = $this->request->withAddedHeader('Authorization', 'Bearer ' . $this->tokenHS256); - $authenticator = new JwtAuthenticator($this->identifiers, [ + $authenticator = new JwtAuthenticator($this->identifier, [ 'secretKey' => 'secretKey', 'subjectKey' => 'subjectId', ]); @@ -122,7 +122,7 @@ public function testAuthenticateViaQueryParamToken() ['token' => $this->tokenHS256], ); - $authenticator = new JwtAuthenticator($this->identifiers, [ + $authenticator = new JwtAuthenticator($this->identifier, [ 'secretKey' => 'secretKey', 'subjectKey' => 'subjectId', ]); @@ -145,8 +145,8 @@ public function testAuthenticationViaIdentifierAndSubject() ['token' => $this->tokenHS256], ); - $this->identifiers = $this->createMock(IdentifierCollection::class); - $this->identifiers->expects($this->once()) + $this->identifier = $this->createMock(IdentifierInterface::class); + $this->identifier->expects($this->once()) ->method('identify') ->with([ 'subjectId' => 3, @@ -158,7 +158,7 @@ public function testAuthenticationViaIdentifierAndSubject() 'firstname' => 'larry', ])); - $authenticator = new JwtAuthenticator($this->identifiers, [ + $authenticator = new JwtAuthenticator($this->identifier, [ 'secretKey' => 'secretKey', 'returnPayload' => false, 'subjectKey' => 'subjectId', @@ -186,7 +186,7 @@ public function testAuthenticateInvalidPayloadNotAnObject() $authenticator = $this->getMockBuilder(JwtAuthenticator::class) ->setConstructorArgs([ - $this->identifiers, + $this->identifier, ]) ->onlyMethods([ 'getPayLoad', @@ -217,7 +217,7 @@ public function testAuthenticateInvalidPayloadEmpty() $authenticator = $this->getMockBuilder(JwtAuthenticator::class) ->setConstructorArgs([ - $this->identifiers, + $this->identifier, ]) ->onlyMethods([ 'getPayLoad', @@ -241,7 +241,7 @@ public function testInvalidToken() ['token' => 'should cause an exception'], ); - $authenticator = new JwtAuthenticator($this->identifiers, [ + $authenticator = new JwtAuthenticator($this->identifier, [ 'secretKey' => 'secretKey', ]); @@ -267,7 +267,7 @@ public function testGetPayloadHS256() ['token' => $this->tokenHS256], ); - $authenticator = new JwtAuthenticator($this->identifiers, [ + $authenticator = new JwtAuthenticator($this->identifier, [ 'secretKey' => 'secretKey', ]); @@ -299,7 +299,7 @@ public function testGetPayloadRS256() ['token' => $this->tokenRS256], ); - $authenticator = new JwtAuthenticator($this->identifiers, [ + $authenticator = new JwtAuthenticator($this->identifier, [ 'jwks' => json_decode(file_get_contents(__DIR__ . '/../../data/rsa-jwkset.json'), true), ]); diff --git a/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php b/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php index 1330900d..5b6d8806 100644 --- a/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php @@ -19,7 +19,7 @@ use ArrayObject; use Authentication\Authenticator\PrimaryKeySessionAuthenticator; use Authentication\Authenticator\Result; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; use Cake\Http\ServerRequestFactory; @@ -35,12 +35,13 @@ class PrimaryKeySessionAuthenticatorTest extends TestCase */ protected array $fixtures = [ 'core.AuthUsers', + 'core.Users', ]; /** - * @var \Authentication\Identifier\IdentifierCollection + * @var \Authentication\Identifier\IdentifierInterface */ - protected $identifiers; + protected $identifier; /** * @var \Cake\Http\Session&\PHPUnit\Framework\MockObject\MockObject @@ -54,9 +55,9 @@ public function setUp(): void { parent::setUp(); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Password' => [ - ], + $this->identifier = IdentifierFactory::create('Authentication.Token', [ + 'tokenField' => 'id', + 'dataField' => 'key', ]); $this->sessionMock = $this->getMockBuilder(Session::class) @@ -81,18 +82,7 @@ public function testAuthenticateSuccess() $request = $request->withAttribute('session', $this->sessionMock); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Token' => [ - 'tokenField' => 'id', - 'dataField' => 'key', - 'resolver' => [ - 'className' => 'Authentication.Orm', - 'userModel' => 'AuthUsers', - ], - ], - ]); - - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -118,20 +108,17 @@ public function testAuthenticateSuccessCustomFinder() $request = $request->withAttribute('session', $this->sessionMock); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Token' => [ - 'tokenField' => 'id', - 'dataField' => 'key', - 'resolver' => [ - 'className' => 'Authentication.Orm', - 'userModel' => 'AuthUsers', - 'finder' => 'auth', - ], + $this->identifier = IdentifierFactory::create('Authentication.Token', [ + 'tokenField' => 'id', + 'dataField' => 'key', + 'resolver' => [ + 'className' => 'Authentication.Orm', + 'userModel' => 'AuthUsers', + 'finder' => 'auth', ], ]); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers, [ - ]); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -157,7 +144,7 @@ public function testAuthenticateFailure() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -180,7 +167,7 @@ public function testVerifyByDatabaseFailure() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers, [ + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier, [ ]); $result = $authenticator->authenticate($request); @@ -198,7 +185,7 @@ public function testPersistIdentity() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $data = new ArrayObject(['id' => 1]); @@ -241,7 +228,7 @@ public function testClearIdentity() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $this->sessionMock->expects($this->once()) ->method('delete') @@ -270,7 +257,7 @@ public function testImpersonate() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $usersTable = $this->fetchTable('Users'); $impersonator = $usersTable->newEntity([ 'username' => 'mariano', @@ -311,7 +298,7 @@ public function testImpersonateAlreadyImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $impersonator = new ArrayObject([ 'username' => 'mariano', 'password' => 'password', @@ -345,7 +332,7 @@ public function testStopImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $impersonator = new ArrayObject([ 'username' => 'mariano', @@ -392,7 +379,7 @@ public function testStopImpersonatingNotImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $this->sessionMock->expects($this->once()) ->method('check') @@ -429,7 +416,7 @@ public function testIsImpersonating() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $authenticator = new PrimaryKeySessionAuthenticator($this->identifier); $this->sessionMock->expects($this->once()) ->method('check') diff --git a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php index bd22d5ea..ce5014f5 100644 --- a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php @@ -19,7 +19,7 @@ use ArrayObject; use Authentication\Authenticator\Result; use Authentication\Authenticator\SessionAuthenticator; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\PasswordIdentifier; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\Exception\UnauthorizedException; @@ -43,9 +43,9 @@ class SessionAuthenticatorTest extends TestCase ]; /** - * @var \Authentication\Identifier\IdentifierCollection + * @var \Authentication\Identifier\IdentifierInterface */ - protected $identifiers; + protected $identifier; protected $sessionMock; @@ -56,9 +56,7 @@ public function setUp(): void { parent::setUp(); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $this->identifier = IdentifierFactory::create('Authentication.Password'); $this->sessionMock = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() @@ -85,7 +83,7 @@ public function testAuthenticateSuccess() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -111,7 +109,7 @@ public function testAuthenticateSuccessWithoutCollection() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ + $authenticator = new SessionAuthenticator(null, [ 'identifier' => 'Authentication.Password', ]); $result = $authenticator->authenticate($request); @@ -139,7 +137,7 @@ public function testAuthenticateSuccessWithoutCollectionButObject() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ + $authenticator = new SessionAuthenticator(null, [ 'identifier' => new PasswordIdentifier(), ]); $result = $authenticator->authenticate($request); @@ -167,8 +165,8 @@ public function testAuthenticateSuccessWithDirectCollection() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ - 'identifier' => new IdentifierCollection(['Authentication.Password']), + $authenticator = new SessionAuthenticator(null, [ + 'identifier' => IdentifierFactory::create('Authentication.Password'), ]); $result = $authenticator->authenticate($request); @@ -192,7 +190,7 @@ public function testAuthenticateFailure() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -218,7 +216,7 @@ public function testVerifyByDatabaseSuccess() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers, [ + $authenticator = new SessionAuthenticator($this->identifier, [ 'identify' => true, ]); $result = $authenticator->authenticate($request); @@ -246,7 +244,7 @@ public function testVerifyByDatabaseFailure() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers, [ + $authenticator = new SessionAuthenticator($this->identifier, [ 'identify' => true, ]); $result = $authenticator->authenticate($request); @@ -265,7 +263,7 @@ public function testPersistIdentity() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $data = new ArrayObject(['username' => 'florian']); @@ -308,7 +306,7 @@ public function testClearIdentity() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $this->sessionMock->expects($this->once()) ->method('delete') @@ -337,7 +335,7 @@ public function testImpersonate() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $AuthUsers = TableRegistry::getTableLocator()->get('AuthUsers'); $impersonator = $AuthUsers->newEntity([ 'username' => 'mariano', @@ -376,7 +374,7 @@ public function testImpersonateAlreadyImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $impersonator = new ArrayObject([ 'username' => 'mariano', 'password' => 'password', @@ -410,7 +408,7 @@ public function testStopImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $impersonator = new ArrayObject([ 'username' => 'mariano', @@ -457,7 +455,7 @@ public function testStopImpersonatingNotImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $this->sessionMock->expects($this->once()) ->method('check') @@ -494,7 +492,7 @@ public function testIsImpersonating() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator($this->identifier); $this->sessionMock->expects($this->once()) ->method('check') diff --git a/tests/TestCase/Authenticator/TokenAuthenticatorTest.php b/tests/TestCase/Authenticator/TokenAuthenticatorTest.php index f471fb38..8b2a4725 100644 --- a/tests/TestCase/Authenticator/TokenAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/TokenAuthenticatorTest.php @@ -18,7 +18,7 @@ use Authentication\Authenticator\Result; use Authentication\Authenticator\TokenAuthenticator; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\ServerRequestFactory; @@ -35,9 +35,9 @@ class TokenAuthenticatorTest extends TestCase ]; /** - * @var \Authentication\Identifier\IdentifierCollection + * @var \Authentication\Identifier\TokenIdentifier */ - protected $identifiers; + protected $identifier; /** * @var \Cake\Http\ServerRequest @@ -51,10 +51,8 @@ public function setUp(): void { parent::setUp(); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Token' => [ - 'tokenField' => 'username', - ], + $this->identifier = IdentifierFactory::create('Authentication.Token', [ + 'tokenField' => 'username', ]); $this->request = ServerRequestFactory::fromGlobals( @@ -72,7 +70,7 @@ public function setUp(): void public function testAuthenticateViaHeaderToken() { // Test without token - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'queryParam' => 'token', ]); $result = $tokenAuth->authenticate($this->request); @@ -81,7 +79,7 @@ public function testAuthenticateViaHeaderToken() // Test header token $requestWithHeaders = $this->request->withAddedHeader('Token', 'mariano'); - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'header' => 'Token', ]); $result = $tokenAuth->authenticate($requestWithHeaders); @@ -98,7 +96,7 @@ public function testViaQueryParamToken() { // Test with query param token $requestWithParams = $this->request->withQueryParams(['token' => 'mariano']); - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'queryParam' => 'token', ]); $result = $tokenAuth->authenticate($requestWithParams); @@ -107,7 +105,7 @@ public function testViaQueryParamToken() // Test with valid query param but invalid token $requestWithParams = $this->request->withQueryParams(['token' => 'does-not-exist']); - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'queryParam' => 'token', ]); $result = $tokenAuth->authenticate($requestWithParams); @@ -124,7 +122,7 @@ public function testTokenPrefix() { //valid prefix $requestWithHeaders = $this->request->withAddedHeader('Token', 'identity mariano'); - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'header' => 'Token', 'tokenPrefix' => 'identity', ]); @@ -133,7 +131,7 @@ public function testTokenPrefix() $this->assertSame(Result::SUCCESS, $result->getStatus()); $requestWithHeaders = $this->request->withAddedHeader('X-Dipper-Auth', 'dipper_mariano'); - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'header' => 'X-Dipper-Auth', 'tokenPrefix' => 'dipper_', ]); @@ -143,7 +141,7 @@ public function testTokenPrefix() //invalid prefix $requestWithHeaders = $this->request->withAddedHeader('Token', 'bearer mariano'); - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'header' => 'Token', 'tokenPrefix' => 'identity', ]); @@ -153,7 +151,7 @@ public function testTokenPrefix() // should not remove prefix from token $requestWithHeaders = $this->request->withAddedHeader('X-Dipper-Auth', 'mari mariano'); - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'header' => 'X-Dipper-Auth', 'tokenPrefix' => 'mari', ]); @@ -169,7 +167,7 @@ public function testTokenPrefix() */ public function testWithoutQueryParamConfig() { - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'header' => 'Token', ]); @@ -185,7 +183,7 @@ public function testWithoutQueryParamConfig() */ public function testWithoutHeaderConfig() { - $tokenAuth = new TokenAuthenticator($this->identifiers, [ + $tokenAuth = new TokenAuthenticator($this->identifier, [ 'queryParam' => 'token', ]); @@ -201,7 +199,7 @@ public function testWithoutHeaderConfig() */ public function testWithoutAnyConfig() { - $tokenAuth = new TokenAuthenticator($this->identifiers); + $tokenAuth = new TokenAuthenticator($this->identifier); $result = $tokenAuth->authenticate(ServerRequestFactory::fromGlobals()); $this->assertInstanceOf(Result::class, $result); diff --git a/tests/TestCase/Identifier/IdentifierCollectionTest.php b/tests/TestCase/Identifier/IdentifierCollectionTest.php deleted file mode 100644 index b296c4b5..00000000 --- a/tests/TestCase/Identifier/IdentifierCollectionTest.php +++ /dev/null @@ -1,121 +0,0 @@ -get('Password'); - $this->assertInstanceOf('\Authentication\Identifier\PasswordIdentifier', $result); - } - - /** - * testLoad - * - * @return void - */ - public function testLoad() - { - $collection = new IdentifierCollection(); - $result = $collection->load('Authentication.Password'); - $this->assertInstanceOf('\Authentication\Identifier\PasswordIdentifier', $result); - } - - /** - * testSet - * - * @return void - */ - public function testSet() - { - $identifier = $this->createMock(IdentifierInterface::class); - $collection = new IdentifierCollection(); - $collection->set('Password', $identifier); - $this->assertSame($identifier, $collection->get('Password')); - } - - public function testLoadException() - { - $this->expectException('RuntimeException'); - $this->expectExceptionMessage('Identifier class `Does-not-exist` was not found.'); - $collection = new IdentifierCollection(); - $collection->load('Does-not-exist'); - } - - /** - * testIsEmpty - * - * @return void - */ - public function testIsEmpty() - { - $collection = new IdentifierCollection(); - $this->assertTrue($collection->isEmpty()); - - $collection->load('Authentication.Password'); - $this->assertFalse($collection->isEmpty()); - } - - /** - * testIterator - * - * @return void - */ - public function testIterator() - { - $identifier = $this->createMock(IdentifierInterface::class); - $collection = new IdentifierCollection(); - $collection->set('Password', $identifier); - - $this->assertContains($identifier, $collection); - } - - /** - * testIdentify - * - * @return void - */ - public function testIdentify() - { - $collection = new IdentifierCollection([ - 'Authentication.Password', - ]); - - $result = $collection->identify([ - 'username' => 'mariano', - 'password' => 'password', - ]); - - $this->assertInstanceOf('\ArrayAccess', $result); - $this->assertInstanceOf(PasswordIdentifier::class, $collection->getIdentificationProvider()); - - $collection->identify([ - 'username' => 'mariano', - 'password' => 'invalid password', - ]); - $this->assertNull($collection->getIdentificationProvider()); - } -} From 1db447312bb41879ed05db22ee65f93deabc1625 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 6 Nov 2025 07:49:27 +0100 Subject: [PATCH 02/30] Remove deprecations, fix up MultiChecker. --- src/Authenticator/FormAuthenticator.php | 2 +- ...outerUrlChecker.php => CakeUrlChecker.php} | 27 ++---- src/UrlChecker/DefaultUrlChecker.php | 25 +++--- src/UrlChecker/MultiUrlChecker.php | 42 +++++++-- src/UrlChecker/UrlCheckerTrait.php | 8 +- .../EnvironmentAuthenticatorTest.php | 2 + .../Authenticator/FormAuthenticatorTest.php | 3 +- ...CheckerTest.php => CakeUrlCheckerTest.php} | 89 ++++--------------- .../UrlChecker/DefaultUrlCheckerTest.php | 26 +----- 9 files changed, 79 insertions(+), 145 deletions(-) rename src/UrlChecker/{CakeRouterUrlChecker.php => CakeUrlChecker.php} (63%) rename tests/TestCase/UrlChecker/{CakeRouterUrlCheckerTest.php => CakeUrlCheckerTest.php} (65%) diff --git a/src/Authenticator/FormAuthenticator.php b/src/Authenticator/FormAuthenticator.php index caca2d98..6292377b 100644 --- a/src/Authenticator/FormAuthenticator.php +++ b/src/Authenticator/FormAuthenticator.php @@ -42,7 +42,7 @@ class FormAuthenticator extends AbstractAuthenticator */ protected array $_defaultConfig = [ 'loginUrl' => null, - 'urlChecker' => 'Authentication.Default', + 'urlChecker' => 'Authentication.Cake', 'fields' => [ PasswordIdentifier::CREDENTIAL_USERNAME => 'username', PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', diff --git a/src/UrlChecker/CakeRouterUrlChecker.php b/src/UrlChecker/CakeUrlChecker.php similarity index 63% rename from src/UrlChecker/CakeRouterUrlChecker.php rename to src/UrlChecker/CakeUrlChecker.php index 078b0dfd..8506a392 100644 --- a/src/UrlChecker/CakeRouterUrlChecker.php +++ b/src/UrlChecker/CakeUrlChecker.php @@ -17,20 +17,19 @@ namespace Authentication\UrlChecker; use Cake\Routing\Router; -use InvalidArgumentException; use Psr\Http\Message\ServerRequestInterface; /** - * Checks if a request object contains a valid URL + * Checks if a request object contains a valid URL using CakePHP Router */ -class CakeRouterUrlChecker extends DefaultUrlChecker +class CakeUrlChecker extends DefaultUrlChecker { /** * Default Options * * - `checkFullUrl` Whether to check the full request URI. * - * @var array + * @var array */ protected array $_defaultOptions = [ 'checkFullUrl' => false, @@ -44,23 +43,9 @@ public function check(ServerRequestInterface $request, array|string $loginUrls, $options = $this->_mergeDefaultOptions($options); $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); - if (!is_array($loginUrls) || empty($loginUrls)) { - throw new InvalidArgumentException('The $loginUrls parameter is empty or not of type array.'); - } + // Support both string URLs and array-based routes (like Router::url()) + $validUrl = Router::url($loginUrls, $options['checkFullUrl']); - // If it's a single route array add to another - if (!is_numeric(key($loginUrls))) { - $loginUrls = [$loginUrls]; - } - - foreach ($loginUrls as $validUrl) { - $validUrl = Router::url($validUrl, $options['checkFullUrl']); - - if ($validUrl === $url) { - return true; - } - } - - return false; + return $validUrl === $url; } } diff --git a/src/UrlChecker/DefaultUrlChecker.php b/src/UrlChecker/DefaultUrlChecker.php index 7515ea8f..e9eadbf6 100644 --- a/src/UrlChecker/DefaultUrlChecker.php +++ b/src/UrlChecker/DefaultUrlChecker.php @@ -17,9 +17,10 @@ namespace Authentication\UrlChecker; use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; /** - * Checks if a request object contains a valid URL + * Checks if a request object contains a valid URL. Framework agnostic. */ class DefaultUrlChecker implements UrlCheckerInterface { @@ -29,7 +30,7 @@ class DefaultUrlChecker implements UrlCheckerInterface * - `urlChecker` Whether to use `loginUrl` as regular expression(s). * - `checkFullUrl` Whether to check the full request URI. * - * @var array + * @var array */ protected array $_defaultOptions = [ 'useRegex' => false, @@ -41,24 +42,18 @@ class DefaultUrlChecker implements UrlCheckerInterface */ public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool { - $options = $this->_mergeDefaultOptions($options); - - $urls = (array)$loginUrls; - if (!$urls) { - return true; + if (is_array($loginUrls)) { + throw new RuntimeException( + 'Array-based login URLs require CakePHP Router and CakeUrlChecker. ' . + 'Either install cakephp/cakephp or use string URLs instead.', + ); } + $options = $this->_mergeDefaultOptions($options); $checker = $this->_getChecker($options); - $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); - foreach ($urls as $validUrl) { - if ($checker($validUrl, $url)) { - return true; - } - } - - return false; + return (bool)$checker($loginUrls, $url); } /** diff --git a/src/UrlChecker/MultiUrlChecker.php b/src/UrlChecker/MultiUrlChecker.php index fa223fa6..e0dfe0e0 100644 --- a/src/UrlChecker/MultiUrlChecker.php +++ b/src/UrlChecker/MultiUrlChecker.php @@ -26,7 +26,7 @@ * string URLs and array-based CakePHP routes. * * This checker automatically detects the URL type and uses the appropriate - * checker (Default for strings, CakeRouter for arrays). + * checker (Default for strings, Cake for arrays). */ class MultiUrlChecker implements UrlCheckerInterface { @@ -49,7 +49,13 @@ class MultiUrlChecker implements UrlCheckerInterface public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool { $options = $this->_mergeDefaultOptions($options); - $urls = (array)$loginUrls; + + // For a single URL (string or array route), convert to array + if (is_string($loginUrls) || $this->_isSingleRoute($loginUrls)) { + $urls = [$loginUrls]; + } else { + $urls = $loginUrls; + } if (!$urls) { return true; @@ -64,6 +70,30 @@ public function check(ServerRequestInterface $request, array|string $loginUrls, return false; } + /** + * Check if the array is a single CakePHP route (not an array of routes) + * + * @param array|string $value The value to check + * @return bool + */ + protected function _isSingleRoute(array|string $value): bool + { + if (!is_array($value)) { + return false; + } + + if (!$value) { + return false; + } + + // A single route has string keys like ['controller' => 'Users'] + // An array of routes has numeric keys [0 => '/login', 1 => '/signin'] + reset($value); + $firstKey = key($value); + + return !is_int($firstKey); + } + /** * Check a single URL * @@ -74,14 +104,12 @@ public function check(ServerRequestInterface $request, array|string $loginUrls, */ protected function _checkSingleUrl(ServerRequestInterface $request, array|string $url, array $options): bool { - // Use CakeRouterUrlChecker for array URLs - if (is_array($url) && class_exists(Router::class)) { - $checker = new CakeRouterUrlChecker(); + if (class_exists(Router::class)) { + $checker = new CakeUrlChecker(); - return $checker->check($request, [$url], $options); + return $checker->check($request, $url, $options); } - // Use DefaultUrlChecker for string URLs $checker = new DefaultUrlChecker(); return $checker->check($request, $url, $options); diff --git a/src/UrlChecker/UrlCheckerTrait.php b/src/UrlChecker/UrlCheckerTrait.php index 3dc0a42f..c38d6675 100644 --- a/src/UrlChecker/UrlCheckerTrait.php +++ b/src/UrlChecker/UrlCheckerTrait.php @@ -54,15 +54,17 @@ protected function _checkUrl(ServerRequestInterface $request): bool protected function _getUrlChecker(): UrlCheckerInterface { $options = $this->getConfig('urlChecker'); + if (!is_array($options)) { $options = [ 'className' => $options, ]; } - if (!isset($options['className'])) { - // Auto-detect CakePHP context + + // If no explicit className is set (or it's null/empty), auto-detect CakePHP context + if (empty($options['className'])) { if (class_exists(Router::class)) { - $options['className'] = CakeRouterUrlChecker::class; + $options['className'] = CakeUrlChecker::class; } else { $options['className'] = DefaultUrlChecker::class; } diff --git a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php index a5e6e9b5..e9ede2c6 100644 --- a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php @@ -260,6 +260,7 @@ public function testMultipleLoginUrlMismatch() ->connect('/{lang}/secure', ['controller' => 'Users', 'action' => 'login']); $envAuth = new EnvironmentAuthenticator($this->identifier, [ + 'urlChecker' => 'Authentication.Multi', 'loginUrl' => [ ['lang' => 'en', 'controller' => 'Users', 'action' => 'login'], ['lang' => 'de', 'controller' => 'Users', 'action' => 'login'], @@ -316,6 +317,7 @@ public function testSingleLoginUrlSuccess() public function testMultipleLoginUrlSuccess() { $envAuth = new EnvironmentAuthenticator($this->identifier, [ + 'urlChecker' => 'Authentication.Multi', 'loginUrl' => [ '/en/secure', '/de/secure', diff --git a/tests/TestCase/Authenticator/FormAuthenticatorTest.php b/tests/TestCase/Authenticator/FormAuthenticatorTest.php index db14ab42..2f3e06c3 100644 --- a/tests/TestCase/Authenticator/FormAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/FormAuthenticatorTest.php @@ -171,7 +171,7 @@ public function testMultipleLoginUrlMismatch() ->connect('/{lang}/users/login', ['controller' => 'Users', 'action' => 'login']); $form = new FormAuthenticator($identifier, [ - 'urlChecker' => 'Authentication.CakeRouter', + 'urlChecker' => 'Authentication.Multi', 'loginUrl' => [ ['lang' => 'en', 'controller' => 'Users', 'action' => 'login'], ['lang' => 'de', 'controller' => 'Users', 'action' => 'login'], @@ -258,6 +258,7 @@ public function testMultipleLoginUrlSuccess() '/en/users/login', '/de/users/login', ], + 'urlChecker' => 'Authentication.Multi', ]); $result = $form->authenticate($request); diff --git a/tests/TestCase/UrlChecker/CakeRouterUrlCheckerTest.php b/tests/TestCase/UrlChecker/CakeUrlCheckerTest.php similarity index 65% rename from tests/TestCase/UrlChecker/CakeRouterUrlCheckerTest.php rename to tests/TestCase/UrlChecker/CakeUrlCheckerTest.php index f8ec480e..b2f1220f 100644 --- a/tests/TestCase/UrlChecker/CakeRouterUrlCheckerTest.php +++ b/tests/TestCase/UrlChecker/CakeUrlCheckerTest.php @@ -17,14 +17,14 @@ namespace Authentication\Test\TestCase\UrlChecker; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; -use Authentication\UrlChecker\CakeRouterUrlChecker; +use Authentication\UrlChecker\CakeUrlChecker; use Cake\Http\ServerRequestFactory; use Cake\Routing\Router; /** - * CakeRouterChecker + * CakeUrlChecker Test */ -class CakeRouterUrlCheckerTest extends TestCase +class CakeUrlCheckerTest extends TestCase { /** * @inheritDoc @@ -59,7 +59,7 @@ public function setUp(): void */ public function testCheckSimple() { - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/invalid'], ); @@ -82,7 +82,7 @@ public function testCheckFullUrls() 'action' => 'login', ]; - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], ); @@ -91,7 +91,7 @@ public function testCheckFullUrls() ]); $this->assertTrue($result); - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/invalid'], ); @@ -100,7 +100,7 @@ public function testCheckFullUrls() ]); $this->assertFalse($result); - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/login'], ); @@ -109,7 +109,7 @@ public function testCheckFullUrls() ]); $this->assertFalse($result); - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( [ 'REQUEST_URI' => '/login', @@ -123,34 +123,21 @@ public function testCheckFullUrls() } /** - * testEmptyUrl + * testStringUrl - CakeUrlChecker now accepts strings too * * @return void */ - public function testEmptyUrl() + public function testStringUrl() { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('The $loginUrls parameter is empty or not of type array.'); - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], ); - $result = $checker->check($request, []); - $this->assertFalse($result); - } + $result = $checker->check($request, '/users/login'); + $this->assertTrue($result); - /** - * testEmptyUrl - * - * @return void - */ - public function testStringUrl() - { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('The $loginUrls parameter is empty or not of type array.'); - $checker = new CakeRouterUrlChecker(); $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], + ['REQUEST_URI' => '/different/url'], ); $result = $checker->check($request, '/users/login'); $this->assertFalse($result); @@ -163,7 +150,7 @@ public function testStringUrl() */ public function testNamedRoute() { - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/login'], ); @@ -177,54 +164,10 @@ public function testNamedRoute() public function testInvalidNamedRoute() { $this->expectException('Cake\Routing\Exception\MissingRouteException'); - $checker = new CakeRouterUrlChecker(); + $checker = new CakeUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/login'], ); $checker->check($request, ['_name' => 'login-does-not-exist']); } - - /** - * testMultipleUrls - * - * @return void - */ - public function testMultipleUrls() - { - $url = [ - [ - 'controller' => 'users', - 'action' => 'login', - ], - [ - 'controller' => 'admins', - 'action' => 'login', - ], - ]; - - $checker = new CakeRouterUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], - ); - $result = $checker->check($request, $url, [ - 'checkFullUrl' => true, - ]); - $this->assertTrue($result); - - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/admins/login'], - ); - $result = $checker->check($request, $url, [ - 'checkFullUrl' => true, - ]); - $this->assertTrue($result); - - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/invalid'], - ); - $result = $checker->check($request, $url, [ - 'checkFullUrl' => true, - ]); - $this->assertFalse($result); - } } diff --git a/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php b/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php index e0a4e4cb..defc5684 100644 --- a/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php +++ b/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php @@ -56,30 +56,8 @@ public function testCheckSimple() $result = $checker->check($request, '/users/login'); $this->assertTrue($result); - $result = $checker->check($request, [ - '/users/login', - '/admin/login', - ]); - $this->assertTrue($result); - } - - /** - * testCheckArray - * - * @return void - */ - public function testCheckArray() - { - $checker = new DefaultUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], - ); - - $result = $checker->check($request, [ - '/users/login', - '/admin/login', - ]); - $this->assertTrue($result); + $result = $checker->check($request, '/different/url'); + $this->assertFalse($result); } /** From 24acd57f5d8429693f48cecd63d19734da3efc08 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 6 Nov 2025 07:50:39 +0100 Subject: [PATCH 03/30] Fix up docs. --- docs/en/url-checkers.rst | 98 +++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 16 deletions(-) diff --git a/docs/en/url-checkers.rst b/docs/en/url-checkers.rst index f27a8532..d0d75430 100644 --- a/docs/en/url-checkers.rst +++ b/docs/en/url-checkers.rst @@ -5,47 +5,113 @@ To provide an abstract and framework agnostic solution there are URL checkers implemented that allow you to customize the comparison of the current URL if needed. For example to another frameworks routing. +All checkers support single URLs in either string or array format (like ``Router::url()``). +For multiple login URLs, use ``MultiUrlChecker``. + Included Checkers ================= +CakeUrlChecker +-------------- + +The default checker when CakePHP is installed. Supports both string URLs and +CakePHP's array-based routing notation. This checker also works with named routes. + +Single URL (string): + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => '/users/login', + ]); + +Single URL (CakePHP route array): + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => [ + 'prefix' => false, + 'plugin' => false, + 'controller' => 'Users', + 'action' => 'login', + ], + ]); + +Options: + +- **checkFullUrl**: To compare the full URL, including protocol, host + and port or not. Default is ``false`` + DefaultUrlChecker ----------------- -The default checker allows you to compare an URL by regex or string -URLs. +Framework-agnostic checker for string URLs. Supports regex matching. +This is the default when CakePHP is not installed. + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Default', + 'loginUrl' => '/users/login', + ]); + +Using regex: + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => [ + 'className' => 'Authentication.Default', + 'useRegex' => true, + ], + 'loginUrl' => '%^/[a-z]{2}/users/login/?$%', + ]); Options: - **checkFullUrl**: To compare the full URL, including protocol, host and port or not. Default is ``false`` - **useRegex**: Compares the URL by a regular expression provided in - the ``$loginUrls`` argument of the checker. + the ``loginUrl`` configuration. + +MultiUrlChecker +--------------- + +Use this checker when you need to support multiple login URLs (e.g., for multi-language sites). +You must explicitly configure this checker - it is not auto-detected. -CakeRouterUrlChecker --------------------- +Multiple string URLs: -Use this checker if you want to use the array notation of CakePHPs -routing system. The checker also works with named routes. +.. code-block:: php $service->loadAuthenticator('Authentication.Form', [ - 'urlChecker' => 'Authentication.CakeRouter', - 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'email', - AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + 'urlChecker' => 'Authentication.Multi', + 'loginUrl' => [ + '/en/users/login', + '/de/users/login', ], + ]); + +Multiple CakePHP route arrays: + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Multi', 'loginUrl' => [ - 'prefix' => false, - 'plugin' => false, - 'controller' => 'Users', - 'action' => 'login', + ['lang' => 'en', 'controller' => 'Users', 'action' => 'login'], + ['lang' => 'de', 'controller' => 'Users', 'action' => 'login'], ], ]); Options: + - **checkFullUrl**: To compare the full URL, including protocol, host and port or not. Default is ``false`` +- **useRegex**: Compares URLs by regular expressions. Default is ``false`` Implementing your own Checker ----------------------------- -An URL checker **must** implement the ``UrlCheckerInterface``. +An URL checkers **must** implement the ``UrlCheckerInterface``. From 5b62daf4445464a939385360813045c8d7376df0 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 6 Nov 2025 08:03:17 +0100 Subject: [PATCH 04/30] Fix up docs. --- docs/en/upgrade-3-to-4.rst | 283 +++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 docs/en/upgrade-3-to-4.rst diff --git a/docs/en/upgrade-3-to-4.rst b/docs/en/upgrade-3-to-4.rst new file mode 100644 index 00000000..c4f75c79 --- /dev/null +++ b/docs/en/upgrade-3-to-4.rst @@ -0,0 +1,283 @@ +Upgrade Guide 3.x to 4.x +######################### + +Version 4.0 is a major release with several breaking changes focused on +simplifying the API and removing deprecated code. + +Breaking Changes +================ + +IdentifierCollection Removed +----------------------------- + +The deprecated ``IdentifierCollection`` has been removed. Authenticators now +accept a nullable ``IdentifierInterface`` directly. + +**Before (3.x):** + +.. code-block:: php + + use Authentication\Identifier\IdentifierCollection; + + $identifiers = new IdentifierCollection([ + 'Authentication.Password', + ]); + + $authenticator = new FormAuthenticator($identifiers); + +**After (4.x):** + +.. code-block:: php + + use Authentication\Identifier\IdentifierFactory; + + // Option 1: Pass identifier directly + $identifier = IdentifierFactory::create('Authentication.Password'); + $authenticator = new FormAuthenticator($identifier); + + // Option 2: Pass null and let authenticator create default + $authenticator = new FormAuthenticator(null); + + // Option 3: Configure identifier in authenticator config + $service->loadAuthenticator('Authentication.Form', [ + 'identifier' => 'Authentication.Password', + ]); + +AuthenticationService Changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``loadIdentifier()`` method has been removed from ``AuthenticationService``. +Identifiers are now managed by individual authenticators. + +**Before (3.x):** + +.. code-block:: php + + $service = new AuthenticationService(); + $service->loadIdentifier('Authentication.Password'); + $service->loadAuthenticator('Authentication.Form'); + +**After (4.x):** + +.. code-block:: php + + $service = new AuthenticationService(); + $service->loadAuthenticator('Authentication.Form', [ + 'identifier' => 'Authentication.Password', + ]); + +CREDENTIAL Constants Moved +--------------------------- + +The ``CREDENTIAL_USERNAME`` and ``CREDENTIAL_PASSWORD`` constants have been +moved from ``AbstractIdentifier`` to specific identifier implementations. + +**Before (3.x):** + +.. code-block:: php + + use Authentication\Identifier\AbstractIdentifier; + + $fields = [ + AbstractIdentifier::CREDENTIAL_USERNAME => 'email', + AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + ]; + +**After (4.x):** + +.. code-block:: php + + use Authentication\Identifier\PasswordIdentifier; + + $fields = [ + PasswordIdentifier::CREDENTIAL_USERNAME => 'email', + PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', + ]; + +For LDAP authentication: + +.. code-block:: php + + use Authentication\Identifier\LdapIdentifier; + + $fields = [ + LdapIdentifier::CREDENTIAL_USERNAME => 'uid', + LdapIdentifier::CREDENTIAL_PASSWORD => 'password', + ]; + +URL Checker Renamed +------------------- + +``CakeRouterUrlChecker`` has been renamed to ``CakeUrlChecker`` and now accepts +both string and array URLs (just like ``Router::url()``). + +**Before (3.x):** + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.CakeRouter', + 'loginUrl' => [ + 'controller' => 'Users', + 'action' => 'login', + ], + ]); + +**After (4.x):** + +.. code-block:: php + + // CakeUrlChecker is now the default when CakePHP is installed + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => [ + 'controller' => 'Users', + 'action' => 'login', + ], + ]); + + // Or explicitly: + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Cake', + 'loginUrl' => [ + 'controller' => 'Users', + 'action' => 'login', + ], + ]); + +Simplified URL Checker API +--------------------------- + +URL checkers now accept a single URL in either string or array format. +For multiple URLs, you must explicitly use ``MultiUrlChecker``. + +**Multiple URLs - Before (3.x):** + +.. code-block:: php + + // This would auto-select the appropriate checker + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => [ + '/en/users/login', + '/de/users/login', + ], + ]); + +**Multiple URLs - After (4.x):** + +.. code-block:: php + + // Must explicitly configure MultiUrlChecker + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Multi', + 'loginUrl' => [ + '/en/users/login', + '/de/users/login', + ], + ]); + +Single URLs work the same in both versions: + +.. code-block:: php + + // String URL + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => '/users/login', + ]); + + // Array URL (CakePHP route) + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => ['controller' => 'Users', 'action' => 'login'], + ]); + +Auto-Detection Changes +---------------------- + +URL Checkers +^^^^^^^^^^^^ + +- When CakePHP Router is available: defaults to ``CakeUrlChecker`` +- Without CakePHP: defaults to ``DefaultUrlChecker`` +- For multiple URLs: you **must** explicitly configure ``MultiUrlChecker`` + +DefaultUrlChecker Changes +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``DefaultUrlChecker`` no longer accepts array-based URLs. It throws a +``RuntimeException`` if an array URL is provided: + +.. code-block:: php + + // This will throw an exception in 4.x + $checker = new DefaultUrlChecker(); + $checker->check($request, ['controller' => 'Users', 'action' => 'login']); + + // Use CakeUrlChecker instead: + $checker = new CakeUrlChecker(); + $checker->check($request, ['controller' => 'Users', 'action' => 'login']); + +New Features +============ + +IdentifierFactory +----------------- + +New factory class for creating identifiers from configuration: + +.. code-block:: php + + use Authentication\Identifier\IdentifierFactory; + + // Create from string + $identifier = IdentifierFactory::create('Authentication.Password'); + + // Create with config + $identifier = IdentifierFactory::create('Authentication.Password', [ + 'fields' => [ + 'username' => 'email', + 'password' => 'password', + ], + ]); + + // Pass existing instance (returns as-is) + $identifier = IdentifierFactory::create($existingIdentifier); + +MultiUrlChecker +--------------- + +New dedicated checker for multiple login URLs: + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Multi', + 'loginUrl' => [ + '/en/login', + '/de/login', + ['lang' => 'fr', 'controller' => 'Users', 'action' => 'login'], + ], + ]); + +Migration Tips +============== + +1. **Search and Replace**: + + - ``AbstractIdentifier::CREDENTIAL_`` → ``PasswordIdentifier::CREDENTIAL_`` + - ``IdentifierCollection`` → ``IdentifierFactory`` + - ``'Authentication.CakeRouter'`` → ``'Authentication.Cake'`` + - ``CakeRouterUrlChecker`` → ``CakeUrlChecker`` + +2. **Multiple Login URLs**: + + If you have multiple login URLs, add ``'urlChecker' => 'Authentication.Multi'`` + to your authenticator configuration. + +3. **Custom Identifier Setup**: + + If you were passing ``IdentifierCollection`` to authenticators, switch to + either passing a single identifier or null (to use defaults). + +4. **Test Thoroughly**: + + The changes to identifier management and URL checking are significant. + Test all authentication flows after upgrading. From 242b12a15d1f96fe18b8caf1e0cddf1e98755fee Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 6 Nov 2025 16:51:58 +0100 Subject: [PATCH 05/30] Fix up identifier defaulting. --- src/Authenticator/AbstractAuthenticator.php | 19 ++++++++++++---- src/Authenticator/CookieAuthenticator.php | 1 - .../EnvironmentAuthenticator.php | 18 ++++++++++++++- src/Authenticator/FormAuthenticator.php | 1 - src/Authenticator/HttpBasicAuthenticator.php | 1 - src/Authenticator/HttpDigestAuthenticator.php | 1 - src/Authenticator/JwtAuthenticator.php | 4 +--- .../PrimaryKeySessionAuthenticator.php | 1 - src/Authenticator/SessionAuthenticator.php | 22 ++++++++++++++++++- src/Authenticator/TokenAuthenticator.php | 1 - 10 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/Authenticator/AbstractAuthenticator.php b/src/Authenticator/AbstractAuthenticator.php index 9d54a50c..06c2287d 100644 --- a/src/Authenticator/AbstractAuthenticator.php +++ b/src/Authenticator/AbstractAuthenticator.php @@ -20,6 +20,7 @@ use Authentication\Identifier\PasswordIdentifier; use Cake\Core\InstanceConfigTrait; use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; abstract class AbstractAuthenticator implements AuthenticatorInterface { @@ -41,18 +42,28 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface /** * Identifier instance. * - * @var \Authentication\Identifier\IdentifierInterface|null + * @var \Authentication\Identifier\IdentifierInterface */ - protected ?IdentifierInterface $_identifier = null; + protected IdentifierInterface $_identifier; /** * Constructor * * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. + * @throws \RuntimeException When identifier is null and the authenticator doesn't provide a default. */ public function __construct(?IdentifierInterface $identifier, array $config = []) { + if ($identifier === null) { + throw new RuntimeException( + sprintf( + 'Identifier is required for `%s`. Please provide an identifier instance.', + static::class, + ), + ); + } + $this->_identifier = $identifier; $this->setConfig($config); } @@ -60,9 +71,9 @@ public function __construct(?IdentifierInterface $identifier, array $config = [] /** * Gets the identifier. * - * @return \Authentication\Identifier\IdentifierInterface|null + * @return \Authentication\Identifier\IdentifierInterface */ - public function getIdentifier(): ?IdentifierInterface + public function getIdentifier(): IdentifierInterface { return $this->_identifier; } diff --git a/src/Authenticator/CookieAuthenticator.php b/src/Authenticator/CookieAuthenticator.php index ea218cdf..53b35de9 100644 --- a/src/Authenticator/CookieAuthenticator.php +++ b/src/Authenticator/CookieAuthenticator.php @@ -105,7 +105,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface [$username, $tokenHash] = $token; - assert($this->_identifier !== null); $identity = $this->_identifier->identify(compact('username')); if (!$identity) { diff --git a/src/Authenticator/EnvironmentAuthenticator.php b/src/Authenticator/EnvironmentAuthenticator.php index 3cb95393..c66a806e 100644 --- a/src/Authenticator/EnvironmentAuthenticator.php +++ b/src/Authenticator/EnvironmentAuthenticator.php @@ -16,6 +16,8 @@ */ namespace Authentication\Authenticator; +use Authentication\Identifier\IdentifierFactory; +use Authentication\Identifier\IdentifierInterface; use Authentication\UrlChecker\UrlCheckerTrait; use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; @@ -45,6 +47,21 @@ class EnvironmentAuthenticator extends AbstractAuthenticator 'optionalFields' => [], ]; + /** + * Constructor + * + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. + * @param array $config Configuration settings. + */ + public function __construct(?IdentifierInterface $identifier, array $config = []) + { + if ($identifier === null) { + $identifier = IdentifierFactory::create('Authentication.Callback'); + } + + parent::__construct($identifier, $config); + } + /** * Get values from the environment variables configured by `fields`. * @@ -153,7 +170,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface $data = array_merge($this->_getOptionalData($request), $data); - assert($this->_identifier !== null); $user = $this->_identifier->identify($data); if (!$user) { diff --git a/src/Authenticator/FormAuthenticator.php b/src/Authenticator/FormAuthenticator.php index 6292377b..7167f1fc 100644 --- a/src/Authenticator/FormAuthenticator.php +++ b/src/Authenticator/FormAuthenticator.php @@ -159,7 +159,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface ]); } - assert($this->_identifier !== null); $user = $this->_identifier->identify($data); if (!$user) { diff --git a/src/Authenticator/HttpBasicAuthenticator.php b/src/Authenticator/HttpBasicAuthenticator.php index 1a5ea6a9..ba9e0a76 100644 --- a/src/Authenticator/HttpBasicAuthenticator.php +++ b/src/Authenticator/HttpBasicAuthenticator.php @@ -81,7 +81,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); } - assert($this->_identifier !== null); $user = $this->_identifier->identify([ PasswordIdentifier::CREDENTIAL_USERNAME => $username, PasswordIdentifier::CREDENTIAL_PASSWORD => $password, diff --git a/src/Authenticator/HttpDigestAuthenticator.php b/src/Authenticator/HttpDigestAuthenticator.php index a6153959..e344e969 100644 --- a/src/Authenticator/HttpDigestAuthenticator.php +++ b/src/Authenticator/HttpDigestAuthenticator.php @@ -94,7 +94,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); } - assert($this->_identifier !== null); $user = $this->_identifier->identify([ PasswordIdentifier::CREDENTIAL_USERNAME => $digest['username'], ]); diff --git a/src/Authenticator/JwtAuthenticator.php b/src/Authenticator/JwtAuthenticator.php index ad2ed150..2b4f4643 100644 --- a/src/Authenticator/JwtAuthenticator.php +++ b/src/Authenticator/JwtAuthenticator.php @@ -99,8 +99,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); } - /** @phpstan-ignore-next-line */ - $result = json_decode(json_encode($result), true); + $result = json_decode((string)json_encode($result), true); $subjectKey = $this->getConfig('subjectKey'); if (empty($result[$subjectKey])) { @@ -113,7 +112,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result($user, Result::SUCCESS); } - assert($this->_identifier !== null); $user = $this->_identifier->identify([ $subjectKey => $result[$subjectKey], ]); diff --git a/src/Authenticator/PrimaryKeySessionAuthenticator.php b/src/Authenticator/PrimaryKeySessionAuthenticator.php index 3f379424..aacb3fa1 100644 --- a/src/Authenticator/PrimaryKeySessionAuthenticator.php +++ b/src/Authenticator/PrimaryKeySessionAuthenticator.php @@ -45,7 +45,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); } - assert($this->_identifier !== null); $user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]); if (!$user) { return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); diff --git a/src/Authenticator/SessionAuthenticator.php b/src/Authenticator/SessionAuthenticator.php index 88f8e0e0..3814ec09 100644 --- a/src/Authenticator/SessionAuthenticator.php +++ b/src/Authenticator/SessionAuthenticator.php @@ -17,6 +17,8 @@ use ArrayAccess; use ArrayObject; +use Authentication\Identifier\IdentifierFactory; +use Authentication\Identifier\IdentifierInterface; use Authentication\Identifier\PasswordIdentifier; use Cake\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; @@ -47,6 +49,25 @@ class SessionAuthenticator extends AbstractAuthenticator implements PersistenceI 'identityAttribute' => 'identity', ]; + /** + * Constructor + * + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. + * @param array $config Configuration settings. + */ + public function __construct(?IdentifierInterface $identifier, array $config = []) + { + if ($identifier === null) { + $identifierConfig = []; + if (isset($config['fields'])) { + $identifierConfig['fields'] = $config['fields']; + } + $identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); + } + + parent::__construct($identifier, $config); + } + /** * Authenticate a user using session data. * @@ -69,7 +90,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface foreach ($this->getConfig('fields') as $key => $field) { $credentials[$key] = $user[$field]; } - assert($this->_identifier !== null); $user = $this->_identifier->identify($credentials); if (!$user) { diff --git a/src/Authenticator/TokenAuthenticator.php b/src/Authenticator/TokenAuthenticator.php index 217c0d99..ef67cacc 100644 --- a/src/Authenticator/TokenAuthenticator.php +++ b/src/Authenticator/TokenAuthenticator.php @@ -142,7 +142,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); } - assert($this->_identifier !== null); $user = $this->_identifier->identify([ TokenIdentifier::CREDENTIAL_TOKEN => $token, ]); From f6dab98f154e2786d9012ea519f2b523e7e45441 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 6 Nov 2025 18:27:05 +0100 Subject: [PATCH 06/30] Update src/Authenticator/EnvironmentAuthenticator.php Co-authored-by: ADmad --- src/Authenticator/EnvironmentAuthenticator.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Authenticator/EnvironmentAuthenticator.php b/src/Authenticator/EnvironmentAuthenticator.php index c66a806e..7c8bb653 100644 --- a/src/Authenticator/EnvironmentAuthenticator.php +++ b/src/Authenticator/EnvironmentAuthenticator.php @@ -55,9 +55,7 @@ class EnvironmentAuthenticator extends AbstractAuthenticator */ public function __construct(?IdentifierInterface $identifier, array $config = []) { - if ($identifier === null) { - $identifier = IdentifierFactory::create('Authentication.Callback'); - } + $identifier ??= IdentifierFactory::create('Authentication.Callback'); parent::__construct($identifier, $config); } From 64801132005255add51723f8d604cc220e8437d6 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 6 Nov 2025 18:27:12 +0100 Subject: [PATCH 07/30] Update src/Authenticator/JwtAuthenticator.php Co-authored-by: ADmad --- src/Authenticator/JwtAuthenticator.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Authenticator/JwtAuthenticator.php b/src/Authenticator/JwtAuthenticator.php index 2b4f4643..2a91c8d6 100644 --- a/src/Authenticator/JwtAuthenticator.php +++ b/src/Authenticator/JwtAuthenticator.php @@ -58,9 +58,7 @@ class JwtAuthenticator extends TokenAuthenticator public function __construct(?IdentifierInterface $identifier, array $config = []) { // Override parent's default - JWT should use JwtSubject identifier - if ($identifier === null) { - $identifier = IdentifierFactory::create('Authentication.JwtSubject'); - } + $identifier ??= IdentifierFactory::create('Authentication.JwtSubject'); // Call AbstractAuthenticator's constructor directly to skip parent's default AbstractAuthenticator::__construct($identifier, $config); From d5ee52b4da9673662f9b78bc2c8b7dfb10d4de0c Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 6 Nov 2025 18:27:18 +0100 Subject: [PATCH 08/30] Update src/Authenticator/TokenAuthenticator.php Co-authored-by: ADmad --- src/Authenticator/TokenAuthenticator.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Authenticator/TokenAuthenticator.php b/src/Authenticator/TokenAuthenticator.php index ef67cacc..541a3194 100644 --- a/src/Authenticator/TokenAuthenticator.php +++ b/src/Authenticator/TokenAuthenticator.php @@ -46,9 +46,7 @@ class TokenAuthenticator extends AbstractAuthenticator implements StatelessInter public function __construct(?IdentifierInterface $identifier, array $config = []) { // If no identifier is configured, set up a default Token identifier - if ($identifier === null) { - $identifier = IdentifierFactory::create('Authentication.Token'); - } + $identifier ??= IdentifierFactory::create('Authentication.Token'); parent::__construct($identifier, $config); } From 4325675e9ff9b2d63d3eae894b0a93fab3bdd66e Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 7 Nov 2025 09:55:58 +0100 Subject: [PATCH 09/30] Fix up Url Checker defaulting. --- docs/en/upgrade-3-to-4.rst | 90 ++++++--- docs/en/url-checkers.rst | 20 +- src/Authenticator/FormAuthenticator.php | 2 +- src/UrlChecker/CakeUrlChecker.php | 51 ------ src/UrlChecker/DefaultUrlChecker.php | 42 +---- src/UrlChecker/GenericUrlChecker.php | 113 ++++++++++++ src/UrlChecker/MultiUrlChecker.php | 12 +- src/UrlChecker/UrlCheckerTrait.php | 9 +- .../EnvironmentAuthenticatorTest.php | 6 +- .../Authenticator/FormAuthenticatorTest.php | 9 +- .../UrlChecker/CakeUrlCheckerTest.php | 173 ------------------ .../UrlChecker/DefaultUrlCheckerTest.php | 126 +++++++++---- .../UrlChecker/GenericUrlCheckerTest.php | 117 ++++++++++++ 13 files changed, 412 insertions(+), 358 deletions(-) delete mode 100644 src/UrlChecker/CakeUrlChecker.php create mode 100644 src/UrlChecker/GenericUrlChecker.php delete mode 100644 tests/TestCase/UrlChecker/CakeUrlCheckerTest.php create mode 100644 tests/TestCase/UrlChecker/GenericUrlCheckerTest.php diff --git a/docs/en/upgrade-3-to-4.rst b/docs/en/upgrade-3-to-4.rst index c4f75c79..b465eb9c 100644 --- a/docs/en/upgrade-3-to-4.rst +++ b/docs/en/upgrade-3-to-4.rst @@ -105,16 +105,20 @@ For LDAP authentication: LdapIdentifier::CREDENTIAL_PASSWORD => 'password', ]; -URL Checker Renamed -------------------- +URL Checker Renamed and Restructured +------------------------------------- -``CakeRouterUrlChecker`` has been renamed to ``CakeUrlChecker`` and now accepts -both string and array URLs (just like ``Router::url()``). +URL checkers have been completely restructured: + +- ``CakeRouterUrlChecker`` has been renamed to ``DefaultUrlChecker`` +- The old ``DefaultUrlChecker`` (framework-agnostic) has been renamed to ``GenericUrlChecker`` +- Auto-detection has been removed - ``DefaultUrlChecker`` is now hardcoded **Before (3.x):** .. code-block:: php + // Using CakeRouterUrlChecker explicitly $service->loadAuthenticator('Authentication.Form', [ 'urlChecker' => 'Authentication.CakeRouter', 'loginUrl' => [ @@ -123,11 +127,22 @@ both string and array URLs (just like ``Router::url()``). ], ]); + // Using DefaultUrlChecker explicitly (framework-agnostic) + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Default', + 'loginUrl' => '/users/login', + ]); + + // Auto-detection (picks CakeRouter if available, otherwise Default) + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => '/users/login', + ]); + **After (4.x):** .. code-block:: php - // CakeUrlChecker is now the default when CakePHP is installed + // DefaultUrlChecker is now hardcoded (formerly CakeRouterUrlChecker) $service->loadAuthenticator('Authentication.Form', [ 'loginUrl' => [ 'controller' => 'Users', @@ -135,13 +150,10 @@ both string and array URLs (just like ``Router::url()``). ], ]); - // Or explicitly: + // For framework-agnostic projects, explicitly use GenericUrlChecker $service->loadAuthenticator('Authentication.Form', [ - 'urlChecker' => 'Authentication.Cake', - 'loginUrl' => [ - 'controller' => 'Users', - 'action' => 'login', - ], + 'urlChecker' => 'Authentication.Generic', + 'loginUrl' => '/users/login', ]); Simplified URL Checker API @@ -189,31 +201,38 @@ Single URLs work the same in both versions: 'loginUrl' => ['controller' => 'Users', 'action' => 'login'], ]); -Auto-Detection Changes +Auto-Detection Removed ---------------------- URL Checkers ^^^^^^^^^^^^ -- When CakePHP Router is available: defaults to ``CakeUrlChecker`` -- Without CakePHP: defaults to ``DefaultUrlChecker`` -- For multiple URLs: you **must** explicitly configure ``MultiUrlChecker`` +**Important:** Auto-detection has been removed. ``DefaultUrlChecker`` is now hardcoded +and assumes CakePHP is available. + +- **4.x default:** Always uses ``DefaultUrlChecker`` (formerly ``CakeUrlChecker``) +- **Framework-agnostic:** Must explicitly configure ``GenericUrlChecker`` +- **Multiple URLs:** Must explicitly configure ``MultiUrlChecker`` -DefaultUrlChecker Changes -^^^^^^^^^^^^^^^^^^^^^^^^^ +DefaultUrlChecker is Now CakePHP-Based +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -``DefaultUrlChecker`` no longer accepts array-based URLs. It throws a -``RuntimeException`` if an array URL is provided: +``DefaultUrlChecker`` is now the CakePHP checker (formerly ``CakeRouterUrlChecker``). +It requires CakePHP Router and supports both string and array URLs. + +The 3.x framework-agnostic ``DefaultUrlChecker`` has been renamed to ``GenericUrlChecker``. .. code-block:: php - // This will throw an exception in 4.x + // DefaultUrlChecker now requires CakePHP Router $checker = new DefaultUrlChecker(); - $checker->check($request, ['controller' => 'Users', 'action' => 'login']); + $checker->check($request, ['controller' => 'Users', 'action' => 'login']); // Works + $checker->check($request, '/users/login'); // Also works - // Use CakeUrlChecker instead: - $checker = new CakeUrlChecker(); - $checker->check($request, ['controller' => 'Users', 'action' => 'login']); + // For framework-agnostic usage: + $checker = new GenericUrlChecker(); + $checker->check($request, '/users/login'); // Works + $checker->check($request, ['controller' => 'Users']); // Throws exception New Features ============ @@ -264,20 +283,33 @@ Migration Tips - ``AbstractIdentifier::CREDENTIAL_`` → ``PasswordIdentifier::CREDENTIAL_`` - ``IdentifierCollection`` → ``IdentifierFactory`` - - ``'Authentication.CakeRouter'`` → ``'Authentication.Cake'`` - - ``CakeRouterUrlChecker`` → ``CakeUrlChecker`` + - ``'Authentication.CakeRouter'`` → Remove (no longer needed, default is now CakePHP-based) + - ``CakeRouterUrlChecker`` → ``DefaultUrlChecker`` + - Old 3.x ``DefaultUrlChecker`` (framework-agnostic) → ``GenericUrlChecker`` + +2. **Framework-Agnostic Projects**: + + If you're using this library without CakePHP, you **must** explicitly configure + ``GenericUrlChecker``: + + .. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Generic', + 'loginUrl' => '/users/login', + ]); -2. **Multiple Login URLs**: +3. **Multiple Login URLs**: If you have multiple login URLs, add ``'urlChecker' => 'Authentication.Multi'`` to your authenticator configuration. -3. **Custom Identifier Setup**: +4. **Custom Identifier Setup**: If you were passing ``IdentifierCollection`` to authenticators, switch to either passing a single identifier or null (to use defaults). -4. **Test Thoroughly**: +5. **Test Thoroughly**: The changes to identifier management and URL checking are significant. Test all authentication flows after upgrading. diff --git a/docs/en/url-checkers.rst b/docs/en/url-checkers.rst index d0d75430..6e7db9db 100644 --- a/docs/en/url-checkers.rst +++ b/docs/en/url-checkers.rst @@ -11,11 +11,11 @@ For multiple login URLs, use ``MultiUrlChecker``. Included Checkers ================= -CakeUrlChecker --------------- +DefaultUrlChecker +----------------- -The default checker when CakePHP is installed. Supports both string URLs and -CakePHP's array-based routing notation. This checker also works with named routes. +The default URL checker. Supports both string URLs and CakePHP's array-based +routing notation. Uses CakePHP Router and works with named routes. Single URL (string): @@ -41,18 +41,18 @@ Single URL (CakePHP route array): Options: - **checkFullUrl**: To compare the full URL, including protocol, host - and port or not. Default is ``false`` + and port or not. Default is ``false``. -DefaultUrlChecker ------------------ +GenericUrlChecker +------------------ Framework-agnostic checker for string URLs. Supports regex matching. -This is the default when CakePHP is not installed. +Use this for non-CakePHP projects. .. code-block:: php $service->loadAuthenticator('Authentication.Form', [ - 'urlChecker' => 'Authentication.Default', + 'urlChecker' => 'Authentication.Generic', 'loginUrl' => '/users/login', ]); @@ -62,7 +62,7 @@ Using regex: $service->loadAuthenticator('Authentication.Form', [ 'urlChecker' => [ - 'className' => 'Authentication.Default', + 'className' => 'Authentication.Generic', 'useRegex' => true, ], 'loginUrl' => '%^/[a-z]{2}/users/login/?$%', diff --git a/src/Authenticator/FormAuthenticator.php b/src/Authenticator/FormAuthenticator.php index 7167f1fc..f1b73dd3 100644 --- a/src/Authenticator/FormAuthenticator.php +++ b/src/Authenticator/FormAuthenticator.php @@ -42,7 +42,7 @@ class FormAuthenticator extends AbstractAuthenticator */ protected array $_defaultConfig = [ 'loginUrl' => null, - 'urlChecker' => 'Authentication.Cake', + 'urlChecker' => null, 'fields' => [ PasswordIdentifier::CREDENTIAL_USERNAME => 'username', PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', diff --git a/src/UrlChecker/CakeUrlChecker.php b/src/UrlChecker/CakeUrlChecker.php deleted file mode 100644 index 8506a392..00000000 --- a/src/UrlChecker/CakeUrlChecker.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - protected array $_defaultOptions = [ - 'checkFullUrl' => false, - ]; - - /** - * @inheritDoc - */ - public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool - { - $options = $this->_mergeDefaultOptions($options); - $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); - - // Support both string URLs and array-based routes (like Router::url()) - $validUrl = Router::url($loginUrls, $options['checkFullUrl']); - - return $validUrl === $url; - } -} diff --git a/src/UrlChecker/DefaultUrlChecker.php b/src/UrlChecker/DefaultUrlChecker.php index e9eadbf6..97e7dec9 100644 --- a/src/UrlChecker/DefaultUrlChecker.php +++ b/src/UrlChecker/DefaultUrlChecker.php @@ -16,24 +16,22 @@ */ namespace Authentication\UrlChecker; +use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; -use RuntimeException; /** - * Checks if a request object contains a valid URL. Framework agnostic. + * Default URL checker for CakePHP applications. Uses CakePHP Router. */ class DefaultUrlChecker implements UrlCheckerInterface { /** * Default Options * - * - `urlChecker` Whether to use `loginUrl` as regular expression(s). * - `checkFullUrl` Whether to check the full request URI. * * @var array */ protected array $_defaultOptions = [ - 'useRegex' => false, 'checkFullUrl' => false, ]; @@ -42,52 +40,26 @@ class DefaultUrlChecker implements UrlCheckerInterface */ public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool { - if (is_array($loginUrls)) { - throw new RuntimeException( - 'Array-based login URLs require CakePHP Router and CakeUrlChecker. ' . - 'Either install cakephp/cakephp or use string URLs instead.', - ); - } - $options = $this->_mergeDefaultOptions($options); - $checker = $this->_getChecker($options); $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); - return (bool)$checker($loginUrls, $url); + // Support both string URLs and array-based routes (like Router::url()) + $validUrl = Router::url($loginUrls, $options['checkFullUrl']); + + return $validUrl === $url; } /** * Merges given options with the defaults. * - * The reason this method exists is that it makes it easy to override the - * method and inject additional options without the need to use the - * MergeVarsTrait. - * * @param array $options Options to merge in - * @return array + * @return array */ protected function _mergeDefaultOptions(array $options): array { return $options + $this->_defaultOptions; } - /** - * Gets the checker function name or a callback - * - * @param array $options Array of options - * @return callable - */ - protected function _getChecker(array $options): callable - { - if (!empty($options['useRegex'])) { - return 'preg_match'; - } - - return function ($validUrl, $url) { - return $validUrl === $url; - }; - } - /** * Returns current url. * diff --git a/src/UrlChecker/GenericUrlChecker.php b/src/UrlChecker/GenericUrlChecker.php new file mode 100644 index 00000000..e36da18c --- /dev/null +++ b/src/UrlChecker/GenericUrlChecker.php @@ -0,0 +1,113 @@ + + */ + protected array $_defaultOptions = [ + 'useRegex' => false, + 'checkFullUrl' => false, + ]; + + /** + * @inheritDoc + */ + public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool + { + if (is_array($loginUrls)) { + throw new RuntimeException( + 'Array-based login URLs require CakePHP Router and DefaultUrlChecker. ' . + 'Use string URLs instead.', + ); + } + + $options = $this->_mergeDefaultOptions($options); + $checker = $this->_getChecker($options); + $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); + + return (bool)$checker($loginUrls, $url); + } + + /** + * Merges given options with the defaults. + * + * The reason this method exists is that it makes it easy to override the + * method and inject additional options without the need to use the + * MergeVarsTrait. + * + * @param array $options Options to merge in + * @return array + */ + protected function _mergeDefaultOptions(array $options): array + { + return $options + $this->_defaultOptions; + } + + /** + * Gets the checker function name or a callback + * + * @param array $options Array of options + * @return callable + */ + protected function _getChecker(array $options): callable + { + if (!empty($options['useRegex'])) { + return 'preg_match'; + } + + return function ($validUrl, $url) { + return $validUrl === $url; + }; + } + + /** + * Returns current url. + * + * @param \Psr\Http\Message\ServerRequestInterface $request Server Request + * @param bool $getFullUrl Get the full URL or just the path + * @return string + */ + protected function _getUrlFromRequest(ServerRequestInterface $request, bool $getFullUrl = false): string + { + $uri = $request->getUri(); + + $requestBase = $request->getAttribute('base'); + if ($requestBase) { + $uri = $uri->withPath($requestBase . $uri->getPath()); + } + + if ($getFullUrl) { + return (string)$uri; + } + + return $uri->getPath(); + } +} diff --git a/src/UrlChecker/MultiUrlChecker.php b/src/UrlChecker/MultiUrlChecker.php index e0dfe0e0..ea525fdd 100644 --- a/src/UrlChecker/MultiUrlChecker.php +++ b/src/UrlChecker/MultiUrlChecker.php @@ -16,17 +16,13 @@ */ namespace Authentication\UrlChecker; -use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; /** * Multi URL Checker * - * Supports checking multiple login URLs, automatically handling both + * Supports checking multiple login URLs, handling both * string URLs and array-based CakePHP routes. - * - * This checker automatically detects the URL type and uses the appropriate - * checker (Default for strings, Cake for arrays). */ class MultiUrlChecker implements UrlCheckerInterface { @@ -104,12 +100,6 @@ protected function _isSingleRoute(array|string $value): bool */ protected function _checkSingleUrl(ServerRequestInterface $request, array|string $url, array $options): bool { - if (class_exists(Router::class)) { - $checker = new CakeUrlChecker(); - - return $checker->check($request, $url, $options); - } - $checker = new DefaultUrlChecker(); return $checker->check($request, $url, $options); diff --git a/src/UrlChecker/UrlCheckerTrait.php b/src/UrlChecker/UrlCheckerTrait.php index c38d6675..d6d6758f 100644 --- a/src/UrlChecker/UrlCheckerTrait.php +++ b/src/UrlChecker/UrlCheckerTrait.php @@ -17,7 +17,6 @@ namespace Authentication\UrlChecker; use Cake\Core\App; -use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; @@ -61,13 +60,9 @@ protected function _getUrlChecker(): UrlCheckerInterface ]; } - // If no explicit className is set (or it's null/empty), auto-detect CakePHP context + // If no explicit className is set (or it's null/empty), use DefaultUrlChecker if (empty($options['className'])) { - if (class_exists(Router::class)) { - $options['className'] = CakeUrlChecker::class; - } else { - $options['className'] = DefaultUrlChecker::class; - } + $options['className'] = DefaultUrlChecker::class; } $className = App::className($options['className'], 'UrlChecker', 'UrlChecker'); diff --git a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php index e9ede2c6..e4c0fe0d 100644 --- a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php @@ -378,7 +378,7 @@ public function testRegexLoginUrlSuccess() $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%^/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Default', + 'className' => 'Authentication.Generic', 'useRegex' => true, ], 'fields' => [ @@ -409,7 +409,7 @@ public function testFullRegexLoginUrlFailure() $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Default', + 'className' => 'Authentication.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -441,7 +441,7 @@ public function testFullRegexLoginUrlSuccess() $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Default', + 'className' => 'Authentication.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], diff --git a/tests/TestCase/Authenticator/FormAuthenticatorTest.php b/tests/TestCase/Authenticator/FormAuthenticatorTest.php index 2f3e06c3..2f5cd6c9 100644 --- a/tests/TestCase/Authenticator/FormAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/FormAuthenticatorTest.php @@ -313,7 +313,7 @@ public function testRegexLoginUrlSuccess() $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%^/[a-z]{2}/users/login/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Default', + 'className' => 'Authentication.Generic', 'useRegex' => true, ], ]); @@ -345,7 +345,7 @@ public function testFullRegexLoginUrlFailure() $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Default', + 'className' => 'Authentication.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -379,7 +379,7 @@ public function testFullRegexLoginUrlSuccess() $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Default', + 'className' => 'Authentication.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -409,6 +409,9 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() $form = new FormAuthenticator($identifier, [ 'loginUrl' => 'http://localhost/users/login', + 'urlChecker' => [ + 'className' => 'Authentication.Generic', + ], ]); $result = $form->authenticate($request); diff --git a/tests/TestCase/UrlChecker/CakeUrlCheckerTest.php b/tests/TestCase/UrlChecker/CakeUrlCheckerTest.php deleted file mode 100644 index b2f1220f..00000000 --- a/tests/TestCase/UrlChecker/CakeUrlCheckerTest.php +++ /dev/null @@ -1,173 +0,0 @@ -connect( - '/login', - ['controller' => 'Users', 'action' => 'login'], - ['_name' => 'login'], - ); - $builder->connect('/{controller}/{action}'); - $builder->connect( - '/login', - ['controller' => 'Users', 'action' => 'login'], - [ - '_host' => 'auth.localhost', - '_name' => 'secureLogin', - ], - ); - } - - /** - * testCheckSimple - * - * @return void - */ - public function testCheckSimple() - { - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/invalid'], - ); - $result = $checker->check($request, [ - 'controller' => 'Users', - 'action' => 'login', - ]); - $this->assertFalse($result); - } - - /** - * checkFullUrls - * - * @return void - */ - public function testCheckFullUrls() - { - $url = [ - 'controller' => 'users', - 'action' => 'login', - ]; - - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], - ); - $result = $checker->check($request, $url, [ - 'checkFullUrl' => true, - ]); - $this->assertTrue($result); - - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/invalid'], - ); - $result = $checker->check($request, $url, [ - 'checkFullUrl' => true, - ]); - $this->assertFalse($result); - - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/login'], - ); - $result = $checker->check($request, ['_name' => 'secureLogin'], [ - 'checkFullUrl' => true, - ]); - $this->assertFalse($result); - - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - [ - 'REQUEST_URI' => '/login', - 'SERVER_NAME' => 'auth.localhost', - ], - ); - $result = $checker->check($request, ['_name' => 'secureLogin'], [ - 'checkFullUrl' => true, - ]); - $this->assertTrue($result); - } - - /** - * testStringUrl - CakeUrlChecker now accepts strings too - * - * @return void - */ - public function testStringUrl() - { - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], - ); - $result = $checker->check($request, '/users/login'); - $this->assertTrue($result); - - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/different/url'], - ); - $result = $checker->check($request, '/users/login'); - $this->assertFalse($result); - } - - /** - * testNamedRoute - * - * @return void - */ - public function testNamedRoute() - { - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/login'], - ); - $result = $checker->check($request, ['_name' => 'login']); - $this->assertTrue($result); - } - - /** - * testInvalidNamedRoute - */ - public function testInvalidNamedRoute() - { - $this->expectException('Cake\Routing\Exception\MissingRouteException'); - $checker = new CakeUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/login'], - ); - $checker->check($request, ['_name' => 'login-does-not-exist']); - } -} diff --git a/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php b/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php index defc5684..a0be5877 100644 --- a/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php +++ b/tests/TestCase/UrlChecker/DefaultUrlCheckerTest.php @@ -19,27 +19,37 @@ use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Authentication\UrlChecker\DefaultUrlChecker; use Cake\Http\ServerRequestFactory; +use Cake\Routing\Router; /** - * DefaultUrlCheckerTest + * DefaultUrlChecker Test */ class DefaultUrlCheckerTest extends TestCase { /** - * testCheckFailure - * - * @return void + * @inheritDoc */ - public function testCheckFailure() + public function setUp(): void { - $checker = new DefaultUrlChecker(); + parent::setUp(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/does-not-match'], - ); + Router::fullBaseUrl('http://localhost'); - $result = $checker->check($request, '/users/login'); - $this->assertFalse($result); + $builder = Router::createRouteBuilder('/'); + $builder->connect( + '/login', + ['controller' => 'Users', 'action' => 'login'], + ['_name' => 'login'], + ); + $builder->connect('/{controller}/{action}'); + $builder->connect( + '/login', + ['controller' => 'Users', 'action' => 'login'], + [ + '_host' => 'auth.localhost', + '_name' => 'secureLogin', + ], + ); } /** @@ -51,67 +61,113 @@ public function testCheckSimple() { $checker = new DefaultUrlChecker(); $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], + ['REQUEST_URI' => '/users/invalid'], ); - $result = $checker->check($request, '/users/login'); - $this->assertTrue($result); - - $result = $checker->check($request, '/different/url'); + $result = $checker->check($request, [ + 'controller' => 'Users', + 'action' => 'login', + ]); $this->assertFalse($result); } /** - * testCheckRegexp + * checkFullUrls * * @return void */ - public function testCheckRegexp() + public function testCheckFullUrls() { + $url = [ + 'controller' => 'users', + 'action' => 'login', + ]; + + $checker = new DefaultUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/users/login'], + ); + $result = $checker->check($request, $url, [ + 'checkFullUrl' => true, + ]); + $this->assertTrue($result); + + $checker = new DefaultUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/users/invalid'], + ); + $result = $checker->check($request, $url, [ + 'checkFullUrl' => true, + ]); + $this->assertFalse($result); + $checker = new DefaultUrlChecker(); $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/en/users/login'], + ['REQUEST_URI' => '/login'], ); + $result = $checker->check($request, ['_name' => 'secureLogin'], [ + 'checkFullUrl' => true, + ]); + $this->assertFalse($result); - $result = $checker->check($request, '%^/[a-z]{2}/users/login/?$%', [ - 'useRegex' => true, + $checker = new DefaultUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + [ + 'REQUEST_URI' => '/login', + 'SERVER_NAME' => 'auth.localhost', + ], + ); + $result = $checker->check($request, ['_name' => 'secureLogin'], [ + 'checkFullUrl' => true, ]); $this->assertTrue($result); } /** - * testCheckFull + * testStringUrl - CakeUrlChecker now accepts strings too * * @return void */ - public function testCheckFull() + public function testStringUrl() { $checker = new DefaultUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], ); - - $result = $checker->check($request, 'http://localhost/users/login', [ - 'checkFullUrl' => true, - ]); + $result = $checker->check($request, '/users/login'); $this->assertTrue($result); + + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/different/url'], + ); + $result = $checker->check($request, '/users/login'); + $this->assertFalse($result); } /** - * testCheckBase + * testNamedRoute * * @return void */ - public function testCheckBase() + public function testNamedRoute() { $checker = new DefaultUrlChecker(); $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], + ['REQUEST_URI' => '/login'], ); - $request = $request->withAttribute('base', '/base'); - - $result = $checker->check($request, 'http://localhost/base/users/login', [ - 'checkFullUrl' => true, - ]); + $result = $checker->check($request, ['_name' => 'login']); $this->assertTrue($result); } + + /** + * testInvalidNamedRoute + */ + public function testInvalidNamedRoute() + { + $this->expectException('Cake\Routing\Exception\MissingRouteException'); + $checker = new DefaultUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/login'], + ); + $checker->check($request, ['_name' => 'login-does-not-exist']); + } } diff --git a/tests/TestCase/UrlChecker/GenericUrlCheckerTest.php b/tests/TestCase/UrlChecker/GenericUrlCheckerTest.php new file mode 100644 index 00000000..cbf2b827 --- /dev/null +++ b/tests/TestCase/UrlChecker/GenericUrlCheckerTest.php @@ -0,0 +1,117 @@ + '/users/does-not-match'], + ); + + $result = $checker->check($request, '/users/login'); + $this->assertFalse($result); + } + + /** + * testCheckSimple + * + * @return void + */ + public function testCheckSimple() + { + $checker = new GenericUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/users/login'], + ); + $result = $checker->check($request, '/users/login'); + $this->assertTrue($result); + + $result = $checker->check($request, '/different/url'); + $this->assertFalse($result); + } + + /** + * testCheckRegexp + * + * @return void + */ + public function testCheckRegexp() + { + $checker = new GenericUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/en/users/login'], + ); + + $result = $checker->check($request, '%^/[a-z]{2}/users/login/?$%', [ + 'useRegex' => true, + ]); + $this->assertTrue($result); + } + + /** + * testCheckFull + * + * @return void + */ + public function testCheckFull() + { + $checker = new GenericUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/users/login'], + ); + + $result = $checker->check($request, 'http://localhost/users/login', [ + 'checkFullUrl' => true, + ]); + $this->assertTrue($result); + } + + /** + * testCheckBase + * + * @return void + */ + public function testCheckBase() + { + $checker = new GenericUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + ['REQUEST_URI' => '/users/login'], + ); + $request = $request->withAttribute('base', '/base'); + + $result = $checker->check($request, 'http://localhost/base/users/login', [ + 'checkFullUrl' => true, + ]); + $this->assertTrue($result); + } +} From 5105cdd69243d8711e4128552d1ec2d52b3b0013 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 29 Nov 2025 20:52:28 +0100 Subject: [PATCH 10/30] Apply suggestions from code review Co-authored-by: ADmad --- src/Authenticator/JwtAuthenticator.php | 6 +----- src/Authenticator/TokenAuthenticator.php | 7 +------ 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Authenticator/JwtAuthenticator.php b/src/Authenticator/JwtAuthenticator.php index d2927b87..56b0e8d9 100644 --- a/src/Authenticator/JwtAuthenticator.php +++ b/src/Authenticator/JwtAuthenticator.php @@ -76,11 +76,7 @@ public function __construct(?IdentifierInterface $identifier, array $config = [] */ public function getIdentifier(): IdentifierInterface { - if ($this->_identifier === null) { - $this->_identifier = IdentifierFactory::create('Authentication.JwtSubject'); - } - - return $this->_identifier; + return $this->_identifier ??= IdentifierFactory::create('Authentication.JwtSubject'); } /** diff --git a/src/Authenticator/TokenAuthenticator.php b/src/Authenticator/TokenAuthenticator.php index 0c1f194a..166cb2c5 100644 --- a/src/Authenticator/TokenAuthenticator.php +++ b/src/Authenticator/TokenAuthenticator.php @@ -45,12 +45,7 @@ class TokenAuthenticator extends AbstractAuthenticator implements StatelessInter * @return \Authentication\Identifier\IdentifierInterface */ public function getIdentifier(): IdentifierInterface - { - if ($this->_identifier === null) { - $this->_identifier = IdentifierFactory::create('Authentication.Token'); - } - - return $this->_identifier; + return $this->_identifier ??= IdentifierFactory::create('Authentication.Token'); } /** From 2ac8fd50d0e21df4c9d7347604d8b2fd4310e0bf Mon Sep 17 00:00:00 2001 From: mscherer Date: Sat, 29 Nov 2025 21:23:17 +0100 Subject: [PATCH 11/30] Fix syntax. --- src/Authenticator/TokenAuthenticator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Authenticator/TokenAuthenticator.php b/src/Authenticator/TokenAuthenticator.php index 166cb2c5..b2afbc36 100644 --- a/src/Authenticator/TokenAuthenticator.php +++ b/src/Authenticator/TokenAuthenticator.php @@ -45,6 +45,7 @@ class TokenAuthenticator extends AbstractAuthenticator implements StatelessInter * @return \Authentication\Identifier\IdentifierInterface */ public function getIdentifier(): IdentifierInterface + { return $this->_identifier ??= IdentifierFactory::create('Authentication.Token'); } From f4f9a636a4729db05135fe0d9bb107324d0bf53b Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 13 Dec 2025 20:44:26 +0100 Subject: [PATCH 12/30] Fix up for cake only use. (#756) --- docs/en/upgrade-3-to-4.rst | 29 +++++++++---------- docs/en/url-checkers.rst | 16 +++++----- ...ricUrlChecker.php => StringUrlChecker.php} | 7 ++--- .../EnvironmentAuthenticatorTest.php | 6 ++-- .../Authenticator/FormAuthenticatorTest.php | 8 ++--- ...eckerTest.php => StringUrlCheckerTest.php} | 16 +++++----- 6 files changed, 39 insertions(+), 43 deletions(-) rename src/UrlChecker/{GenericUrlChecker.php => StringUrlChecker.php} (94%) rename tests/TestCase/UrlChecker/{GenericUrlCheckerTest.php => StringUrlCheckerTest.php} (89%) diff --git a/docs/en/upgrade-3-to-4.rst b/docs/en/upgrade-3-to-4.rst index b465eb9c..e631c1b0 100644 --- a/docs/en/upgrade-3-to-4.rst +++ b/docs/en/upgrade-3-to-4.rst @@ -111,7 +111,7 @@ URL Checker Renamed and Restructured URL checkers have been completely restructured: - ``CakeRouterUrlChecker`` has been renamed to ``DefaultUrlChecker`` -- The old ``DefaultUrlChecker`` (framework-agnostic) has been renamed to ``GenericUrlChecker`` +- The old ``DefaultUrlChecker`` has been renamed to ``StringUrlChecker`` - Auto-detection has been removed - ``DefaultUrlChecker`` is now hardcoded **Before (3.x):** @@ -127,7 +127,7 @@ URL checkers have been completely restructured: ], ]); - // Using DefaultUrlChecker explicitly (framework-agnostic) + // Using DefaultUrlChecker explicitly $service->loadAuthenticator('Authentication.Form', [ 'urlChecker' => 'Authentication.Default', 'loginUrl' => '/users/login', @@ -150,9 +150,9 @@ URL checkers have been completely restructured: ], ]); - // For framework-agnostic projects, explicitly use GenericUrlChecker + // For string-only URL checking, explicitly use StringUrlChecker $service->loadAuthenticator('Authentication.Form', [ - 'urlChecker' => 'Authentication.Generic', + 'urlChecker' => 'Authentication.String', 'loginUrl' => '/users/login', ]); @@ -207,11 +207,10 @@ Auto-Detection Removed URL Checkers ^^^^^^^^^^^^ -**Important:** Auto-detection has been removed. ``DefaultUrlChecker`` is now hardcoded -and assumes CakePHP is available. +**Important:** Auto-detection has been removed. ``DefaultUrlChecker`` is now hardcoded. - **4.x default:** Always uses ``DefaultUrlChecker`` (formerly ``CakeUrlChecker``) -- **Framework-agnostic:** Must explicitly configure ``GenericUrlChecker`` +- **String URLs only:** Must explicitly configure ``StringUrlChecker`` - **Multiple URLs:** Must explicitly configure ``MultiUrlChecker`` DefaultUrlChecker is Now CakePHP-Based @@ -220,7 +219,7 @@ DefaultUrlChecker is Now CakePHP-Based ``DefaultUrlChecker`` is now the CakePHP checker (formerly ``CakeRouterUrlChecker``). It requires CakePHP Router and supports both string and array URLs. -The 3.x framework-agnostic ``DefaultUrlChecker`` has been renamed to ``GenericUrlChecker``. +The 3.x ``DefaultUrlChecker`` has been renamed to ``StringUrlChecker``. .. code-block:: php @@ -229,8 +228,8 @@ The 3.x framework-agnostic ``DefaultUrlChecker`` has been renamed to ``GenericUr $checker->check($request, ['controller' => 'Users', 'action' => 'login']); // Works $checker->check($request, '/users/login'); // Also works - // For framework-agnostic usage: - $checker = new GenericUrlChecker(); + // For string URL only usage: + $checker = new StringUrlChecker(); $checker->check($request, '/users/login'); // Works $checker->check($request, ['controller' => 'Users']); // Throws exception @@ -285,17 +284,17 @@ Migration Tips - ``IdentifierCollection`` → ``IdentifierFactory`` - ``'Authentication.CakeRouter'`` → Remove (no longer needed, default is now CakePHP-based) - ``CakeRouterUrlChecker`` → ``DefaultUrlChecker`` - - Old 3.x ``DefaultUrlChecker`` (framework-agnostic) → ``GenericUrlChecker`` + - Old 3.x ``DefaultUrlChecker`` → ``StringUrlChecker`` -2. **Framework-Agnostic Projects**: +2. **String URL Checking**: - If you're using this library without CakePHP, you **must** explicitly configure - ``GenericUrlChecker``: + If you want to use string-only URL checking, explicitly configure + ``StringUrlChecker``: .. code-block:: php $service->loadAuthenticator('Authentication.Form', [ - 'urlChecker' => 'Authentication.Generic', + 'urlChecker' => 'Authentication.String', 'loginUrl' => '/users/login', ]); diff --git a/docs/en/url-checkers.rst b/docs/en/url-checkers.rst index 6e7db9db..fc6a33e2 100644 --- a/docs/en/url-checkers.rst +++ b/docs/en/url-checkers.rst @@ -1,9 +1,8 @@ URL Checkers ############ -To provide an abstract and framework agnostic solution there are URL -checkers implemented that allow you to customize the comparison of the -current URL if needed. For example to another frameworks routing. +There are URL checkers implemented that allow you to customize the comparison +of the current URL if needed. All checkers support single URLs in either string or array format (like ``Router::url()``). For multiple login URLs, use ``MultiUrlChecker``. @@ -43,16 +42,15 @@ Options: - **checkFullUrl**: To compare the full URL, including protocol, host and port or not. Default is ``false``. -GenericUrlChecker ------------------- +StringUrlChecker +----------------- -Framework-agnostic checker for string URLs. Supports regex matching. -Use this for non-CakePHP projects. +Checker for string URLs. Supports regex matching. .. code-block:: php $service->loadAuthenticator('Authentication.Form', [ - 'urlChecker' => 'Authentication.Generic', + 'urlChecker' => 'Authentication.String', 'loginUrl' => '/users/login', ]); @@ -62,7 +60,7 @@ Using regex: $service->loadAuthenticator('Authentication.Form', [ 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', 'useRegex' => true, ], 'loginUrl' => '%^/[a-z]{2}/users/login/?$%', diff --git a/src/UrlChecker/GenericUrlChecker.php b/src/UrlChecker/StringUrlChecker.php similarity index 94% rename from src/UrlChecker/GenericUrlChecker.php rename to src/UrlChecker/StringUrlChecker.php index e36da18c..15f3ac9c 100644 --- a/src/UrlChecker/GenericUrlChecker.php +++ b/src/UrlChecker/StringUrlChecker.php @@ -20,9 +20,9 @@ use RuntimeException; /** - * Generic URL checker for framework-agnostic usage. Supports also regex. + * URL checker for string URLs. Supports also regex. */ -class GenericUrlChecker implements UrlCheckerInterface +class StringUrlChecker implements UrlCheckerInterface { /** * Default Options @@ -44,8 +44,7 @@ public function check(ServerRequestInterface $request, array|string $loginUrls, { if (is_array($loginUrls)) { throw new RuntimeException( - 'Array-based login URLs require CakePHP Router and DefaultUrlChecker. ' . - 'Use string URLs instead.', + 'Array-based login URLs require CakePHP Router and DefaultUrlChecker.', ); } diff --git a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php index e4c0fe0d..464e835b 100644 --- a/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/EnvironmentAuthenticatorTest.php @@ -378,7 +378,7 @@ public function testRegexLoginUrlSuccess() $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%^/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', 'useRegex' => true, ], 'fields' => [ @@ -409,7 +409,7 @@ public function testFullRegexLoginUrlFailure() $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -441,7 +441,7 @@ public function testFullRegexLoginUrlSuccess() $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/secure/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', 'useRegex' => true, 'checkFullUrl' => true, ], diff --git a/tests/TestCase/Authenticator/FormAuthenticatorTest.php b/tests/TestCase/Authenticator/FormAuthenticatorTest.php index 13d4701b..96103f52 100644 --- a/tests/TestCase/Authenticator/FormAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/FormAuthenticatorTest.php @@ -313,7 +313,7 @@ public function testRegexLoginUrlSuccess() $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%^/[a-z]{2}/users/login/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', 'useRegex' => true, ], ]); @@ -345,7 +345,7 @@ public function testFullRegexLoginUrlFailure() $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -379,7 +379,7 @@ public function testFullRegexLoginUrlSuccess() $form = new FormAuthenticator($identifier, [ 'loginUrl' => '%auth\.localhost/[a-z]{2}/users/login/?$%', 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -410,7 +410,7 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() $form = new FormAuthenticator($identifier, [ 'loginUrl' => 'http://localhost/users/login', 'urlChecker' => [ - 'className' => 'Authentication.Generic', + 'className' => 'Authentication.String', ], ]); diff --git a/tests/TestCase/UrlChecker/GenericUrlCheckerTest.php b/tests/TestCase/UrlChecker/StringUrlCheckerTest.php similarity index 89% rename from tests/TestCase/UrlChecker/GenericUrlCheckerTest.php rename to tests/TestCase/UrlChecker/StringUrlCheckerTest.php index cbf2b827..f865bc27 100644 --- a/tests/TestCase/UrlChecker/GenericUrlCheckerTest.php +++ b/tests/TestCase/UrlChecker/StringUrlCheckerTest.php @@ -17,13 +17,13 @@ namespace Authentication\Test\TestCase\UrlChecker; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; -use Authentication\UrlChecker\GenericUrlChecker; +use Authentication\UrlChecker\StringUrlChecker; use Cake\Http\ServerRequestFactory; /** - * GenericUrlCheckerTest + * StringUrlCheckerTest */ -class GenericUrlCheckerTest extends TestCase +class StringUrlCheckerTest extends TestCase { /** * testCheckFailure @@ -32,7 +32,7 @@ class GenericUrlCheckerTest extends TestCase */ public function testCheckFailure() { - $checker = new GenericUrlChecker(); + $checker = new StringUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/does-not-match'], @@ -49,7 +49,7 @@ public function testCheckFailure() */ public function testCheckSimple() { - $checker = new GenericUrlChecker(); + $checker = new StringUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], ); @@ -67,7 +67,7 @@ public function testCheckSimple() */ public function testCheckRegexp() { - $checker = new GenericUrlChecker(); + $checker = new StringUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/en/users/login'], ); @@ -85,7 +85,7 @@ public function testCheckRegexp() */ public function testCheckFull() { - $checker = new GenericUrlChecker(); + $checker = new StringUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], ); @@ -103,7 +103,7 @@ public function testCheckFull() */ public function testCheckBase() { - $checker = new GenericUrlChecker(); + $checker = new StringUrlChecker(); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], ); From 85076bf567caf52b8cb6eeb0e8a337ca68de58fd Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 07:45:48 +0100 Subject: [PATCH 13/30] Update version references from 3 to 4 - composer.json docs URL - English and French index.rst --- composer.json | 2 +- docs/en/index.rst | 2 +- docs/fr/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 660c1547..e80bf089 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "issues": "https://github.com/cakephp/authentication/issues", "forum": "https://discourse.cakephp.org/", "source": "https://github.com/cakephp/authentication", - "docs": "https://book.cakephp.org/authentication/3/en/" + "docs": "https://book.cakephp.org/authentication/4/en/" }, "require": { "php": ">=8.1", diff --git a/docs/en/index.rst b/docs/en/index.rst index 4f12ea25..3fb17d29 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -8,7 +8,7 @@ Project's ROOT directory (where the **composer.json** file is located) php composer.phar require cakephp/authentication -Version 3 of the Authentication Plugin is compatible with CakePHP 5. +Version 4 of the Authentication Plugin is compatible with CakePHP 5. Load the plugin by adding the following statement in your project's ``src/Application.php``:: diff --git a/docs/fr/index.rst b/docs/fr/index.rst index fefd8587..e216f554 100644 --- a/docs/fr/index.rst +++ b/docs/fr/index.rst @@ -9,7 +9,7 @@ répertoire ROOT de votre projet CakePHP (là où se trouve le fichier php composer.phar require cakephp/authentication -La version 3 du Plugin Authentication est compatible avec CakePHP 5. +La version 4 du Plugin Authentication est compatible avec CakePHP 5. Chargez le plugin en ajoutant l'instruction suivante dans le fichier ``src/Application.php`` de votre projet:: From 740b68d834e427c8f36112a933d8699a6011f4fc Mon Sep 17 00:00:00 2001 From: ADmad Date: Tue, 13 Jan 2026 12:57:52 +0530 Subject: [PATCH 14/30] Bump up firebase/php-jwt to ^7.0 (#760) --- composer.json | 2 +- .../TestCase/Authenticator/JwtAuthenticatorTest.php | 12 ++++++------ .../Middleware/AuthenticationMiddlewareTest.php | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 553cd29c..ca993987 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "require-dev": { "cakephp/cakephp": "^5.1.0", "cakephp/cakephp-codesniffer": "^5.0", - "firebase/php-jwt": "^6.2", + "firebase/php-jwt": "^7.0", "phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.4" }, "suggest": { diff --git a/tests/TestCase/Authenticator/JwtAuthenticatorTest.php b/tests/TestCase/Authenticator/JwtAuthenticatorTest.php index c2b36c4c..cdfb44b9 100644 --- a/tests/TestCase/Authenticator/JwtAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/JwtAuthenticatorTest.php @@ -79,7 +79,7 @@ public function setUp(): void 'firstname' => 'larry', ]; - $this->tokenHS256 = JWT::encode($data, 'secretKey', 'HS256'); + $this->tokenHS256 = JWT::encode($data, 'secretKey0123456789secretKey0123456789', 'HS256'); $privKey1 = file_get_contents(__DIR__ . '/../../data/rsa1-private.pem'); $this->tokenRS256 = JWT::encode($data, $privKey1, 'RS256', 'jwk1'); @@ -100,7 +100,7 @@ public function testAuthenticateViaHeaderToken() $this->request = $this->request->withAddedHeader('Authorization', 'Bearer ' . $this->tokenHS256); $authenticator = new JwtAuthenticator($this->identifier, [ - 'secretKey' => 'secretKey', + 'secretKey' => 'secretKey0123456789secretKey0123456789', 'subjectKey' => 'subjectId', ]); @@ -123,7 +123,7 @@ public function testAuthenticateViaQueryParamToken() ); $authenticator = new JwtAuthenticator($this->identifier, [ - 'secretKey' => 'secretKey', + 'secretKey' => 'secretKey0123456789secretKey0123456789', 'subjectKey' => 'subjectId', ]); @@ -159,7 +159,7 @@ public function testAuthenticationViaIdentifierAndSubject() ])); $authenticator = new JwtAuthenticator($this->identifier, [ - 'secretKey' => 'secretKey', + 'secretKey' => 'secretKey0123456789secretKey0123456789', 'returnPayload' => false, 'subjectKey' => 'subjectId', ]); @@ -242,7 +242,7 @@ public function testInvalidToken() ); $authenticator = new JwtAuthenticator($this->identifier, [ - 'secretKey' => 'secretKey', + 'secretKey' => 'secretKey0123456789secretKey0123456789', ]); $result = $authenticator->authenticate($this->request); @@ -268,7 +268,7 @@ public function testGetPayloadHS256() ); $authenticator = new JwtAuthenticator($this->identifier, [ - 'secretKey' => 'secretKey', + 'secretKey' => 'secretKey0123456789secretKey0123456789', ]); $result = $authenticator->getPayload(); diff --git a/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php b/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php index 6d5c21f5..b1d14ffa 100644 --- a/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php +++ b/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php @@ -578,13 +578,13 @@ public function testJwtTokenAuthorizationThroughTheMiddlewareStack() 'firstname' => 'larry', ]; - $token = JWT::encode($data, 'secretKey', 'HS256'); + $token = JWT::encode($data, 'secretKey0123456789secretKey0123456789', 'HS256'); $this->service = new AuthenticationService([ 'authenticators' => [ 'Authentication.Form' => ['identifier' => 'Authentication.Password'], 'Authentication.Jwt' => [ - 'secretKey' => 'secretKey', + 'secretKey' => 'secretKey0123456789secretKey0123456789', 'identifier' => 'Authentication.JwtSubject', ], ], From 680525a464b0c4644988c5b1d0ca683b110ec5d7 Mon Sep 17 00:00:00 2001 From: ADmad Date: Tue, 13 Jan 2026 14:00:20 +0530 Subject: [PATCH 15/30] Update docs (#761) --- docs/en/authenticators.rst | 4 ++-- docs/en/identifiers.rst | 2 +- docs/en/index.rst | 12 ++++-------- docs/es/identifiers.rst | 2 +- docs/es/index.rst | 2 +- docs/fr/identifiers.rst | 2 +- docs/ja/index.rst | 2 +- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/en/authenticators.rst b/docs/en/authenticators.rst index a3bfc1b3..83e785cc 100644 --- a/docs/en/authenticators.rst +++ b/docs/en/authenticators.rst @@ -143,7 +143,7 @@ example. If provided will be used instead of the secret key. You need to add the lib `firebase/php-jwt `_ -v6.2 or above to your app to use the ``JwtAuthenticator``. +v7.0 or above to your app to use the ``JwtAuthenticator``. By default the ``JwtAuthenticator`` uses ``HS256`` symmetric key algorithm and uses the value of ``Cake\Utility\Security::salt()`` as encryption key. @@ -430,7 +430,7 @@ There is only one event that is fired by authentication: ``Authentication.afterIdentify``. If you don’t know what events are and how to use them `check the -documentation `__. +documentation `__. The ``Authentication.afterIdentify`` event is fired by the ``AuthenticationComponent`` after an identity was successfully diff --git a/docs/en/identifiers.rst b/docs/en/identifiers.rst index 5c673fa9..9db0e9aa 100644 --- a/docs/en/identifiers.rst +++ b/docs/en/identifiers.rst @@ -172,7 +172,7 @@ Configuration options: - **userModel**: The user model identities are located in. Default is ``Users``. - **finder**: The finder to use with the model. Default is ``all``. - You can read more about model finders `here `__. + You can read more about model finders `here `__. In order to use ORM resolver you must require ``cakephp/orm`` in your ``composer.json`` file (if you are not already using the full CakePHP framework). diff --git a/docs/en/index.rst b/docs/en/index.rst index 3fb17d29..728f80d8 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -10,20 +10,16 @@ Project's ROOT directory (where the **composer.json** file is located) Version 4 of the Authentication Plugin is compatible with CakePHP 5. -Load the plugin by adding the following statement in your project's ``src/Application.php``:: +Load the plugin using the following command:: - public function bootstrap(): void - { - parent::bootstrap(); - - $this->addPlugin('Authentication'); - } +.. code-block:: shell + bin/cake plugin load Authentication Getting Started =============== -The authentication plugin integrates with your application as a `middleware `_. It can also +The authentication plugin integrates with your application as a `middleware `_. It can also be used as a component to make unauthenticated access simpler. First, let's apply the middleware. In **src/Application.php**, add the following to the class imports:: diff --git a/docs/es/identifiers.rst b/docs/es/identifiers.rst index 76e117d0..eb13b0b8 100644 --- a/docs/es/identifiers.rst +++ b/docs/es/identifiers.rst @@ -163,7 +163,7 @@ Opciones de configuración: - **userModel**: El modelo donde están localizadas las indentidades. Por defecto es ``Users``. - **finder**: El finder a usar con el modelo. Por defecto es ``all``. - Puede leer mas sobre los finders de los modelos `aquí `__. + Puede leer mas sobre los finders de los modelos `aquí `__. Para usar el resolver ORM se requiere tener ``cakephp/orm`` en su archivo ``composer.json`` (si no estás usando el framework CakePHP completo). diff --git a/docs/es/index.rst b/docs/es/index.rst index 206e9514..3c86e3a6 100644 --- a/docs/es/index.rst +++ b/docs/es/index.rst @@ -21,7 +21,7 @@ Carge el plugin agregando la siguiente declaración en ``src/Application.php``:: Empezando ========= -El plugin authentication se integra con su aplicación como un `middleware `_. También, se +El plugin authentication se integra con su aplicación como un `middleware `_. También, se puede utilizar como un componente para simplificar el acceso no autenticado. Primero aplique el middleware. En **src/Application.php**, agregue las siguientes importaciones de clase:: diff --git a/docs/fr/identifiers.rst b/docs/fr/identifiers.rst index fac152cb..2936398b 100644 --- a/docs/fr/identifiers.rst +++ b/docs/fr/identifiers.rst @@ -169,7 +169,7 @@ Options de configuration: Par défaut ``Users``. - **finder**: Le finder à utiliser avec le modèle. Par défaut ``all``. Pour en savoir plus sur les finders de modèle, consultez - `cette documentation `__. + `cette documentation `__. Afin d'utiliser le résolveur ORM, vous devez requérir ``cakephp/orm`` dans votre fichier ``composer.json`` (si vous n'utilisez pas déjà le framework CakePHP diff --git a/docs/ja/index.rst b/docs/ja/index.rst index 6023daa0..574317d4 100644 --- a/docs/ja/index.rst +++ b/docs/ja/index.rst @@ -22,7 +22,7 @@ CakePHPから `composer `_ でプラグインをイン はじめに =============== -認証プラグインは、ミドルウェアとしてアプリケーションと統合します。 `middleware `_ +認証プラグインは、ミドルウェアとしてアプリケーションと統合します。 `middleware `_ また、認証されていないアクセスをより簡単にするためのコンポーネントとして使用することもできます。 まずはミドルウェアを適用してみましょう。 **src/Application.php** に以下のクラスを追加します。 From 1efa12b7091c1e5de94bbb36c56d4dfe706e5366 Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Tue, 13 Jan 2026 17:25:25 +0100 Subject: [PATCH 16/30] Drop deprecated Plugin.php for 4.x (#762) --- src/Plugin.php | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/Plugin.php diff --git a/src/Plugin.php b/src/Plugin.php deleted file mode 100644 index 6c4d5ea3..00000000 --- a/src/Plugin.php +++ /dev/null @@ -1,25 +0,0 @@ - Date: Tue, 13 Jan 2026 22:17:04 +0530 Subject: [PATCH 17/30] Update readme --- readme.md | 1 - 1 file changed, 1 deletion(-) diff --git a/readme.md b/readme.md index c63fb39c..9f5dc5bb 100644 --- a/readme.md +++ b/readme.md @@ -39,4 +39,3 @@ Documentation for this plugin can be found in the [CakePHP Cookbook](https://boo There are IdeHelper tasks in [IdeHelperExtra plugin](https://github.com/dereuromark/cakephp-ide-helper-extra/) to provide auto-complete: - `AuthenticationService::loadAuthenticator()` -- `IdentifierCollection::load()` From cfc37d62048c8e45b6ba4fec02045edb8d9263e2 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 14 Jan 2026 02:38:11 +0100 Subject: [PATCH 18/30] Add default TokenIdentifier for PrimaryKeySessionAuthenticator PrimaryKeySessionAuthenticator now works out of the box without requiring explicit identifier configuration. When no identifier is provided, it lazily creates a TokenIdentifier configured to look up users by their `id` field. Before: ```php $service->loadAuthenticator('Authentication.PrimaryKeySession', [ 'identifier' => [ 'className' => 'Authentication.Token', 'tokenField' => 'id', 'dataField' => 'key', ], ]); ``` After: ```php $service->loadAuthenticator('Authentication.PrimaryKeySession'); ``` Custom configuration is still supported by passing an explicit identifier or by using the `idField` and `identifierKey` config options which propagate to the default TokenIdentifier. --- .../PrimaryKeySessionAuthenticator.php | 81 +++++++++++++++++-- .../PrimaryKeySessionAuthenticatorTest.php | 59 +++++++++++++- 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/src/Authenticator/PrimaryKeySessionAuthenticator.php b/src/Authenticator/PrimaryKeySessionAuthenticator.php index a55e8bb6..a03f896f 100644 --- a/src/Authenticator/PrimaryKeySessionAuthenticator.php +++ b/src/Authenticator/PrimaryKeySessionAuthenticator.php @@ -4,6 +4,7 @@ namespace Authentication\Authenticator; use ArrayAccess; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\IdentifierInterface; use Cake\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; @@ -11,21 +12,87 @@ /** * Session Authenticator with only ID + * + * This authenticator stores only the user's primary key in the session, + * and looks up the full user record from the database on each request. + * + * By default, it uses a TokenIdentifier configured to look up users by + * their `id` field. This works out of the box for most applications: + * + * ```php + * $service->loadAuthenticator('Authentication.PrimaryKeySession'); + * ``` + * + * You can customize the identifier configuration if needed: + * + * ```php + * $service->loadAuthenticator('Authentication.PrimaryKeySession', [ + * 'identifier' => [ + * 'className' => 'Authentication.Token', + * 'tokenField' => 'uuid', + * 'dataField' => 'key', + * 'resolver' => [ + * 'className' => 'Authentication.Orm', + * 'userModel' => 'Members', + * ], + * ], + * ]); + * ``` */ class PrimaryKeySessionAuthenticator extends SessionAuthenticator { /** - * @param \Authentication\Identifier\IdentifierInterface|null $identifier - * @param array $config + * Default config for this object. + * + * - `identifierKey` The key used when passing the ID to the identifier. + * - `idField` The field on the user entity that contains the primary key. + * + * @var array + */ + protected array $_defaultConfig = [ + 'fields' => [], + 'sessionKey' => 'Auth', + 'impersonateSessionKey' => 'AuthImpersonate', + 'identify' => false, + 'identityAttribute' => 'identity', + 'identifierKey' => 'key', + 'idField' => 'id', + ]; + + /** + * Constructor + * + * Bypasses SessionAuthenticator's default PasswordIdentifier creation + * to allow lazy initialization of the TokenIdentifier in getIdentifier(). + * + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. + * @param array $config Configuration settings. */ public function __construct(?IdentifierInterface $identifier, array $config = []) { - $config += [ - 'identifierKey' => 'key', - 'idField' => 'id', - ]; + $this->_identifier = $identifier; + $this->setConfig($config); + } + + /** + * Gets the identifier. + * + * If no identifier was explicitly configured, creates a default TokenIdentifier + * configured to look up users by their primary key (`id` field). + * + * @return \Authentication\Identifier\IdentifierInterface + */ + public function getIdentifier(): IdentifierInterface + { + if ($this->_identifier === null) { + $this->_identifier = IdentifierFactory::create([ + 'className' => 'Authentication.Token', + 'tokenField' => $this->getConfig('idField'), + 'dataField' => $this->getConfig('identifierKey'), + ]); + } - parent::__construct($identifier, $config); + return $this->_identifier; } /** diff --git a/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php b/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php index 5b6d8806..7c5f05fd 100644 --- a/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php @@ -67,7 +67,7 @@ public function setUp(): void } /** - * Test authentication + * Test authentication with explicit identifier * * @return void */ @@ -89,6 +89,63 @@ public function testAuthenticateSuccess() $this->assertSame(Result::SUCCESS, $result->getStatus()); } + /** + * Test authentication works with default identifier (no explicit configuration) + * + * @return void + */ + public function testAuthenticateSuccessWithDefaultIdentifier() + { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); + + $this->sessionMock->expects($this->once()) + ->method('read') + ->with('Auth') + ->willReturn(1); + + $request = $request->withAttribute('session', $this->sessionMock); + + // No identifier passed - should use the default TokenIdentifier + $authenticator = new PrimaryKeySessionAuthenticator(null); + $result = $authenticator->authenticate($request); + + $this->assertInstanceOf(Result::class, $result); + $this->assertSame(Result::SUCCESS, $result->getStatus()); + } + + /** + * Test getIdentifier returns default TokenIdentifier when none configured + * + * @return void + */ + public function testGetIdentifierReturnsDefaultWhenNotConfigured() + { + $authenticator = new PrimaryKeySessionAuthenticator(null); + $identifier = $authenticator->getIdentifier(); + + $this->assertInstanceOf(\Authentication\Identifier\TokenIdentifier::class, $identifier); + $this->assertSame('id', $identifier->getConfig('tokenField')); + $this->assertSame('key', $identifier->getConfig('dataField')); + } + + /** + * Test custom idField/identifierKey config propagates to default identifier + * + * @return void + */ + public function testGetIdentifierUsesCustomConfig() + { + $authenticator = new PrimaryKeySessionAuthenticator(null, [ + 'idField' => 'uuid', + 'identifierKey' => 'token', + ]); + $identifier = $authenticator->getIdentifier(); + + $this->assertInstanceOf(\Authentication\Identifier\TokenIdentifier::class, $identifier); + $this->assertSame('uuid', $identifier->getConfig('tokenField')); + $this->assertSame('token', $identifier->getConfig('dataField')); + } + /** * Test authentication * From a7fc2972b81ce20306aece599e71a56c8e87fbaf Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 14 Jan 2026 02:45:48 +0100 Subject: [PATCH 19/30] Fix code style - use import instead of FQCN --- .../Authenticator/PrimaryKeySessionAuthenticatorTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php b/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php index 7c5f05fd..ce704aae 100644 --- a/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php @@ -20,6 +20,7 @@ use Authentication\Authenticator\PrimaryKeySessionAuthenticator; use Authentication\Authenticator\Result; use Authentication\Identifier\IdentifierFactory; +use Authentication\Identifier\TokenIdentifier; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; use Cake\Http\ServerRequestFactory; @@ -123,7 +124,7 @@ public function testGetIdentifierReturnsDefaultWhenNotConfigured() $authenticator = new PrimaryKeySessionAuthenticator(null); $identifier = $authenticator->getIdentifier(); - $this->assertInstanceOf(\Authentication\Identifier\TokenIdentifier::class, $identifier); + $this->assertInstanceOf(TokenIdentifier::class, $identifier); $this->assertSame('id', $identifier->getConfig('tokenField')); $this->assertSame('key', $identifier->getConfig('dataField')); } @@ -141,7 +142,7 @@ public function testGetIdentifierUsesCustomConfig() ]); $identifier = $authenticator->getIdentifier(); - $this->assertInstanceOf(\Authentication\Identifier\TokenIdentifier::class, $identifier); + $this->assertInstanceOf(TokenIdentifier::class, $identifier); $this->assertSame('uuid', $identifier->getConfig('tokenField')); $this->assertSame('token', $identifier->getConfig('dataField')); } From ef15407b9d8deb011f92aa05447debb63de4ccc2 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 21 Jan 2026 17:06:34 +0100 Subject: [PATCH 20/30] Update docs for PrimaryKeySession default identifier --- docs/en/authenticators.rst | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/en/authenticators.rst b/docs/en/authenticators.rst index 83e785cc..86738e8e 100644 --- a/docs/en/authenticators.rst +++ b/docs/en/authenticators.rst @@ -37,23 +37,34 @@ It also helps to avoid session invalidation. Session itself stores the entity object including nested objects like DateTime or enums. With only the ID stored, the invalidation due to objects being modified will also dissolve. -Make sure to match this with a Token identifier with ``key``/``id`` keys:: +A default ``TokenIdentifier`` is provided that looks up users by their ``id`` field, +so minimal configuration is required:: + + $service->loadAuthenticator('Authentication.PrimaryKeySession'); + +Configuration options: + +- **idField**: The field in the database table to look up. Default is ``id``. +- **identifierKey**: The key used to store/retrieve the primary key from session data. + Default is ``key``. + +For custom lookup fields, the ``idField`` and ``identifierKey`` options propagate +to the default identifier automatically:: + + $service->loadAuthenticator('Authentication.PrimaryKeySession', [ + 'idField' => 'uuid', + ]); + +You can also provide a fully custom identifier configuration if needed:: $service->loadAuthenticator('Authentication.PrimaryKeySession', [ 'identifier' => [ 'Authentication.Token' => [ - 'tokenField' => 'id', // lookup for resolver and DB table - 'dataField' => 'key', // incoming data from authenticator + 'tokenField' => 'id', + 'dataField' => 'key', 'resolver' => 'Authentication.Orm', ], ], - 'urlChecker' => 'Authentication.CakeRouter', - 'loginUrl' => [ - 'prefix' => false, - 'plugin' => false, - 'controller' => 'Users', - 'action' => 'login', - ], ]); Form From f49d9bac6c9ffadd7a4ea2b4ab60efa78ea2753f Mon Sep 17 00:00:00 2001 From: ADmad Date: Thu, 29 Jan 2026 21:33:30 +0530 Subject: [PATCH 21/30] Allow using dot separated field names for Identity::get() --- composer.json | 1 + src/Identity.php | 15 ++++++++------- tests/TestCase/IdentityTest.php | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index ca993987..f1cb210e 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "require": { "php": ">=8.1", "cakephp/http": "^5.0", + "cakephp/utility": "^5.0", "laminas/laminas-diactoros": "^3.0", "psr/http-client": "^1.0", "psr/http-message": "^1.1 || ^2.0", diff --git a/src/Identity.php b/src/Identity.php index 0a699fce..9e119dd1 100644 --- a/src/Identity.php +++ b/src/Identity.php @@ -19,6 +19,7 @@ use ArrayAccess; use BadMethodCallException; use Cake\Core\InstanceConfigTrait; +use Cake\Utility\Hash; /** * Identity object @@ -92,21 +93,21 @@ public function __isset(string $field): bool /** * Get data from the identity * - * @param string $field Field in the user data. + * @param string|null $field Field in the user data. * @return mixed */ - public function get(string $field): mixed + public function get(?string $field = null): mixed { + if ($field === null) { + return $this->data; + } + $map = $this->_config['fieldMap']; if (isset($map[$field])) { $field = $map[$field]; } - if (isset($this->data[$field])) { - return $this->data[$field]; - } - - return null; + return Hash::get($this->data, $field); } /** diff --git a/tests/TestCase/IdentityTest.php b/tests/TestCase/IdentityTest.php index 731c0c51..4f8ba363 100644 --- a/tests/TestCase/IdentityTest.php +++ b/tests/TestCase/IdentityTest.php @@ -19,6 +19,7 @@ use ArrayObject; use Authentication\Identity; use BadMethodCallException; +use Cake\ORM\Entity; use Cake\TestSuite\TestCase; class IdentityTest extends TestCase @@ -43,6 +44,23 @@ public function testGetIdentifier() $this->assertSame('florian', $identity->username); } + public function testGet(): void + { + $data = new Entity([ + 'id' => 1, + 'username' => 'florian', + 'account' => new Entity(['id' => 2, 'role' => 'admin']), + ]); + + $identity = new Identity($data); + + $this->assertSame(1, $identity->get('id')); + $this->assertSame('florian', $identity->get('username')); + $this->assertSame('admin', $identity->get('account.role')); + $this->assertNull($identity->get('missing')); + $this->assertSame($data, $identity->get()); + } + /** * Test mapping fields * From 462ff6a942b81bf757a4e9325bf003e9cb1fa8ae Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 30 Jan 2026 11:44:13 +0530 Subject: [PATCH 22/30] Add IdentityHelper::getIdentity() --- src/View/Helper/IdentityHelper.php | 12 ++++++++++++ tests/TestCase/View/Helper/IdentityHelperTest.php | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/View/Helper/IdentityHelper.php b/src/View/Helper/IdentityHelper.php index 6c2dc947..ca248536 100644 --- a/src/View/Helper/IdentityHelper.php +++ b/src/View/Helper/IdentityHelper.php @@ -120,4 +120,16 @@ public function get(?string $key = null): mixed return Hash::get($this->_identity, $key); } + + /** + * Returns the identity instance. + * + * @return \Authentication\IdentityInterface|null + */ + public function getIdentity(): ?IdentityInterface + { + return $this->_View + ->getRequest() + ->getAttribute($this->getConfig('identityAttribute')); + } } diff --git a/tests/TestCase/View/Helper/IdentityHelperTest.php b/tests/TestCase/View/Helper/IdentityHelperTest.php index b731f769..893418c2 100644 --- a/tests/TestCase/View/Helper/IdentityHelperTest.php +++ b/tests/TestCase/View/Helper/IdentityHelperTest.php @@ -91,4 +91,16 @@ public function testWithOutIdentity() $this->assertFalse($helper->is(1)); } + + public function testGetIdentity() + { + $identity = new Identity([ + 'id' => 1, + ]); + $request = (new ServerRequest())->withAttribute('identity', $identity); + $view = new View($request); + + $helper = new IdentityHelper($view); + $this->assertSame($identity, $helper->getIdentity()); + } } From 19df42a74bdcb99c1b34dee2f19f66e7a533fb3d Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 30 Jan 2026 12:57:24 +0530 Subject: [PATCH 23/30] Update docblock --- src/Identity.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Identity.php b/src/Identity.php index 9e119dd1..18d5281b 100644 --- a/src/Identity.php +++ b/src/Identity.php @@ -91,7 +91,11 @@ public function __isset(string $field): bool } /** - * Get data from the identity + * Get data from the identity. + * + * You can use dot notation to fetch nested data. + * Calling the method without any argument will return + * the entire data array/object (same as `getOriginalData()`). * * @param string|null $field Field in the user data. * @return mixed From d3ab4fe081298773479805a71ced2d42a6a9344c Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 30 Jan 2026 13:00:01 +0530 Subject: [PATCH 24/30] Update docblock --- src/View/Helper/IdentityHelper.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/View/Helper/IdentityHelper.php b/src/View/Helper/IdentityHelper.php index ca248536..2396afb7 100644 --- a/src/View/Helper/IdentityHelper.php +++ b/src/View/Helper/IdentityHelper.php @@ -103,7 +103,11 @@ public function is(int|string $id, string $field = 'id'): bool } /** - * Gets user data + * Get data from the identity. + * + * You can use dot notation to fetch nested data. + * Calling the method without any argument will return + * the entire data array/object (same as `IdentityInterface::getOriginalData()`). * * @param string|null $key Key of something you want to get from the identity data * @return mixed From 50e6f786ad1af46a933022a51af0f0cd44a9a6f8 Mon Sep 17 00:00:00 2001 From: ADmad Date: Fri, 30 Jan 2026 14:24:05 +0530 Subject: [PATCH 25/30] Use the _identity property Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/View/Helper/IdentityHelper.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/View/Helper/IdentityHelper.php b/src/View/Helper/IdentityHelper.php index 2396afb7..88da0cd7 100644 --- a/src/View/Helper/IdentityHelper.php +++ b/src/View/Helper/IdentityHelper.php @@ -132,8 +132,6 @@ public function get(?string $key = null): mixed */ public function getIdentity(): ?IdentityInterface { - return $this->_View - ->getRequest() - ->getAttribute($this->getConfig('identityAttribute')); + return $this->_identity; } } From cbc12d3717bcc91eef19c64594d97540b9aea51d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:54:41 +0000 Subject: [PATCH 26/30] Initial plan From c52efc232eae7831b36a1aafbf7d0fc981288971 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:09:51 +0000 Subject: [PATCH 27/30] Add test for getIdentity() returning null without identity Co-authored-by: ADmad <142658+ADmad@users.noreply.github.com> --- tests/TestCase/View/Helper/IdentityHelperTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/TestCase/View/Helper/IdentityHelperTest.php b/tests/TestCase/View/Helper/IdentityHelperTest.php index 893418c2..b41f0162 100644 --- a/tests/TestCase/View/Helper/IdentityHelperTest.php +++ b/tests/TestCase/View/Helper/IdentityHelperTest.php @@ -90,6 +90,8 @@ public function testWithOutIdentity() $this->assertNull($helper->getId()); $this->assertFalse($helper->is(1)); + + $this->assertNull($helper->getIdentity()); } public function testGetIdentity() From 18afbf881765babde3fa46ec050345431df19aa3 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 1 Feb 2026 07:16:29 +0100 Subject: [PATCH 28/30] Remove deprecated identify option from SessionAuthenticator The identify option was deprecated in 3.x in favor of PrimaryKeySessionAuthenticator. Remove it for the 4.x major. --- docs/en/authenticators.rst | 7 +-- docs/en/upgrade-3-to-4.rst | 37 ++++++++++-- docs/es/authenticators.rst | 7 +-- docs/fr/authenticators.rst | 9 +-- docs/ja/authenticators.rst | 5 -- .../PrimaryKeySessionAuthenticator.php | 2 - src/Authenticator/SessionAuthenticator.php | 18 ------ tests/TestCase/AuthenticationServiceTest.php | 51 ----------------- .../SessionAuthenticatorTest.php | 56 ------------------- 9 files changed, 35 insertions(+), 157 deletions(-) diff --git a/docs/en/authenticators.rst b/docs/en/authenticators.rst index 86738e8e..c235eea3 100644 --- a/docs/en/authenticators.rst +++ b/docs/en/authenticators.rst @@ -18,13 +18,8 @@ Configuration options: - **sessionKey**: The session key for the user data, default is ``Auth`` -- **identify**: Set this key with a value of bool ``true`` to enable checking - the session credentials against the identifiers. When ``true``, the configured - :doc:`/identifiers` are used to identify the user using data - stored in the session on each request. Default value is ``false``. - **fields**: Allows you to map the ``username`` field to the unique - identifier in your user storage. Defaults to ``username``. This option is - used when the ``identify`` option is set to true. + identifier in your user storage. Defaults to ``username``. PrimaryKeySession ================= diff --git a/docs/en/upgrade-3-to-4.rst b/docs/en/upgrade-3-to-4.rst index e631c1b0..d254f724 100644 --- a/docs/en/upgrade-3-to-4.rst +++ b/docs/en/upgrade-3-to-4.rst @@ -105,6 +105,28 @@ For LDAP authentication: LdapIdentifier::CREDENTIAL_PASSWORD => 'password', ]; +SessionAuthenticator ``identify`` Option Removed +------------------------------------------------- + +The deprecated ``identify`` option has been removed from ``SessionAuthenticator``. +Use ``PrimaryKeySessionAuthenticator`` instead if you need to fetch fresh user +data from the database on each request. + +**Before (3.x):** + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Session', [ + 'identify' => true, + 'identifier' => 'Authentication.Password', + ]); + +**After (4.x):** + +.. code-block:: php + + $service->loadAuthenticator('Authentication.PrimaryKeySession'); + URL Checker Renamed and Restructured ------------------------------------- @@ -278,7 +300,12 @@ New dedicated checker for multiple login URLs: Migration Tips ============== -1. **Search and Replace**: +1. **Session Identify**: + + If you used ``'identify' => true`` on ``SessionAuthenticator``, switch to + ``PrimaryKeySessionAuthenticator`` which always fetches fresh data. + +2. **Search and Replace**: - ``AbstractIdentifier::CREDENTIAL_`` → ``PasswordIdentifier::CREDENTIAL_`` - ``IdentifierCollection`` → ``IdentifierFactory`` @@ -286,7 +313,7 @@ Migration Tips - ``CakeRouterUrlChecker`` → ``DefaultUrlChecker`` - Old 3.x ``DefaultUrlChecker`` → ``StringUrlChecker`` -2. **String URL Checking**: +3. **String URL Checking**: If you want to use string-only URL checking, explicitly configure ``StringUrlChecker``: @@ -298,17 +325,17 @@ Migration Tips 'loginUrl' => '/users/login', ]); -3. **Multiple Login URLs**: +4. **Multiple Login URLs**: If you have multiple login URLs, add ``'urlChecker' => 'Authentication.Multi'`` to your authenticator configuration. -4. **Custom Identifier Setup**: +5. **Custom Identifier Setup**: If you were passing ``IdentifierCollection`` to authenticators, switch to either passing a single identifier or null (to use defaults). -5. **Test Thoroughly**: +6. **Test Thoroughly**: The changes to identifier management and URL checking are significant. Test all authentication flows after upgrading. diff --git a/docs/es/authenticators.rst b/docs/es/authenticators.rst index 12d4a74c..963e8b9c 100644 --- a/docs/es/authenticators.rst +++ b/docs/es/authenticators.rst @@ -18,13 +18,8 @@ Opciones de configuración: - **sessionKey**: Key para los datos de usuario, por defecto es ``Auth`` -- **identify**: Establezca esta key con un valor ``true`` para permitir la verificación de las - credenciales de sesión con los identificadores. Cuando es ``true``, los - :doc:`/identifiers` configurados se utilizan para identificar al usuario utilizando los datos - almacenados en la sesión en cada request. El valor predeterminado es ``false``. - **fields**: Permite mapear el campo ``username`` al identificador único - en su almacenamiento de usuario. Por defecto es ``username``. Esta opción se utiliza cuando - la opción ``identify`` se establece en verdadero. + en su almacenamiento de usuario. Por defecto es ``username``. Form ==== diff --git a/docs/fr/authenticators.rst b/docs/fr/authenticators.rst index 246f81e4..4c6a85b7 100644 --- a/docs/fr/authenticators.rst +++ b/docs/fr/authenticators.rst @@ -19,16 +19,9 @@ Les options de configuration: - **sessionKey**: La clé de session pour les données de l'utilisateur, par défaut ``Auth``. -- **identify**: Définissez cette clé avec la valeur booléenne ``true`` pour - activer la confrontation des identifiants utilisateur contenus dans la - session avec les identificateurs (*identifiers*). Lorsque que la valeur est - ``true``, les :doc:`/identifiers` configurés sont utilisés à chaque requête - pour identifier l'utilisateur à partir des informations stockées en session. - La valeur par défaut est ``false``. - **fields**: Vous permet de mapper le champ ``username`` à l'identifiant unique dans votre système de stockage des utilisateurs. Vaut ``username`` par - défaut. Cette option est utilisée quand l'option ``identify`` est définie à - *true*. + défaut. Form ==== diff --git a/docs/ja/authenticators.rst b/docs/ja/authenticators.rst index bbf911c9..26c4cf9d 100644 --- a/docs/ja/authenticators.rst +++ b/docs/ja/authenticators.rst @@ -14,13 +14,8 @@ Authenticatorは、リクエストを認証操作に変換する処理を行い 設定オプション: - **sessionKey**: ユーザーのセッションキー, デフォルトは ``Auth`` -- **identify**: bool ``true`` の値を指定してこのキーを設定すると、 - セッションの認証情報を識別子と照合できるようになります。 - ``true`` の場合、設定された :doc:`/identifiers` はリクエストのたびにセッションに - 保存されたデータを使ってユーザを識別するために使われます。デフォルト値は ``false``. - **fields**: ``username`` フィールドをユーザストレージ内の一意の識別しに写像することができます。 デフォルトは ``username`` です。 - このオプションは ``identify`` オプションが true に設定されている場合に使用されます. `Form` ========= diff --git a/src/Authenticator/PrimaryKeySessionAuthenticator.php b/src/Authenticator/PrimaryKeySessionAuthenticator.php index a03f896f..c1963e8a 100644 --- a/src/Authenticator/PrimaryKeySessionAuthenticator.php +++ b/src/Authenticator/PrimaryKeySessionAuthenticator.php @@ -53,7 +53,6 @@ class PrimaryKeySessionAuthenticator extends SessionAuthenticator 'fields' => [], 'sessionKey' => 'Auth', 'impersonateSessionKey' => 'AuthImpersonate', - 'identify' => false, 'identityAttribute' => 'identity', 'identifierKey' => 'key', 'idField' => 'id', @@ -167,7 +166,6 @@ public function impersonate( } $session->write($impersonateSessionKey, $impersonator[$this->getConfig('idField')]); $session->write($sessionKey, $impersonated[$this->getConfig('idField')]); - $this->setConfig('identify', true); return [ 'request' => $request, diff --git a/src/Authenticator/SessionAuthenticator.php b/src/Authenticator/SessionAuthenticator.php index e5b573ee..84786d1d 100644 --- a/src/Authenticator/SessionAuthenticator.php +++ b/src/Authenticator/SessionAuthenticator.php @@ -33,9 +33,6 @@ 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 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. * * @var array */ @@ -45,7 +42,6 @@ class SessionAuthenticator extends AbstractAuthenticator implements PersistenceI ], 'sessionKey' => 'Auth', 'impersonateSessionKey' => 'AuthImpersonate', - 'identify' => false, 'identityAttribute' => 'identity', ]; @@ -85,18 +81,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); } - if ($this->getConfig('identify') === true) { - $credentials = []; - foreach ($this->getConfig('fields') as $key => $field) { - $credentials[$key] = $user[$field]; - } - $user = $this->getIdentifier()->identify($credentials); - - if (!$user) { - return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); - } - } - if (!($user instanceof ArrayAccess)) { $user = new ArrayObject($user); } @@ -168,7 +152,6 @@ public function impersonate( } $session->write($impersonateSessionKey, $impersonator); $session->write($sessionKey, $impersonated); - $this->setConfig('identify', true); return [ 'request' => $request, @@ -193,7 +176,6 @@ public function stopImpersonating(ServerRequestInterface $request, ResponseInter $identity = $session->read($impersonateSessionKey); $session->delete($impersonateSessionKey); $session->write($sessionKey, $identity); - $this->setConfig('identify', true); } return [ diff --git a/tests/TestCase/AuthenticationServiceTest.php b/tests/TestCase/AuthenticationServiceTest.php index 85f2afe5..479d70e6 100644 --- a/tests/TestCase/AuthenticationServiceTest.php +++ b/tests/TestCase/AuthenticationServiceTest.php @@ -30,7 +30,6 @@ use Cake\Http\Response; use Cake\Http\ServerRequest; use Cake\Http\ServerRequestFactory; -use Cake\I18n\DateTime; use Cake\Routing\Router; use InvalidArgumentException; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; @@ -166,56 +165,6 @@ public function testAuthenticateWithChallengeDisabled() $this->assertFalse($result->isValid()); } - /** - * Integration test for session auth + identify always getting a fresh user record. - * - * @return void - */ - public function testAuthenticationWithSessionIdentify() - { - $users = $this->fetchTable('Users'); - $user = $users->get(1); - - $request = ServerRequestFactory::fromGlobals([ - 'SERVER_NAME' => 'example.com', - 'REQUEST_URI' => '/testpath', - ]); - $request->getSession()->write('Auth', [ - 'username' => $user->username, - 'password' => $user->password, - ]); - - $factory = function () { - return new AuthenticationService([ - 'authenticators' => [ - 'Authentication.Session' => [ - 'identify' => true, - 'identifier' => 'Authentication.Password', - ], - ], - ]); - }; - $service = $factory(); - $result = $service->authenticate($request); - $this->assertTrue($result->isValid()); - - $dateValue = new DateTime('2022-01-01 10:11:12'); - $identity = $result->getData(); - $this->assertEquals($identity->username, $user->username); - $this->assertNotEquals($identity->created, $dateValue); - - // Update the user so that we can ensure session is reading from the db. - $user->created = $dateValue; - $users->saveOrFail($user); - - $service = $factory(); - $result = $service->authenticate($request); - $this->assertTrue($result->isValid()); - $identity = $result->getData(); - $this->assertEquals($identity->username, $user->username); - $this->assertEquals($identity->created, $dateValue); - } - /** * testLoadAuthenticatorException */ diff --git a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php index ce5014f5..e1dede5a 100644 --- a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php @@ -197,62 +197,6 @@ public function testAuthenticateFailure() $this->assertSame(Result::FAILURE_IDENTITY_NOT_FOUND, $result->getStatus()); } - /** - * Test successful session data verification by database lookup - * - * @return void - */ - public function testVerifyByDatabaseSuccess() - { - $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); - - $this->sessionMock->expects($this->once()) - ->method('read') - ->with('Auth') - ->willReturn([ - 'username' => 'mariano', - 'password' => 'h45h', - ]); - - $request = $request->withAttribute('session', $this->sessionMock); - - $authenticator = new SessionAuthenticator($this->identifier, [ - 'identify' => true, - ]); - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::SUCCESS, $result->getStatus()); - } - - /** - * Test session data verification by database lookup failure - * - * @return void - */ - public function testVerifyByDatabaseFailure() - { - $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); - - $this->sessionMock->expects($this->once()) - ->method('read') - ->with('Auth') - ->willReturn([ - 'username' => 'does-not', - 'password' => 'exist', - ]); - - $request = $request->withAttribute('session', $this->sessionMock); - - $authenticator = new SessionAuthenticator($this->identifier, [ - 'identify' => true, - ]); - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::FAILURE_CREDENTIALS_INVALID, $result->getStatus()); - } - /** * testPersistIdentity * From a3f120a33fc308bffc8f58667e747f9b60673b4f Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 1 Feb 2026 09:05:08 +0100 Subject: [PATCH 29/30] Clean up dormant code after identify option removal Remove fields config and constructor from SessionAuthenticator since they were only used by the removed identify logic. Remove redundant constructor override from PrimaryKeySessionAuthenticator. Simplify tests. --- docs/en/authenticators.rst | 2 - docs/es/authenticators.rst | 2 - docs/fr/authenticators.rst | 3 - docs/ja/authenticators.rst | 2 - .../PrimaryKeySessionAuthenticator.php | 15 --- src/Authenticator/SessionAuthenticator.php | 29 +---- .../SessionAuthenticatorTest.php | 111 ++---------------- 7 files changed, 12 insertions(+), 152 deletions(-) diff --git a/docs/en/authenticators.rst b/docs/en/authenticators.rst index c235eea3..bb1f509e 100644 --- a/docs/en/authenticators.rst +++ b/docs/en/authenticators.rst @@ -18,8 +18,6 @@ Configuration options: - **sessionKey**: The session key for the user data, default is ``Auth`` -- **fields**: Allows you to map the ``username`` field to the unique - identifier in your user storage. Defaults to ``username``. PrimaryKeySession ================= diff --git a/docs/es/authenticators.rst b/docs/es/authenticators.rst index 963e8b9c..e35861f3 100644 --- a/docs/es/authenticators.rst +++ b/docs/es/authenticators.rst @@ -18,8 +18,6 @@ Opciones de configuración: - **sessionKey**: Key para los datos de usuario, por defecto es ``Auth`` -- **fields**: Permite mapear el campo ``username`` al identificador único - en su almacenamiento de usuario. Por defecto es ``username``. Form ==== diff --git a/docs/fr/authenticators.rst b/docs/fr/authenticators.rst index 4c6a85b7..5b0f07d3 100644 --- a/docs/fr/authenticators.rst +++ b/docs/fr/authenticators.rst @@ -19,9 +19,6 @@ Les options de configuration: - **sessionKey**: La clé de session pour les données de l'utilisateur, par défaut ``Auth``. -- **fields**: Vous permet de mapper le champ ``username`` à l'identifiant - unique dans votre système de stockage des utilisateurs. Vaut ``username`` par - défaut. Form ==== diff --git a/docs/ja/authenticators.rst b/docs/ja/authenticators.rst index 26c4cf9d..834e8607 100644 --- a/docs/ja/authenticators.rst +++ b/docs/ja/authenticators.rst @@ -14,8 +14,6 @@ Authenticatorは、リクエストを認証操作に変換する処理を行い 設定オプション: - **sessionKey**: ユーザーのセッションキー, デフォルトは ``Auth`` -- **fields**: ``username`` フィールドをユーザストレージ内の一意の識別しに写像することができます。 - デフォルトは ``username`` です。 `Form` ========= diff --git a/src/Authenticator/PrimaryKeySessionAuthenticator.php b/src/Authenticator/PrimaryKeySessionAuthenticator.php index c1963e8a..eedbe961 100644 --- a/src/Authenticator/PrimaryKeySessionAuthenticator.php +++ b/src/Authenticator/PrimaryKeySessionAuthenticator.php @@ -58,21 +58,6 @@ class PrimaryKeySessionAuthenticator extends SessionAuthenticator 'idField' => 'id', ]; - /** - * Constructor - * - * Bypasses SessionAuthenticator's default PasswordIdentifier creation - * to allow lazy initialization of the TokenIdentifier in getIdentifier(). - * - * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. - * @param array $config Configuration settings. - */ - public function __construct(?IdentifierInterface $identifier, array $config = []) - { - $this->_identifier = $identifier; - $this->setConfig($config); - } - /** * Gets the identifier. * diff --git a/src/Authenticator/SessionAuthenticator.php b/src/Authenticator/SessionAuthenticator.php index 84786d1d..de945208 100644 --- a/src/Authenticator/SessionAuthenticator.php +++ b/src/Authenticator/SessionAuthenticator.php @@ -17,9 +17,6 @@ use ArrayAccess; use ArrayObject; -use Authentication\Identifier\IdentifierFactory; -use Authentication\Identifier\IdentifierInterface; -use Authentication\Identifier\PasswordIdentifier; use Cake\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -31,39 +28,19 @@ class SessionAuthenticator extends AbstractAuthenticator implements PersistenceI { /** * Default config for this object. - * - `fields` The fields to use to verify a user by. * - `sessionKey` Session key. + * - `impersonateSessionKey` Session key for impersonation. + * - `identityAttribute` Request attribute for the identity. * * @var array */ protected array $_defaultConfig = [ - 'fields' => [ - PasswordIdentifier::CREDENTIAL_USERNAME => 'username', - ], + 'fields' => [], 'sessionKey' => 'Auth', 'impersonateSessionKey' => 'AuthImpersonate', 'identityAttribute' => 'identity', ]; - /** - * Constructor - * - * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. - * @param array $config Configuration settings. - */ - public function __construct(?IdentifierInterface $identifier, array $config = []) - { - if ($identifier === null) { - $identifierConfig = []; - if (isset($config['fields'])) { - $identifierConfig['fields'] = $config['fields']; - } - $identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); - } - - parent::__construct($identifier, $config); - } - /** * Authenticate a user using session data. * diff --git a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php index e1dede5a..853aa809 100644 --- a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php @@ -19,8 +19,6 @@ use ArrayObject; use Authentication\Authenticator\Result; use Authentication\Authenticator\SessionAuthenticator; -use Authentication\Identifier\IdentifierFactory; -use Authentication\Identifier\PasswordIdentifier; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; @@ -42,11 +40,6 @@ class SessionAuthenticatorTest extends TestCase 'core.Users', ]; - /** - * @var \Authentication\Identifier\IdentifierInterface - */ - protected $identifier; - protected $sessionMock; /** @@ -56,8 +49,6 @@ public function setUp(): void { parent::setUp(); - $this->identifier = IdentifierFactory::create('Authentication.Password'); - $this->sessionMock = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() ->onlyMethods(['read', 'write', 'delete', 'renew', 'check']) @@ -83,91 +74,7 @@ public function testAuthenticateSuccess() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifier); - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $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(null, [ - '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(null, [ - '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(null, [ - 'identifier' => IdentifierFactory::create('Authentication.Password'), - ]); + $authenticator = new SessionAuthenticator(null); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -190,7 +97,7 @@ public function testAuthenticateFailure() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -207,7 +114,7 @@ public function testPersistIdentity() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $data = new ArrayObject(['username' => 'florian']); @@ -250,7 +157,7 @@ public function testClearIdentity() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $this->sessionMock->expects($this->once()) ->method('delete') @@ -279,7 +186,7 @@ public function testImpersonate() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $AuthUsers = TableRegistry::getTableLocator()->get('AuthUsers'); $impersonator = $AuthUsers->newEntity([ 'username' => 'mariano', @@ -318,7 +225,7 @@ public function testImpersonateAlreadyImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $impersonator = new ArrayObject([ 'username' => 'mariano', 'password' => 'password', @@ -352,7 +259,7 @@ public function testStopImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $impersonator = new ArrayObject([ 'username' => 'mariano', @@ -399,7 +306,7 @@ public function testStopImpersonatingNotImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $this->sessionMock->expects($this->once()) ->method('check') @@ -436,7 +343,7 @@ public function testIsImpersonating() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifier); + $authenticator = new SessionAuthenticator(null); $this->sessionMock->expects($this->once()) ->method('check') From 8faadabacd15c3e9dc3f5e95c7b32a2bfc153a8a Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 1 Feb 2026 09:09:07 +0100 Subject: [PATCH 30/30] Make identifier parameter optional in AbstractAuthenticator Default to null so authenticators that don't need an identifier (like SessionAuthenticator) can be constructed without arguments. --- src/Authenticator/AbstractAuthenticator.php | 2 +- .../Authenticator/SessionAuthenticatorTest.php | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Authenticator/AbstractAuthenticator.php b/src/Authenticator/AbstractAuthenticator.php index a384bcb4..343b1d21 100644 --- a/src/Authenticator/AbstractAuthenticator.php +++ b/src/Authenticator/AbstractAuthenticator.php @@ -52,7 +52,7 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(?IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier = null, array $config = []) { $this->_identifier = $identifier; $this->setConfig($config); diff --git a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php index 853aa809..88ef913b 100644 --- a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php @@ -74,7 +74,7 @@ public function testAuthenticateSuccess() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -97,7 +97,7 @@ public function testAuthenticateFailure() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -114,7 +114,7 @@ public function testPersistIdentity() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $data = new ArrayObject(['username' => 'florian']); @@ -157,7 +157,7 @@ public function testClearIdentity() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $this->sessionMock->expects($this->once()) ->method('delete') @@ -186,7 +186,7 @@ public function testImpersonate() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $AuthUsers = TableRegistry::getTableLocator()->get('AuthUsers'); $impersonator = $AuthUsers->newEntity([ 'username' => 'mariano', @@ -225,7 +225,7 @@ public function testImpersonateAlreadyImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $impersonator = new ArrayObject([ 'username' => 'mariano', 'password' => 'password', @@ -259,7 +259,7 @@ public function testStopImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $impersonator = new ArrayObject([ 'username' => 'mariano', @@ -306,7 +306,7 @@ public function testStopImpersonatingNotImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $this->sessionMock->expects($this->once()) ->method('check') @@ -343,7 +343,7 @@ public function testIsImpersonating() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator(null); + $authenticator = new SessionAuthenticator(); $this->sessionMock->expects($this->once()) ->method('check')