From 7f8277e40d21bd32394ff796301d931604c1dbc1 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 6 Nov 2025 05:44:28 +0100 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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); + } +}