diff --git a/docs/en/upgrade-3-to-4.rst b/docs/en/upgrade-3-to-4.rst new file mode 100644 index 00000000..b465eb9c --- /dev/null +++ b/docs/en/upgrade-3-to-4.rst @@ -0,0 +1,315 @@ +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 and Restructured +------------------------------------- + +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' => [ + 'controller' => 'Users', + 'action' => 'login', + ], + ]); + + // 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 + + // DefaultUrlChecker is now hardcoded (formerly CakeRouterUrlChecker) + $service->loadAuthenticator('Authentication.Form', [ + 'loginUrl' => [ + 'controller' => 'Users', + 'action' => 'login', + ], + ]); + + // For framework-agnostic projects, explicitly use GenericUrlChecker + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Generic', + 'loginUrl' => '/users/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 Removed +---------------------- + +URL Checkers +^^^^^^^^^^^^ + +**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 is Now CakePHP-Based +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``DefaultUrlChecker`` is now the CakePHP checker (formerly ``CakeRouterUrlChecker``). +It requires CakePHP Router and supports both string and array URLs. + +The 3.x framework-agnostic ``DefaultUrlChecker`` has been renamed to ``GenericUrlChecker``. + +.. code-block:: php + + // DefaultUrlChecker now requires CakePHP Router + $checker = new DefaultUrlChecker(); + $checker->check($request, ['controller' => 'Users', 'action' => 'login']); // Works + $checker->check($request, '/users/login'); // Also works + + // For framework-agnostic usage: + $checker = new GenericUrlChecker(); + $checker->check($request, '/users/login'); // Works + $checker->check($request, ['controller' => 'Users']); // Throws exception + +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'`` → 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', + ]); + +3. **Multiple Login URLs**: + + If you have multiple login URLs, add ``'urlChecker' => 'Authentication.Multi'`` + to your authenticator configuration. + +4. **Custom Identifier Setup**: + + If you were passing ``IdentifierCollection`` to authenticators, switch to + either passing a single identifier or null (to use defaults). + +5. **Test Thoroughly**: + + 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 f27a8532..6e7db9db 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 ================= DefaultUrlChecker ----------------- -The default checker allows you to compare an URL by regex or string -URLs. +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): + +.. 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``. + +GenericUrlChecker +------------------ + +Framework-agnostic checker for string URLs. Supports regex matching. +Use this for non-CakePHP projects. + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.Generic', + 'loginUrl' => '/users/login', + ]); + +Using regex: + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => [ + 'className' => 'Authentication.Generic', + '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``. 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..06c2287d 100644 --- a/src/Authenticator/AbstractAuthenticator.php +++ b/src/Authenticator/AbstractAuthenticator.php @@ -16,10 +16,11 @@ */ namespace Authentication\Authenticator; -use Authentication\Identifier\AbstractIdentifier; use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Cake\Core\InstanceConfigTrait; use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; abstract class AbstractAuthenticator implements AuthenticatorInterface { @@ -33,13 +34,13 @@ 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 */ @@ -48,11 +49,21 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface /** * Constructor * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @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 = []) + 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); } 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..53b35de9 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); diff --git a/src/Authenticator/EnvironmentAuthenticator.php b/src/Authenticator/EnvironmentAuthenticator.php index 40959a00..7c8bb653 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,19 @@ 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 = []) + { + $identifier ??= IdentifierFactory::create('Authentication.Callback'); + + parent::__construct($identifier, $config); + } + /** * Get values from the environment variables configured by `fields`. * diff --git a/src/Authenticator/FormAuthenticator.php b/src/Authenticator/FormAuthenticator.php index f784c483..f1b73dd3 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; @@ -42,31 +42,29 @@ class FormAuthenticator extends AbstractAuthenticator */ protected array $_defaultConfig = [ 'loginUrl' => null, - 'urlChecker' => 'Authentication.Default', + 'urlChecker' => null, '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); diff --git a/src/Authenticator/HttpBasicAuthenticator.php b/src/Authenticator/HttpBasicAuthenticator.php index 715e002f..ba9e0a76 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); @@ -84,8 +82,8 @@ public function authenticate(ServerRequestInterface $request): ResultInterface } $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..e344e969 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)) { @@ -95,7 +95,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface } $user = $this->_identifier->identify([ - AbstractIdentifier::CREDENTIAL_USERNAME => $digest['username'], + PasswordIdentifier::CREDENTIAL_USERNAME => $digest['username'], ]); if (!$user) { @@ -106,7 +106,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..2a91c8d6 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,12 @@ 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']); - } + $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'])) { @@ -99,8 +97,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])) { diff --git a/src/Authenticator/PrimaryKeySessionAuthenticator.php b/src/Authenticator/PrimaryKeySessionAuthenticator.php index 0de92fbd..aacb3fa1 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', diff --git a/src/Authenticator/SessionAuthenticator.php b/src/Authenticator/SessionAuthenticator.php index a6c98dea..3814ec09 100644 --- a/src/Authenticator/SessionAuthenticator.php +++ b/src/Authenticator/SessionAuthenticator.php @@ -17,7 +17,9 @@ use ArrayAccess; use ArrayObject; -use Authentication\Identifier\AbstractIdentifier; +use Authentication\Identifier\IdentifierFactory; +use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Cake\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -39,7 +41,7 @@ class SessionAuthenticator extends AbstractAuthenticator implements PersistenceI */ protected array $_defaultConfig = [ 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', + PasswordIdentifier::CREDENTIAL_USERNAME => 'username', ], 'sessionKey' => 'Auth', 'impersonateSessionKey' => 'AuthImpersonate', @@ -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. * diff --git a/src/Authenticator/TokenAuthenticator.php b/src/Authenticator/TokenAuthenticator.php index d02e26b5..541a3194 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,15 +40,13 @@ 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']); - } + $identifier ??= IdentifierFactory::create('Authentication.Token'); parent::__construct($identifier, $config); } 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 deleted file mode 100644 index 401c85f2..00000000 --- a/src/UrlChecker/CakeRouterUrlChecker.php +++ /dev/null @@ -1,66 +0,0 @@ - false, - ]; - - /** - * @inheritDoc - */ - public function check(ServerRequestInterface $request, $loginUrls, array $options = []): bool - { - $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.'); - } - - // 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; - } -} diff --git a/src/UrlChecker/DefaultUrlChecker.php b/src/UrlChecker/DefaultUrlChecker.php index f148a02f..97e7dec9 100644 --- a/src/UrlChecker/DefaultUrlChecker.php +++ b/src/UrlChecker/DefaultUrlChecker.php @@ -16,83 +16,50 @@ */ namespace Authentication\UrlChecker; +use Cake\Routing\Router; use Psr\Http\Message\ServerRequestInterface; /** - * Checks if a request object contains a valid URL + * 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 + * @var array */ protected array $_defaultOptions = [ - 'useRegex' => false, 'checkFullUrl' => false, ]; /** * @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); - - $urls = (array)$loginUrls; - if (!$urls) { - return true; - } - - $checker = $this->_getChecker($options); - $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']); - foreach ($urls as $validUrl) { - if ($checker($validUrl, $url)) { - return true; - } - } + // Support both string URLs and array-based routes (like Router::url()) + $validUrl = Router::url($loginUrls, $options['checkFullUrl']); - return false; + 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 new file mode 100644 index 00000000..ea525fdd --- /dev/null +++ b/src/UrlChecker/MultiUrlChecker.php @@ -0,0 +1,118 @@ + + */ + protected array $_defaultOptions = [ + 'useRegex' => false, + 'checkFullUrl' => false, + ]; + + /** + * @inheritDoc + */ + public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool + { + $options = $this->_mergeDefaultOptions($options); + + // 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; + } + + foreach ($urls as $url) { + if ($this->_checkSingleUrl($request, $url, $options)) { + return true; + } + } + + 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 + * + * @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 + { + $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..d6d6758f 100644 --- a/src/UrlChecker/UrlCheckerTrait.php +++ b/src/UrlChecker/UrlCheckerTrait.php @@ -33,9 +33,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'), ); } @@ -48,12 +53,15 @@ 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'])) { + + // If no explicit className is set (or it's null/empty), use DefaultUrlChecker + if (empty($options['className'])) { $options['className'] = DefaultUrlChecker::class; } 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..e4c0fe0d 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,8 @@ public function testMultipleLoginUrlMismatch() Router::createRouteBuilder('/') ->connect('/{lang}/secure', ['controller' => 'Users', 'action' => 'login']); - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ + 'urlChecker' => 'Authentication.Multi', 'loginUrl' => [ ['lang' => 'en', 'controller' => 'Users', 'action' => 'login'], ['lang' => 'de', 'controller' => 'Users', 'action' => 'login'], @@ -295,7 +290,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 +316,8 @@ public function testSingleLoginUrlSuccess() */ public function testMultipleLoginUrlSuccess() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ + 'urlChecker' => 'Authentication.Multi', 'loginUrl' => [ '/en/secure', '/de/secure', @@ -351,7 +347,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 +375,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.Generic', 'useRegex' => true, ], 'fields' => [ @@ -409,9 +406,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.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -440,9 +438,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.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -472,7 +471,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 +498,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 +525,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..2f5cd6c9 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,8 +170,8 @@ public function testMultipleLoginUrlMismatch() Router::createRouteBuilder('/') ->connect('/{lang}/users/login', ['controller' => 'Users', 'action' => 'login']); - $form = new FormAuthenticator($identifiers, [ - 'urlChecker' => 'Authentication.CakeRouter', + $form = new FormAuthenticator($identifier, [ + 'urlChecker' => 'Authentication.Multi', 'loginUrl' => [ ['lang' => 'en', 'controller' => 'Users', 'action' => 'login'], ['lang' => 'de', '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,11 +253,12 @@ public function testMultipleLoginUrlSuccess() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => [ '/en/users/login', '/de/users/login', ], + 'urlChecker' => 'Authentication.Multi', ]); $result = $form->authenticate($request); @@ -291,9 +275,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 +284,7 @@ public function testLoginUrlSuccessWithBase() ); $request = $request->withAttribute('base', '/base'); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/base/users/login', ]); @@ -320,9 +302,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 +310,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.Generic', 'useRegex' => true, ], ]); @@ -351,9 +332,7 @@ public function testRegexLoginUrlSuccess() */ public function testFullRegexLoginUrlFailure() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( [ @@ -363,9 +342,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.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -385,9 +365,7 @@ public function testFullRegexLoginUrlFailure() */ public function testFullRegexLoginUrlSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( [ @@ -398,9 +376,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.Generic', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -420,9 +399,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,8 +407,11 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => 'http://localhost/users/login', + 'urlChecker' => [ + 'className' => 'Authentication.Generic', + ], ]); $result = $form->authenticate($request); @@ -448,7 +428,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 +436,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 +444,7 @@ public function testAuthenticateCustomFields() ], ]); - $identifiers->expects($this->once()) + $identifier->expects($this->once()) ->method('identify') ->with([ 'username' => 'mariano@cakephp.org', @@ -485,7 +465,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 +473,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 +498,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 +506,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 +524,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 +532,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 +545,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()); - } -} diff --git a/tests/TestCase/UrlChecker/CakeRouterUrlCheckerTest.php b/tests/TestCase/UrlChecker/CakeRouterUrlCheckerTest.php deleted file mode 100644 index f8ec480e..00000000 --- a/tests/TestCase/UrlChecker/CakeRouterUrlCheckerTest.php +++ /dev/null @@ -1,230 +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 CakeRouterUrlChecker(); - $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 CakeRouterUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], - ); - $result = $checker->check($request, $url, [ - 'checkFullUrl' => true, - ]); - $this->assertTrue($result); - - $checker = new CakeRouterUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/invalid'], - ); - $result = $checker->check($request, $url, [ - 'checkFullUrl' => true, - ]); - $this->assertFalse($result); - - $checker = new CakeRouterUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/login'], - ); - $result = $checker->check($request, ['_name' => 'secureLogin'], [ - 'checkFullUrl' => true, - ]); - $this->assertFalse($result); - - $checker = new CakeRouterUrlChecker(); - $request = ServerRequestFactory::fromGlobals( - [ - 'REQUEST_URI' => '/login', - 'SERVER_NAME' => 'auth.localhost', - ], - ); - $result = $checker->check($request, ['_name' => 'secureLogin'], [ - 'checkFullUrl' => true, - ]); - $this->assertTrue($result); - } - - /** - * testEmptyUrl - * - * @return void - */ - public function testEmptyUrl() - { - $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'], - ); - $result = $checker->check($request, []); - $this->assertFalse($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'], - ); - $result = $checker->check($request, '/users/login'); - $this->assertFalse($result); - } - - /** - * testNamedRoute - * - * @return void - */ - public function testNamedRoute() - { - $checker = new CakeRouterUrlChecker(); - $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 CakeRouterUrlChecker(); - $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..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,89 +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, [ - '/users/login', - '/admin/login', + 'controller' => 'Users', + 'action' => 'login', ]); - $this->assertTrue($result); + $this->assertFalse($result); } /** - * testCheckArray + * checkFullUrls * * @return void */ - public function testCheckArray() + 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); - $result = $checker->check($request, [ - '/users/login', - '/admin/login', + $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' => '/login'], + ); + $result = $checker->check($request, ['_name' => 'secureLogin'], [ + 'checkFullUrl' => true, + ]); + $this->assertFalse($result); + + $checker = new DefaultUrlChecker(); + $request = ServerRequestFactory::fromGlobals( + [ + 'REQUEST_URI' => '/login', + 'SERVER_NAME' => 'auth.localhost', + ], + ); + $result = $checker->check($request, ['_name' => 'secureLogin'], [ + 'checkFullUrl' => true, ]); $this->assertTrue($result); } /** - * testCheckRegexp + * testStringUrl - CakeUrlChecker now accepts strings too * * @return void */ - public function testCheckRegexp() + public function testStringUrl() { $checker = new DefaultUrlChecker(); $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/en/users/login'], + ['REQUEST_URI' => '/users/login'], ); - - $result = $checker->check($request, '%^/[a-z]{2}/users/login/?$%', [ - 'useRegex' => 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); } /** - * testCheckFull + * testNamedRoute * * @return void */ - public function testCheckFull() + public function testNamedRoute() { $checker = new DefaultUrlChecker(); $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/users/login'], + ['REQUEST_URI' => '/login'], ); - - $result = $checker->check($request, 'http://localhost/users/login', [ - 'checkFullUrl' => true, - ]); + $result = $checker->check($request, ['_name' => 'login']); $this->assertTrue($result); } /** - * testCheckBase - * - * @return void + * testInvalidNamedRoute */ - public function testCheckBase() + public function testInvalidNamedRoute() { + $this->expectException('Cake\Routing\Exception\MissingRouteException'); $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, - ]); - $this->assertTrue($result); + $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); + } +}