diff --git a/composer.json b/composer.json index 8f74bcc9..f1cb210e 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,12 @@ "issues": "https://github.com/cakephp/authentication/issues", "forum": "https://discourse.cakephp.org/", "source": "https://github.com/cakephp/authentication", - "docs": "https://book.cakephp.org/authentication/3/en/" + "docs": "https://book.cakephp.org/authentication/4/en/" }, "require": { "php": ">=8.1", "cakephp/http": "^5.0", + "cakephp/utility": "^5.0", "laminas/laminas-diactoros": "^3.0", "psr/http-client": "^1.0", "psr/http-message": "^1.1 || ^2.0", @@ -34,7 +35,7 @@ "require-dev": { "cakephp/cakephp": "^5.1.0", "cakephp/cakephp-codesniffer": "^5.0", - "firebase/php-jwt": "^6.2", + "firebase/php-jwt": "^7.0", "phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.4" }, "suggest": { diff --git a/docs/en/authenticators.rst b/docs/en/authenticators.rst index 7c1b4953..bb1f509e 100644 --- a/docs/en/authenticators.rst +++ b/docs/en/authenticators.rst @@ -18,10 +18,6 @@ Configuration options: - **sessionKey**: The session key for the user data, default is ``Auth`` -- **identify**: Deprecated in 3.4.0. Use ``PrimaryKeySessionAuthenticator`` - instead if you need to fetch fresh user data from the database on each request. -- **fields**: Allows you to map the ``username`` field to the unique - identifier in your user storage. Defaults to ``username``. PrimaryKeySession ================= @@ -34,23 +30,34 @@ It also helps to avoid session invalidation. Session itself stores the entity object including nested objects like DateTime or enums. With only the ID stored, the invalidation due to objects being modified will also dissolve. -Make sure to match this with a Token identifier with ``key``/``id`` keys:: +A default ``TokenIdentifier`` is provided that looks up users by their ``id`` field, +so minimal configuration is required:: + + $service->loadAuthenticator('Authentication.PrimaryKeySession'); + +Configuration options: + +- **idField**: The field in the database table to look up. Default is ``id``. +- **identifierKey**: The key used to store/retrieve the primary key from session data. + Default is ``key``. + +For custom lookup fields, the ``idField`` and ``identifierKey`` options propagate +to the default identifier automatically:: + + $service->loadAuthenticator('Authentication.PrimaryKeySession', [ + 'idField' => 'uuid', + ]); + +You can also provide a fully custom identifier configuration if needed:: $service->loadAuthenticator('Authentication.PrimaryKeySession', [ 'identifier' => [ 'Authentication.Token' => [ - 'tokenField' => 'id', // lookup for resolver and DB table - 'dataField' => 'key', // incoming data from authenticator + 'tokenField' => 'id', + 'dataField' => 'key', 'resolver' => 'Authentication.Orm', ], ], - 'urlChecker' => 'Authentication.CakeRouter', - 'loginUrl' => [ - 'prefix' => false, - 'plugin' => false, - 'controller' => 'Users', - 'action' => 'login', - ], ]); Form @@ -140,7 +147,7 @@ example. If provided will be used instead of the secret key. You need to add the lib `firebase/php-jwt `_ -v6.2 or above to your app to use the ``JwtAuthenticator``. +v7.0 or above to your app to use the ``JwtAuthenticator``. By default the ``JwtAuthenticator`` uses ``HS256`` symmetric key algorithm and uses the value of ``Cake\Utility\Security::salt()`` as encryption key. @@ -427,7 +434,7 @@ There is only one event that is fired by authentication: ``Authentication.afterIdentify``. If you don’t know what events are and how to use them `check the -documentation `__. +documentation `__. The ``Authentication.afterIdentify`` event is fired by the ``AuthenticationComponent`` after an identity was successfully diff --git a/docs/en/identifiers.rst b/docs/en/identifiers.rst index 5c673fa9..9db0e9aa 100644 --- a/docs/en/identifiers.rst +++ b/docs/en/identifiers.rst @@ -172,7 +172,7 @@ Configuration options: - **userModel**: The user model identities are located in. Default is ``Users``. - **finder**: The finder to use with the model. Default is ``all``. - You can read more about model finders `here `__. + You can read more about model finders `here `__. In order to use ORM resolver you must require ``cakephp/orm`` in your ``composer.json`` file (if you are not already using the full CakePHP framework). diff --git a/docs/en/index.rst b/docs/en/index.rst index 01f22317..36de148f 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -8,22 +8,18 @@ Project's ROOT directory (where the **composer.json** file is located) php composer.phar require cakephp/authentication -Version 3 of the Authentication Plugin is compatible with CakePHP 5. +Version 4 of the Authentication Plugin is compatible with CakePHP 5. -Load the plugin by adding the following statement in your project's ``src/Application.php``:: +Load the plugin using the following command:: - public function bootstrap(): void - { - parent::bootstrap(); - - $this->addPlugin('Authentication'); - } +.. code-block:: shell + bin/cake plugin load Authentication Getting Started =============== -The authentication plugin integrates with your application as a `middleware `_. It can also +The authentication plugin integrates with your application as a `middleware `_. It can also be used as a component to make unauthenticated access simpler. First, let's apply the middleware. In **src/Application.php**, add the following to the class imports:: diff --git a/docs/en/upgrade-3-to-4.rst b/docs/en/upgrade-3-to-4.rst new file mode 100644 index 00000000..d254f724 --- /dev/null +++ b/docs/en/upgrade-3-to-4.rst @@ -0,0 +1,341 @@ +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', + ]; + +SessionAuthenticator ``identify`` Option Removed +------------------------------------------------- + +The deprecated ``identify`` option has been removed from ``SessionAuthenticator``. +Use ``PrimaryKeySessionAuthenticator`` instead if you need to fetch fresh user +data from the database on each request. + +**Before (3.x):** + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Session', [ + 'identify' => true, + 'identifier' => 'Authentication.Password', + ]); + +**After (4.x):** + +.. code-block:: php + + $service->loadAuthenticator('Authentication.PrimaryKeySession'); + +URL Checker Renamed and Restructured +------------------------------------- + +URL checkers have been completely restructured: + +- ``CakeRouterUrlChecker`` has been renamed to ``DefaultUrlChecker`` +- The old ``DefaultUrlChecker`` has been renamed to ``StringUrlChecker`` +- 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 + $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 string-only URL checking, explicitly use StringUrlChecker + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.String', + '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. + +- **4.x default:** Always uses ``DefaultUrlChecker`` (formerly ``CakeUrlChecker``) +- **String URLs only:** Must explicitly configure ``StringUrlChecker`` +- **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 ``DefaultUrlChecker`` has been renamed to ``StringUrlChecker``. + +.. 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 string URL only usage: + $checker = new StringUrlChecker(); + $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. **Session Identify**: + + If you used ``'identify' => true`` on ``SessionAuthenticator``, switch to + ``PrimaryKeySessionAuthenticator`` which always fetches fresh data. + +2. **Search and Replace**: + + - ``AbstractIdentifier::CREDENTIAL_`` → ``PasswordIdentifier::CREDENTIAL_`` + - ``IdentifierCollection`` → ``IdentifierFactory`` + - ``'Authentication.CakeRouter'`` → Remove (no longer needed, default is now CakePHP-based) + - ``CakeRouterUrlChecker`` → ``DefaultUrlChecker`` + - Old 3.x ``DefaultUrlChecker`` → ``StringUrlChecker`` + +3. **String URL Checking**: + + If you want to use string-only URL checking, explicitly configure + ``StringUrlChecker``: + + .. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.String', + 'loginUrl' => '/users/login', + ]); + +4. **Multiple Login URLs**: + + If you have multiple login URLs, add ``'urlChecker' => 'Authentication.Multi'`` + to your authenticator configuration. + +5. **Custom Identifier Setup**: + + If you were passing ``IdentifierCollection`` to authenticators, switch to + either passing a single identifier or null (to use defaults). + +6. **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..fc6a33e2 100644 --- a/docs/en/url-checkers.rst +++ b/docs/en/url-checkers.rst @@ -1,9 +1,11 @@ URL Checkers ############ -To provide an abstract and framework agnostic solution there are URL -checkers implemented that allow you to customize the comparison of the -current URL if needed. For example to another frameworks routing. +There are URL checkers implemented that allow you to customize the comparison +of the current URL if needed. + +All checkers support single URLs in either string or array format (like ``Router::url()``). +For multiple login URLs, use ``MultiUrlChecker``. Included Checkers ================= @@ -11,41 +13,103 @@ 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``. + +StringUrlChecker +----------------- + +Checker for string URLs. Supports regex matching. + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => 'Authentication.String', + 'loginUrl' => '/users/login', + ]); + +Using regex: + +.. code-block:: php + + $service->loadAuthenticator('Authentication.Form', [ + 'urlChecker' => [ + 'className' => 'Authentication.String', + '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/docs/es/authenticators.rst b/docs/es/authenticators.rst index 12d4a74c..e35861f3 100644 --- a/docs/es/authenticators.rst +++ b/docs/es/authenticators.rst @@ -18,13 +18,6 @@ Opciones de configuración: - **sessionKey**: Key para los datos de usuario, por defecto es ``Auth`` -- **identify**: Establezca esta key con un valor ``true`` para permitir la verificación de las - credenciales de sesión con los identificadores. Cuando es ``true``, los - :doc:`/identifiers` configurados se utilizan para identificar al usuario utilizando los datos - almacenados en la sesión en cada request. El valor predeterminado es ``false``. -- **fields**: Permite mapear el campo ``username`` al identificador único - en su almacenamiento de usuario. Por defecto es ``username``. Esta opción se utiliza cuando - la opción ``identify`` se establece en verdadero. Form ==== diff --git a/docs/es/identifiers.rst b/docs/es/identifiers.rst index 76e117d0..eb13b0b8 100644 --- a/docs/es/identifiers.rst +++ b/docs/es/identifiers.rst @@ -163,7 +163,7 @@ Opciones de configuración: - **userModel**: El modelo donde están localizadas las indentidades. Por defecto es ``Users``. - **finder**: El finder a usar con el modelo. Por defecto es ``all``. - Puede leer mas sobre los finders de los modelos `aquí `__. + Puede leer mas sobre los finders de los modelos `aquí `__. Para usar el resolver ORM se requiere tener ``cakephp/orm`` en su archivo ``composer.json`` (si no estás usando el framework CakePHP completo). diff --git a/docs/es/index.rst b/docs/es/index.rst index 206e9514..3c86e3a6 100644 --- a/docs/es/index.rst +++ b/docs/es/index.rst @@ -21,7 +21,7 @@ Carge el plugin agregando la siguiente declaración en ``src/Application.php``:: Empezando ========= -El plugin authentication se integra con su aplicación como un `middleware `_. También, se +El plugin authentication se integra con su aplicación como un `middleware `_. También, se puede utilizar como un componente para simplificar el acceso no autenticado. Primero aplique el middleware. En **src/Application.php**, agregue las siguientes importaciones de clase:: diff --git a/docs/fr/authenticators.rst b/docs/fr/authenticators.rst index 246f81e4..5b0f07d3 100644 --- a/docs/fr/authenticators.rst +++ b/docs/fr/authenticators.rst @@ -19,16 +19,6 @@ Les options de configuration: - **sessionKey**: La clé de session pour les données de l'utilisateur, par défaut ``Auth``. -- **identify**: Définissez cette clé avec la valeur booléenne ``true`` pour - activer la confrontation des identifiants utilisateur contenus dans la - session avec les identificateurs (*identifiers*). Lorsque que la valeur est - ``true``, les :doc:`/identifiers` configurés sont utilisés à chaque requête - pour identifier l'utilisateur à partir des informations stockées en session. - La valeur par défaut est ``false``. -- **fields**: Vous permet de mapper le champ ``username`` à l'identifiant - unique dans votre système de stockage des utilisateurs. Vaut ``username`` par - défaut. Cette option est utilisée quand l'option ``identify`` est définie à - *true*. Form ==== diff --git a/docs/fr/identifiers.rst b/docs/fr/identifiers.rst index fac152cb..2936398b 100644 --- a/docs/fr/identifiers.rst +++ b/docs/fr/identifiers.rst @@ -169,7 +169,7 @@ Options de configuration: Par défaut ``Users``. - **finder**: Le finder à utiliser avec le modèle. Par défaut ``all``. Pour en savoir plus sur les finders de modèle, consultez - `cette documentation `__. + `cette documentation `__. Afin d'utiliser le résolveur ORM, vous devez requérir ``cakephp/orm`` dans votre fichier ``composer.json`` (si vous n'utilisez pas déjà le framework CakePHP diff --git a/docs/fr/index.rst b/docs/fr/index.rst index fefd8587..e216f554 100644 --- a/docs/fr/index.rst +++ b/docs/fr/index.rst @@ -9,7 +9,7 @@ répertoire ROOT de votre projet CakePHP (là où se trouve le fichier php composer.phar require cakephp/authentication -La version 3 du Plugin Authentication est compatible avec CakePHP 5. +La version 4 du Plugin Authentication est compatible avec CakePHP 5. Chargez le plugin en ajoutant l'instruction suivante dans le fichier ``src/Application.php`` de votre projet:: diff --git a/docs/ja/authenticators.rst b/docs/ja/authenticators.rst index bbf911c9..834e8607 100644 --- a/docs/ja/authenticators.rst +++ b/docs/ja/authenticators.rst @@ -14,13 +14,6 @@ Authenticatorは、リクエストを認証操作に変換する処理を行い 設定オプション: - **sessionKey**: ユーザーのセッションキー, デフォルトは ``Auth`` -- **identify**: bool ``true`` の値を指定してこのキーを設定すると、 - セッションの認証情報を識別子と照合できるようになります。 - ``true`` の場合、設定された :doc:`/identifiers` はリクエストのたびにセッションに - 保存されたデータを使ってユーザを識別するために使われます。デフォルト値は ``false``. -- **fields**: ``username`` フィールドをユーザストレージ内の一意の識別しに写像することができます。 - デフォルトは ``username`` です。 - このオプションは ``identify`` オプションが true に設定されている場合に使用されます. `Form` ========= diff --git a/docs/ja/index.rst b/docs/ja/index.rst index 6023daa0..574317d4 100644 --- a/docs/ja/index.rst +++ b/docs/ja/index.rst @@ -22,7 +22,7 @@ CakePHPから `composer `_ でプラグインをイン はじめに =============== -認証プラグインは、ミドルウェアとしてアプリケーションと統合します。 `middleware `_ +認証プラグインは、ミドルウェアとしてアプリケーションと統合します。 `middleware `_ また、認証されていないアクセスをより簡単にするためのコンポーネントとして使用することもできます。 まずはミドルウェアを適用してみましょう。 **src/Application.php** に以下のクラスを追加します。 diff --git a/readme.md b/readme.md index c63fb39c..9f5dc5bb 100644 --- a/readme.md +++ b/readme.md @@ -39,4 +39,3 @@ Documentation for this plugin can be found in the [CakePHP Cookbook](https://boo There are IdeHelper tasks in [IdeHelperExtra plugin](https://github.com/dereuromark/cakephp-ide-helper-extra/) to provide auto-complete: - `AuthenticationService::loadAuthenticator()` -- `IdentifierCollection::load()` diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index c9d03513..2f526337 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 @@ -112,7 +100,6 @@ class AuthenticationService implements AuthenticationServiceInterface, Impersona */ protected array $_defaultConfig = [ 'authenticators' => [], - 'identifiers' => [], 'identityClass' => Identity::class, 'identityAttribute' => 'identity', 'queryParam' => null, @@ -135,20 +122,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 * @@ -157,9 +130,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; @@ -177,24 +149,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} * @@ -309,12 +263,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..343b1d21 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,25 +34,25 @@ abstract class AbstractAuthenticator implements AuthenticatorInterface */ protected array $_defaultConfig = [ 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', - AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', + PasswordIdentifier::CREDENTIAL_USERNAME => 'username', + PasswordIdentifier::CREDENTIAL_PASSWORD => 'password', ], ]; /** - * Identifier or identifiers collection. + * Identifier instance. * - * @var \Authentication\Identifier\IdentifierInterface + * @var \Authentication\Identifier\IdentifierInterface|null */ - protected IdentifierInterface $_identifier; + protected ?IdentifierInterface $_identifier = null; /** * Constructor * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier = null, array $config = []) { $this->_identifier = $identifier; $this->setConfig($config); @@ -60,10 +61,23 @@ public function __construct(IdentifierInterface $identifier, array $config = []) /** * Gets the identifier. * + * Subclasses can override this method to provide a default identifier + * when none was configured, enabling lazy initialization. + * * @return \Authentication\Identifier\IdentifierInterface + * @throws \RuntimeException When identifier is null. */ public function getIdentifier(): IdentifierInterface { + if ($this->_identifier === null) { + throw new RuntimeException( + sprintf( + 'Identifier is required for `%s`. Please provide an identifier instance.', + static::class, + ), + ); + } + return $this->_identifier; } diff --git a/src/Authenticator/AuthenticatorCollection.php b/src/Authenticator/AuthenticatorCollection.php index 5e4249d0..461a6600 100644 --- a/src/Authenticator/AuthenticatorCollection.php +++ b/src/Authenticator/AuthenticatorCollection.php @@ -17,42 +17,15 @@ namespace Authentication\Authenticator; use Authentication\AbstractCollection; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; use Cake\Core\App; use RuntimeException; -use function Cake\Core\deprecationWarning; /** * @extends \Authentication\AbstractCollection<\Authentication\Authenticator\AuthenticatorInterface> */ class AuthenticatorCollection extends AbstractCollection { - /** - * Identifier collection. - * - * @var \Authentication\Identifier\IdentifierCollection - */ - protected IdentifierCollection $_identifiers; - - /** - * Constructor. - * - * @param \Authentication\Identifier\IdentifierCollection $identifiers Identifiers collection. - * @param array $config Config array. - */ - public function __construct(IdentifierCollection $identifiers, array $config = []) - { - $this->_identifiers = $identifiers; - if ($identifiers->count() > 0) { - deprecationWarning( - '3.3.0', - 'loadIdentifier() usage is deprecated. Directly pass `\'identifier\'` config to the Authenticator.', - ); - } - - parent::__construct($config); - } - /** * Creates authenticator instance. * @@ -65,11 +38,12 @@ public function __construct(IdentifierCollection $identifiers, array $config = [ protected function _create(object|string $class, string $alias, array $config): AuthenticatorInterface { if (is_string($class)) { + $identifier = null; if (!empty($config['identifier'])) { - $this->_identifiers = new IdentifierCollection((array)$config['identifier']); + $identifier = IdentifierFactory::create($config['identifier']); } - return new $class($this->_identifiers, $config); + return new $class($identifier, $config); } return $class; diff --git a/src/Authenticator/CookieAuthenticator.php b/src/Authenticator/CookieAuthenticator.php index 580aec6b..a95a9ab9 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,18 +60,18 @@ class CookieAuthenticator extends AbstractAuthenticator implements PersistenceIn /** * Gets the identifier, loading a default Password identifier if none configured. * - * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator(). + * This is done lazily to allow configuration to be fully set before creating the identifier. * * @return \Authentication\Identifier\IdentifierInterface */ public function getIdentifier(): IdentifierInterface { - if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) { + if ($this->_identifier === null) { $identifierConfig = []; if ($this->getConfig('fields')) { $identifierConfig['fields'] = $this->getConfig('fields'); } - $this->_identifier->load('Authentication.Password', $identifierConfig); + $this->_identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); } return $this->_identifier; diff --git a/src/Authenticator/EnvironmentAuthenticator.php b/src/Authenticator/EnvironmentAuthenticator.php index 40959a00..d0b0f370 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`. * @@ -153,10 +168,11 @@ public function authenticate(ServerRequestInterface $request): ResultInterface $data = array_merge($this->_getOptionalData($request), $data); - $user = $this->_identifier->identify($data); + $identifier = $this->getIdentifier(); + $user = $identifier->identify($data); if (!$user) { - return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors()); + return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $identifier->getErrors()); } return new Result($user, Result::SUCCESS); diff --git a/src/Authenticator/FormAuthenticator.php b/src/Authenticator/FormAuthenticator.php index 781b4e70..0c48a0b9 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,28 +42,28 @@ 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', ], ]; /** * Gets the identifier, loading a default Password identifier if none configured. * - * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator(). + * This is done lazily to allow configuration to be fully set before creating the identifier. * * @return \Authentication\Identifier\IdentifierInterface */ public function getIdentifier(): IdentifierInterface { - if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) { + if ($this->_identifier === null) { $identifierConfig = []; if ($this->getConfig('fields')) { $identifierConfig['fields'] = $this->getConfig('fields'); } - $this->_identifier->load('Authentication.Password', $identifierConfig); + $this->_identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); } return $this->_identifier; diff --git a/src/Authenticator/HttpBasicAuthenticator.php b/src/Authenticator/HttpBasicAuthenticator.php index 7d5c2b8d..ccda9ded 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,18 +46,18 @@ class HttpBasicAuthenticator extends AbstractAuthenticator implements StatelessI /** * Gets the identifier, loading a default Password identifier if none configured. * - * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator(). + * This is done lazily to allow configuration to be fully set before creating the identifier. * * @return \Authentication\Identifier\IdentifierInterface */ public function getIdentifier(): IdentifierInterface { - if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) { + if ($this->_identifier === null) { $identifierConfig = []; if ($this->getConfig('fields')) { $identifierConfig['fields'] = $this->getConfig('fields'); } - $this->_identifier->load('Authentication.Password', $identifierConfig); + $this->_identifier = IdentifierFactory::create('Authentication.Password', $identifierConfig); } return $this->_identifier; @@ -81,8 +81,8 @@ public function authenticate(ServerRequestInterface $request): ResultInterface } $user = $this->getIdentifier()->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..340c2cb7 100644 --- a/src/Authenticator/HttpDigestAuthenticator.php +++ b/src/Authenticator/HttpDigestAuthenticator.php @@ -15,8 +15,8 @@ */ namespace Authentication\Authenticator; -use Authentication\Identifier\AbstractIdentifier; use Authentication\Identifier\IdentifierInterface; +use Authentication\Identifier\PasswordIdentifier; use Cake\Utility\Security; use InvalidArgumentException; use Psr\Http\Message\ServerRequestInterface; @@ -55,10 +55,10 @@ class HttpDigestAuthenticator extends HttpBasicAuthenticator * - `opaque` A string that must be returned unchanged by clients. * Defaults to `md5($config['realm'])` * - * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier instance. + * @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance. * @param array $config Configuration settings. */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { $secret = ''; if (class_exists(Security::class)) { @@ -94,8 +94,8 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); } - $user = $this->_identifier->identify([ - AbstractIdentifier::CREDENTIAL_USERNAME => $digest['username'], + $user = $this->getIdentifier()->identify([ + 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 3ce212e3..56b0e8d9 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,7 +55,7 @@ class JwtAuthenticator extends TokenAuthenticator /** * @inheritDoc */ - public function __construct(IdentifierInterface $identifier, array $config = []) + public function __construct(?IdentifierInterface $identifier, array $config = []) { parent::__construct($identifier, $config); @@ -70,17 +70,13 @@ public function __construct(IdentifierInterface $identifier, array $config = []) /** * Gets the identifier, loading a default JwtSubject identifier if none configured. * - * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator(). + * This is done lazily to allow configuration to be fully set before creating the identifier. * * @return \Authentication\Identifier\IdentifierInterface */ public function getIdentifier(): IdentifierInterface { - if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) { - $this->_identifier->load('Authentication.JwtSubject'); - } - - return $this->_identifier; + return $this->_identifier ??= IdentifierFactory::create('Authentication.JwtSubject'); } /** @@ -109,8 +105,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..eedbe961 100644 --- a/src/Authenticator/PrimaryKeySessionAuthenticator.php +++ b/src/Authenticator/PrimaryKeySessionAuthenticator.php @@ -4,6 +4,7 @@ namespace Authentication\Authenticator; use ArrayAccess; +use Authentication\Identifier\IdentifierFactory; use Authentication\Identifier\IdentifierInterface; use Cake\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; @@ -11,21 +12,71 @@ /** * Session Authenticator with only ID + * + * This authenticator stores only the user's primary key in the session, + * and looks up the full user record from the database on each request. + * + * By default, it uses a TokenIdentifier configured to look up users by + * their `id` field. This works out of the box for most applications: + * + * ```php + * $service->loadAuthenticator('Authentication.PrimaryKeySession'); + * ``` + * + * You can customize the identifier configuration if needed: + * + * ```php + * $service->loadAuthenticator('Authentication.PrimaryKeySession', [ + * 'identifier' => [ + * 'className' => 'Authentication.Token', + * 'tokenField' => 'uuid', + * 'dataField' => 'key', + * 'resolver' => [ + * 'className' => 'Authentication.Orm', + * 'userModel' => 'Members', + * ], + * ], + * ]); + * ``` */ class PrimaryKeySessionAuthenticator extends SessionAuthenticator { /** - * @param \Authentication\Identifier\IdentifierInterface $identifier - * @param array $config + * Default config for this object. + * + * - `identifierKey` The key used when passing the ID to the identifier. + * - `idField` The field on the user entity that contains the primary key. + * + * @var array */ - public function __construct(IdentifierInterface $identifier, array $config = []) + protected array $_defaultConfig = [ + 'fields' => [], + 'sessionKey' => 'Auth', + 'impersonateSessionKey' => 'AuthImpersonate', + 'identityAttribute' => 'identity', + 'identifierKey' => 'key', + 'idField' => 'id', + ]; + + /** + * Gets the identifier. + * + * If no identifier was explicitly configured, creates a default TokenIdentifier + * configured to look up users by their primary key (`id` field). + * + * @return \Authentication\Identifier\IdentifierInterface + */ + public function getIdentifier(): IdentifierInterface { - $config += [ - 'identifierKey' => 'key', - 'idField' => 'id', - ]; + if ($this->_identifier === null) { + $this->_identifier = IdentifierFactory::create([ + 'className' => 'Authentication.Token', + 'tokenField' => $this->getConfig('idField'), + 'dataField' => $this->getConfig('identifierKey'), + ]); + } - parent::__construct($identifier, $config); + return $this->_identifier; } /** @@ -45,7 +96,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); } - $user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]); + $user = $this->getIdentifier()->identify([$this->getConfig('identifierKey') => $userId]); if (!$user) { return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); } @@ -100,7 +151,6 @@ public function impersonate( } $session->write($impersonateSessionKey, $impersonator[$this->getConfig('idField')]); $session->write($sessionKey, $impersonated[$this->getConfig('idField')]); - $this->setConfig('identify', true); return [ 'request' => $request, diff --git a/src/Authenticator/SessionAuthenticator.php b/src/Authenticator/SessionAuthenticator.php index 32c7f983..de945208 100644 --- a/src/Authenticator/SessionAuthenticator.php +++ b/src/Authenticator/SessionAuthenticator.php @@ -17,11 +17,9 @@ use ArrayAccess; use ArrayObject; -use Authentication\Identifier\AbstractIdentifier; use Cake\Http\Exception\UnauthorizedException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use function Cake\Core\deprecationWarning; /** * Session Authenticator @@ -30,21 +28,16 @@ class SessionAuthenticator extends AbstractAuthenticator implements PersistenceI { /** * Default config for this object. - * - `fields` The fields to use to verify a user by. * - `sessionKey` Session key. - * - `identify` Whether to identify user data stored in a session. - * Deprecated: Use `PrimaryKeySessionAuthenticator` instead if you - * need to fetch fresh user data from the database on each request. + * - `impersonateSessionKey` Session key for impersonation. + * - `identityAttribute` Request attribute for the identity. * * @var array */ protected array $_defaultConfig = [ - 'fields' => [ - AbstractIdentifier::CREDENTIAL_USERNAME => 'username', - ], + 'fields' => [], 'sessionKey' => 'Auth', 'impersonateSessionKey' => 'AuthImpersonate', - 'identify' => false, 'identityAttribute' => 'identity', ]; @@ -65,23 +58,6 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); } - if ($this->getConfig('identify') === true) { - deprecationWarning( - '3.4.0', - 'The `identify` option is deprecated. ' . - 'Use `PrimaryKeySessionAuthenticator` instead to fetch fresh user data on each request.', - ); - $credentials = []; - foreach ($this->getConfig('fields') as $key => $field) { - $credentials[$key] = $user[$field]; - } - $user = $this->_identifier->identify($credentials); - - if (!$user) { - return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); - } - } - if (!($user instanceof ArrayAccess)) { $user = new ArrayObject($user); } @@ -153,7 +129,6 @@ public function impersonate( } $session->write($impersonateSessionKey, $impersonator); $session->write($sessionKey, $impersonated); - $this->setConfig('identify', true); return [ 'request' => $request, @@ -178,7 +153,6 @@ public function stopImpersonating(ServerRequestInterface $request, ResponseInter $identity = $session->read($impersonateSessionKey); $session->delete($impersonateSessionKey); $session->write($sessionKey, $identity); - $this->setConfig('identify', true); } return [ diff --git a/src/Authenticator/TokenAuthenticator.php b/src/Authenticator/TokenAuthenticator.php index 3751ac82..b2afbc36 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,17 +40,13 @@ class TokenAuthenticator extends AbstractAuthenticator implements StatelessInter /** * Gets the identifier, loading a default Token identifier if none configured. * - * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator(). + * This is done lazily to allow configuration to be fully set before creating the identifier. * * @return \Authentication\Identifier\IdentifierInterface */ public function getIdentifier(): IdentifierInterface { - if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) { - $this->_identifier->load('Authentication.Token'); - } - - return $this->_identifier; + return $this->_identifier ??= IdentifierFactory::create('Authentication.Token'); } /** diff --git a/src/Identifier/AbstractIdentifier.php b/src/Identifier/AbstractIdentifier.php index e2e36e36..f8aa9c79 100644 --- a/src/Identifier/AbstractIdentifier.php +++ b/src/Identifier/AbstractIdentifier.php @@ -22,10 +22,6 @@ abstract class AbstractIdentifier implements IdentifierInterface { use InstanceConfigTrait; - public const CREDENTIAL_USERNAME = 'username'; - - public const CREDENTIAL_PASSWORD = 'password'; - /** * Default configuration * diff --git a/src/Identifier/IdentifierCollection.php b/src/Identifier/IdentifierCollection.php deleted file mode 100644 index 257b25d8..00000000 --- a/src/Identifier/IdentifierCollection.php +++ /dev/null @@ -1,132 +0,0 @@ - - */ -class IdentifierCollection extends AbstractCollection implements IdentifierInterface -{ - /** - * Errors - * - * @var array - */ - protected array $_errors = []; - - /** - * Identifier that successfully Identified the identity. - * - * @var \Authentication\Identifier\IdentifierInterface|null - */ - protected ?IdentifierInterface $_successfulIdentifier = null; - - /** - * Identifies an user or service by the passed credentials - * - * @param array $credentials Authentication credentials - * @return \ArrayAccess|array|null - */ - public function identify(array $credentials): ArrayAccess|array|null - { - /** @var \Authentication\Identifier\IdentifierInterface $identifier */ - foreach ($this->_loaded as $name => $identifier) { - $result = $identifier->identify($credentials); - if ($result) { - $this->_successfulIdentifier = $identifier; - - return $result; - } - - $errors = $identifier->getErrors(); - if ($errors) { - $this->_errors[$name] = $identifier->getErrors(); - } - } - - $this->_successfulIdentifier = null; - - return null; - } - - /** - * Creates identifier instance. - * - * @param \Authentication\Identifier\IdentifierInterface|class-string<\Authentication\Identifier\IdentifierInterface> $class Identifier class. - * @param string $alias Identifier alias. - * @param array $config Config array. - * @return \Authentication\Identifier\IdentifierInterface - * @throws \RuntimeException - */ - protected function _create(object|string $class, string $alias, array $config): IdentifierInterface - { - if (is_object($class)) { - return $class; - } - - return new $class($config); - } - - /** - * Get errors - * - * @return array - */ - public function getErrors(): array - { - return $this->_errors; - } - - /** - * Resolves identifier class name. - * - * @param string $class Class name to be resolved. - * @return class-string<\Authentication\Identifier\IdentifierInterface>|null - */ - protected function _resolveClassName(string $class): ?string - { - /** @var class-string<\Authentication\Identifier\IdentifierInterface>|null */ - return App::className($class, 'Identifier', 'Identifier'); - } - - /** - * @param string $class Missing class. - * @param string $plugin Class plugin. - * @return void - * @throws \RuntimeException - */ - protected function _throwMissingClassError(string $class, ?string $plugin): void - { - $message = sprintf('Identifier class `%s` was not found.', $class); - throw new RuntimeException($message); - } - - /** - * Gets the successful identifier instance if one was successful after calling identify. - * - * @return \Authentication\Identifier\IdentifierInterface|null - */ - public function getIdentificationProvider(): ?IdentifierInterface - { - return $this->_successfulIdentifier; - } -} diff --git a/src/Identifier/IdentifierFactory.php b/src/Identifier/IdentifierFactory.php new file mode 100644 index 00000000..83161dfc --- /dev/null +++ b/src/Identifier/IdentifierFactory.php @@ -0,0 +1,66 @@ +|string $config Identifier configuration. + * Can be a class name string, an instance, or an array with 'className' key. + * @param array $defaultConfig Default configuration to merge. + * @return \Authentication\Identifier\IdentifierInterface + * @throws \RuntimeException When the identifier class cannot be found or created. + */ + public static function create( + string|array|IdentifierInterface $config, + array $defaultConfig = [], + ): IdentifierInterface { + if ($config instanceof IdentifierInterface) { + return $config; + } + + if (is_string($config)) { + $className = $config; + $config = []; + } else { + $className = $config['className'] ?? ''; + unset($config['className']); + } + + if (empty($className)) { + throw new RuntimeException('Identifier configuration must specify a class name.'); + } + + $config += $defaultConfig; + + /** @var class-string<\Authentication\Identifier\IdentifierInterface>|null $class */ + $class = App::className($className, 'Identifier', 'Identifier'); + if ($class === null) { + throw new RuntimeException(sprintf('Identifier class `%s` was not found.', $className)); + } + + return new $class($config); + } +} diff --git a/src/Identifier/IdentifierInterface.php b/src/Identifier/IdentifierInterface.php index adf29c22..7883fb72 100644 --- a/src/Identifier/IdentifierInterface.php +++ b/src/Identifier/IdentifierInterface.php @@ -21,7 +21,7 @@ interface IdentifierInterface { /** - * Identifies an user or service by the passed credentials + * Identifies a user or service by the passed credentials * * @param array $credentials Authentication credentials * @return \ArrayAccess|array|null diff --git a/src/Identifier/LdapIdentifier.php b/src/Identifier/LdapIdentifier.php index 70398149..c80076d2 100644 --- a/src/Identifier/LdapIdentifier.php +++ b/src/Identifier/LdapIdentifier.php @@ -46,6 +46,10 @@ */ class LdapIdentifier extends AbstractIdentifier { + public const CREDENTIAL_USERNAME = 'username'; + + public const CREDENTIAL_PASSWORD = 'password'; + /** * Default configuration * diff --git a/src/Identifier/PasswordIdentifier.php b/src/Identifier/PasswordIdentifier.php index 45c39353..964f352e 100644 --- a/src/Identifier/PasswordIdentifier.php +++ b/src/Identifier/PasswordIdentifier.php @@ -47,6 +47,10 @@ class PasswordIdentifier extends AbstractIdentifier } use ResolverAwareTrait; + public const CREDENTIAL_USERNAME = 'username'; + + public const CREDENTIAL_PASSWORD = 'password'; + /** * Default configuration. * - `fields` The fields to use to identify a user by: diff --git a/src/Identity.php b/src/Identity.php index 0a699fce..18d5281b 100644 --- a/src/Identity.php +++ b/src/Identity.php @@ -19,6 +19,7 @@ use ArrayAccess; use BadMethodCallException; use Cake\Core\InstanceConfigTrait; +use Cake\Utility\Hash; /** * Identity object @@ -90,23 +91,27 @@ public function __isset(string $field): bool } /** - * Get data from the identity + * Get data from the identity. * - * @param string $field Field in the user data. + * You can use dot notation to fetch nested data. + * Calling the method without any argument will return + * the entire data array/object (same as `getOriginalData()`). + * + * @param string|null $field Field in the user data. * @return mixed */ - public function get(string $field): mixed + public function get(?string $field = null): mixed { + if ($field === null) { + return $this->data; + } + $map = $this->_config['fieldMap']; if (isset($map[$field])) { $field = $map[$field]; } - if (isset($this->data[$field])) { - return $this->data[$field]; - } - - return null; + return Hash::get($this->data, $field); } /** diff --git a/src/Plugin.php b/src/Plugin.php deleted file mode 100644 index 40c4da55..00000000 --- a/src/Plugin.php +++ /dev/null @@ -1,23 +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/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/StringUrlChecker.php b/src/UrlChecker/StringUrlChecker.php new file mode 100644 index 00000000..15f3ac9c --- /dev/null +++ b/src/UrlChecker/StringUrlChecker.php @@ -0,0 +1,112 @@ + + */ + 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.', + ); + } + + $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/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/src/View/Helper/IdentityHelper.php b/src/View/Helper/IdentityHelper.php index 6c2dc947..88da0cd7 100644 --- a/src/View/Helper/IdentityHelper.php +++ b/src/View/Helper/IdentityHelper.php @@ -103,7 +103,11 @@ public function is(int|string $id, string $field = 'id'): bool } /** - * Gets user data + * Get data from the identity. + * + * You can use dot notation to fetch nested data. + * Calling the method without any argument will return + * the entire data array/object (same as `IdentityInterface::getOriginalData()`). * * @param string|null $key Key of something you want to get from the identity data * @return mixed @@ -120,4 +124,14 @@ public function get(?string $key = null): mixed return Hash::get($this->_identity, $key); } + + /** + * Returns the identity instance. + * + * @return \Authentication\IdentityInterface|null + */ + public function getIdentity(): ?IdentityInterface + { + return $this->_identity; + } } diff --git a/tests/TestCase/AuthenticationServiceTest.php b/tests/TestCase/AuthenticationServiceTest.php index 2b96b8b8..479d70e6 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; @@ -31,11 +30,9 @@ use Cake\Http\Response; use Cake\Http\ServerRequest; use Cake\Http\ServerRequestFactory; -use Cake\I18n\DateTime; use Cake\Routing\Router; use InvalidArgumentException; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; -use PHPUnit\Runner\Version; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -168,65 +165,6 @@ public function testAuthenticateWithChallengeDisabled() $this->assertFalse($result->isValid()); } - /** - * Integration test for session auth + identify always getting a fresh user record. - * - * @return void - * @deprecated The `identify` option is deprecated. - */ - public function testAuthenticationWithSessionIdentify() - { - $this->skipIf( - version_compare(Version::id(), '11.0', '<'), - 'For some reason PHPUnit doesn\'t pick up the deprecation on v10', - ); - - $users = $this->fetchTable('Users'); - $user = $users->get(1); - - $request = ServerRequestFactory::fromGlobals([ - 'SERVER_NAME' => 'example.com', - 'REQUEST_URI' => '/testpath', - ]); - $request->getSession()->write('Auth', [ - 'username' => $user->username, - 'password' => $user->password, - ]); - - $factory = function () { - return new AuthenticationService([ - 'authenticators' => [ - 'Authentication.Session' => [ - 'identify' => true, - 'identifier' => 'Authentication.Password', - ], - ], - ]); - }; - - $this->deprecated(function () use ($factory, $request, $users, $user) { - $service = $factory(); - $result = $service->authenticate($request); - $this->assertTrue($result->isValid()); - - $dateValue = new DateTime('2022-01-01 10:11:12'); - $identity = $result->getData(); - $this->assertEquals($identity->username, $user->username); - $this->assertNotEquals($identity->created, $dateValue); - - // Update the user so that we can ensure session is reading from the db. - $user->created = $dateValue; - $users->saveOrFail($user); - - $service = $factory(); - $result = $service->authenticate($request); - $this->assertTrue($result->isValid()); - $identity = $result->getData(); - $this->assertEquals($identity->username, $user->username); - $this->assertEquals($identity->created, $dateValue); - }); - } - /** * testLoadAuthenticatorException */ @@ -237,37 +175,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 * @@ -1384,62 +1291,4 @@ public function testFormAuthenticatorDefaultIdentifier() $authenticator = $service->getAuthenticationProvider(); $this->assertInstanceOf(FormAuthenticator::class, $authenticator); } - - /** - * Test that loadIdentifier called after loadAuthenticator still works. - * - * This is a regression test for https://github.com/cakephp/authentication/issues/754 - * When loadAuthenticator was called before loadIdentifier, the authenticator would - * create its own default identifier collection and ignore the later loadIdentifier call. - * - * @deprecated Note that this test will be removed in 4.x as this is only to keep BC in 3.x. - * @return void - */ - public function testLoadIdentifierAfterLoadAuthenticator() - { - $this->skipIf( - version_compare(Version::id(), '11.0', '<'), - 'For some reason PHPUnit doesn\'t pick up the deprecation on v10', - ); - - $request = ServerRequestFactory::fromGlobals( - ['REQUEST_URI' => '/testpath'], - [], - ['username' => 'mariano', 'password' => 'password'], - ); - - $service = new AuthenticationService(); - - // Load authenticator FIRST - $service->loadAuthenticator('Authentication.Form'); - - // Then load identifier with custom resolver config - $this->deprecated(function () use ($service) { - $service->loadIdentifier('Authentication.Password', [ - 'resolver' => [ - 'className' => 'Authentication.Orm', - 'userModel' => 'AuthUsers', - 'finder' => 'auth', - ], - ]); - }); - - // The authenticator should use the identifier we loaded, not its default - $formAuth = $service->authenticators()->get('Form'); - $identifierCollection = $formAuth->getIdentifier(); - $this->assertInstanceOf(IdentifierCollection::class, $identifierCollection); - - $passwordIdentifier = $identifierCollection->get('Password'); - $this->assertInstanceOf(PasswordIdentifier::class, $passwordIdentifier); - - // Verify the resolver config was applied - $resolverConfig = $passwordIdentifier->getConfig('resolver'); - $this->assertIsArray($resolverConfig); - $this->assertSame('AuthUsers', $resolverConfig['userModel']); - $this->assertSame('auth', $resolverConfig['finder']); - - // Verify authentication actually works with the custom config - $result = $service->authenticate($request); - $this->assertTrue($result->isValid()); - } } diff --git a/tests/TestCase/Authenticator/AuthenticatorCollectionTest.php b/tests/TestCase/Authenticator/AuthenticatorCollectionTest.php index 35ac283d..96d23e0b 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; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; @@ -33,8 +32,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', ], @@ -50,8 +48,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', ]); @@ -65,10 +62,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')); } @@ -77,8 +73,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'); } @@ -89,8 +84,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', [ @@ -106,10 +100,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 6d37b067..16b16990 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; @@ -41,9 +41,9 @@ class EnvironmentAuthenticatorTest extends TestCase /** * Identifiers * - * @var \Authentication\Identifier\IdentifierCollection + * @var \Authentication\Identifier\IdentifierInterface */ - public $identifiers; + public $identifier; /** * @inheritDoc @@ -52,11 +52,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', ]); } @@ -67,18 +65,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', @@ -104,7 +100,7 @@ public function testAuthenticate() */ public function testFailedAuthentication() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -130,7 +126,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); @@ -144,7 +140,7 @@ public function testWithoutFieldConfig() */ public function testWithIncorrectFieldConfig() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'INCORRECT_USER_ID', @@ -170,7 +166,7 @@ public function testWithIncorrectFieldConfig() */ public function testCredentialsEmpty() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -196,18 +192,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', @@ -237,7 +231,7 @@ public function testOptionalFields() */ public function testSingleLoginUrlMismatch() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/secure', 'fields' => [ 'USER_ID', @@ -267,7 +261,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'], @@ -297,7 +292,7 @@ public function testMultipleLoginUrlMismatch() */ public function testSingleLoginUrlSuccess() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/en/secure', 'fields' => [ 'USER_ID', @@ -323,7 +318,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', @@ -353,7 +349,7 @@ public function testMultipleLoginUrlSuccess() */ public function testLoginUrlSuccessWithBase() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => '/base/fr/secure', 'fields' => [ 'USER_ID', @@ -381,9 +377,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.String', 'useRegex' => true, ], 'fields' => [ @@ -411,9 +408,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.String', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -442,9 +440,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.String', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -474,7 +473,7 @@ public function testFullRegexLoginUrlSuccess() */ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() { - $envAuth = new EnvironmentAuthenticator($this->identifiers, [ + $envAuth = new EnvironmentAuthenticator($this->identifier, [ 'loginUrl' => 'http://localhost/secure', 'fields' => [ 'USER_ID', @@ -501,8 +500,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', @@ -529,8 +527,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 faffcafd..b0bd80b7 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; @@ -45,9 +46,7 @@ class FormAuthenticatorTest extends TestCase */ public function testAuthenticate() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/testpath'], @@ -55,7 +54,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); @@ -69,9 +68,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'], @@ -79,7 +76,7 @@ public function testCredentialsNotPresent() [], ); - $form = new FormAuthenticator($identifiers); + $form = new FormAuthenticator($identifier); $result = $form->authenticate($request); @@ -95,9 +92,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'], @@ -105,7 +100,7 @@ public function testCredentialsEmpty() ['username' => '', 'password' => ''], ); - $form = new FormAuthenticator($identifiers); + $form = new FormAuthenticator($identifier); $result = $form->authenticate($request); @@ -116,9 +111,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'], @@ -126,7 +119,7 @@ public function testIdentityNotFound() ['username' => 'non-existent', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers); + $form = new FormAuthenticator($identifier); $result = $form->authenticate($request); @@ -142,9 +135,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'], @@ -152,7 +143,7 @@ public function testSingleLoginUrlMismatch() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', ]); @@ -170,9 +161,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'], @@ -183,8 +172,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'], @@ -205,9 +194,7 @@ public function testMultipleLoginUrlMismatch() */ public function testLoginUrlMismatchWithBase() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -216,7 +203,7 @@ public function testLoginUrlMismatchWithBase() ); $request = $request->withAttribute('base', '/base'); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', ]); @@ -234,9 +221,7 @@ public function testLoginUrlMismatchWithBase() */ public function testSingleLoginUrlSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/Users/login'], @@ -244,7 +229,7 @@ public function testSingleLoginUrlSuccess() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/Users/login', ]); @@ -262,9 +247,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'], @@ -272,11 +255,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); @@ -293,9 +277,7 @@ public function testMultipleLoginUrlSuccess() */ public function testLoginUrlSuccessWithBase() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -304,7 +286,7 @@ public function testLoginUrlSuccessWithBase() ); $request = $request->withAttribute('base', '/base'); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/base/users/login', ]); @@ -322,9 +304,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'], @@ -332,9 +312,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.String', 'useRegex' => true, ], ]); @@ -353,9 +334,7 @@ public function testRegexLoginUrlSuccess() */ public function testFullRegexLoginUrlFailure() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( [ @@ -365,9 +344,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.String', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -387,9 +367,7 @@ public function testFullRegexLoginUrlFailure() */ public function testFullRegexLoginUrlSuccess() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( [ @@ -400,9 +378,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.String', 'useRegex' => true, 'checkFullUrl' => true, ], @@ -422,9 +401,7 @@ public function testFullRegexLoginUrlSuccess() */ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() { - $identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); + $identifier = IdentifierFactory::create('Authentication.Password'); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -432,8 +409,11 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() ['username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => 'http://localhost/users/login', + 'urlChecker' => [ + 'className' => 'Authentication.String', + ], ]); $result = $form->authenticate($request); @@ -450,7 +430,7 @@ public function testFullLoginUrlFailureWithoutCheckFullUrlOption() */ public function testAuthenticateCustomFields() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -458,7 +438,7 @@ public function testAuthenticateCustomFields() ['email' => 'mariano@cakephp.org', 'secret' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', 'fields' => [ 'username' => 'email', @@ -466,7 +446,7 @@ public function testAuthenticateCustomFields() ], ]); - $identifiers->expects($this->once()) + $identifier->expects($this->once()) ->method('identify') ->with([ 'username' => 'mariano@cakephp.org', @@ -487,7 +467,7 @@ public function testAuthenticateCustomFields() */ public function testAuthenticateValidData() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -495,11 +475,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', @@ -520,7 +500,7 @@ public function testAuthenticateValidData() */ public function testAuthenticateMissingChecker() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -528,7 +508,7 @@ public function testAuthenticateMissingChecker() ['id' => 1, 'username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', 'urlChecker' => 'Foo', ]); @@ -546,7 +526,7 @@ public function testAuthenticateMissingChecker() */ public function testAuthenticateInvalidChecker() { - $identifiers = $this->createMock(IdentifierCollection::class); + $identifier = $this->createMock(IdentifierInterface::class); $request = ServerRequestFactory::fromGlobals( ['REQUEST_URI' => '/users/login'], @@ -554,7 +534,7 @@ public function testAuthenticateInvalidChecker() ['id' => 1, 'username' => 'mariano', 'password' => 'password'], ); - $form = new FormAuthenticator($identifiers, [ + $form = new FormAuthenticator($identifier, [ 'loginUrl' => '/users/login', 'urlChecker' => self::class, ]); @@ -575,26 +555,22 @@ public function testAuthenticateInvalidChecker() */ 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); + // FormAuthenticator should automatically configure a Password identifier when null is passed + $form = new FormAuthenticator(null); $result = $form->authenticate($request); $this->assertInstanceOf(Result::class, $result); $this->assertSame(Result::SUCCESS, $result->getStatus()); - // Verify the identifier collection now has the Password identifier + // Verify the identifier was lazily created $identifier = $form->getIdentifier(); - $this->assertInstanceOf(IdentifierCollection::class, $identifier); - $this->assertFalse($identifier->isEmpty()); + $this->assertInstanceOf(IdentifierInterface::class, $identifier); } /** @@ -604,32 +580,19 @@ public function testDefaultPasswordIdentifier() */ public function testExplicitIdentifierNotOverridden() { - // Create an IdentifierCollection with a specific identifier - $identifiers = new IdentifierCollection([ - 'Password' => [ - 'className' => 'Authentication.Password', - 'fields' => [ - 'username' => 'email', - 'password' => 'password', - ], + // Create an identifier explicitly + $identifier = IdentifierFactory::create('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); + $form = new FormAuthenticator($identifier); // 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.'); + $this->assertSame($identifier, $form->getIdentifier(), 'Identifier should be the same.'); } /** @@ -639,9 +602,6 @@ public function testExplicitIdentifierNotOverridden() */ public function testDefaultIdentifierInheritsFieldsConfig() { - // Create an empty IdentifierCollection - $identifiers = new IdentifierCollection(); - // Configure authenticator with custom fields mapping // Also set a loginUrl that won't match, so authenticate() returns early // without actually trying to identify (which would require database access) @@ -654,8 +614,8 @@ public function testDefaultIdentifierInheritsFieldsConfig() ]; // FormAuthenticator should create default identifier with inherited fields - // The default identifier is loaded lazily when authenticate() is called - $form = new FormAuthenticator($identifiers, $config); + // The default identifier is loaded lazily when authenticate() or getIdentifier() is called + $form = new FormAuthenticator(null, $config); // Trigger the lazy loading by calling authenticate on a non-matching URL $request = ServerRequestFactory::fromGlobals( @@ -667,12 +627,10 @@ public function testDefaultIdentifierInheritsFieldsConfig() // Verify the identifier was created with the correct configuration $identifier = $form->getIdentifier(); - $this->assertInstanceOf(IdentifierCollection::class, $identifier); - $this->assertFalse($identifier->isEmpty()); + $this->assertInstanceOf(IdentifierInterface::class, $identifier); // Verify the fields are properly configured on the identifier - $passwordIdentifier = $identifier->get('Password'); - $this->assertEquals('user_name', $passwordIdentifier->getConfig('fields.username')); - $this->assertEquals('pass_word', $passwordIdentifier->getConfig('fields.password')); + $this->assertEquals('user_name', $identifier->getConfig('fields.username')); + $this->assertEquals('pass_word', $identifier->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..cdfb44b9 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 @@ -79,12 +79,12 @@ public function setUp(): void 'firstname' => 'larry', ]; - $this->tokenHS256 = JWT::encode($data, 'secretKey', 'HS256'); + $this->tokenHS256 = JWT::encode($data, 'secretKey0123456789secretKey0123456789', 'HS256'); $privKey1 = file_get_contents(__DIR__ . '/../../data/rsa1-private.pem'); $this->tokenRS256 = JWT::encode($data, $privKey1, 'RS256', 'jwk1'); - $this->identifiers = new IdentifierCollection([]); + $this->identifier = null; } /** @@ -99,8 +99,8 @@ public function testAuthenticateViaHeaderToken() ); $this->request = $this->request->withAddedHeader('Authorization', 'Bearer ' . $this->tokenHS256); - $authenticator = new JwtAuthenticator($this->identifiers, [ - 'secretKey' => 'secretKey', + $authenticator = new JwtAuthenticator($this->identifier, [ + 'secretKey' => 'secretKey0123456789secretKey0123456789', 'subjectKey' => 'subjectId', ]); @@ -122,8 +122,8 @@ public function testAuthenticateViaQueryParamToken() ['token' => $this->tokenHS256], ); - $authenticator = new JwtAuthenticator($this->identifiers, [ - 'secretKey' => 'secretKey', + $authenticator = new JwtAuthenticator($this->identifier, [ + 'secretKey' => 'secretKey0123456789secretKey0123456789', '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,8 +158,8 @@ public function testAuthenticationViaIdentifierAndSubject() 'firstname' => 'larry', ])); - $authenticator = new JwtAuthenticator($this->identifiers, [ - 'secretKey' => 'secretKey', + $authenticator = new JwtAuthenticator($this->identifier, [ + 'secretKey' => 'secretKey0123456789secretKey0123456789', '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,8 +241,8 @@ public function testInvalidToken() ['token' => 'should cause an exception'], ); - $authenticator = new JwtAuthenticator($this->identifiers, [ - 'secretKey' => 'secretKey', + $authenticator = new JwtAuthenticator($this->identifier, [ + 'secretKey' => 'secretKey0123456789secretKey0123456789', ]); $result = $authenticator->authenticate($this->request); @@ -267,8 +267,8 @@ public function testGetPayloadHS256() ['token' => $this->tokenHS256], ); - $authenticator = new JwtAuthenticator($this->identifiers, [ - 'secretKey' => 'secretKey', + $authenticator = new JwtAuthenticator($this->identifier, [ + 'secretKey' => 'secretKey0123456789secretKey0123456789', ]); $result = $authenticator->getPayload(); @@ -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..ce704aae 100644 --- a/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/PrimaryKeySessionAuthenticatorTest.php @@ -19,7 +19,8 @@ use ArrayObject; use Authentication\Authenticator\PrimaryKeySessionAuthenticator; use Authentication\Authenticator\Result; -use Authentication\Identifier\IdentifierCollection; +use Authentication\Identifier\IdentifierFactory; +use Authentication\Identifier\TokenIdentifier; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; use Cake\Http\ServerRequestFactory; @@ -35,12 +36,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 +56,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) @@ -66,7 +68,7 @@ public function setUp(): void } /** - * Test authentication + * Test authentication with explicit identifier * * @return void */ @@ -81,24 +83,70 @@ 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->identifier); + $result = $authenticator->authenticate($request); + + $this->assertInstanceOf(Result::class, $result); + $this->assertSame(Result::SUCCESS, $result->getStatus()); + } + + /** + * Test authentication works with default identifier (no explicit configuration) + * + * @return void + */ + public function testAuthenticateSuccessWithDefaultIdentifier() + { + $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); + + $this->sessionMock->expects($this->once()) + ->method('read') + ->with('Auth') + ->willReturn(1); - $authenticator = new PrimaryKeySessionAuthenticator($this->identifiers); + $request = $request->withAttribute('session', $this->sessionMock); + + // No identifier passed - should use the default TokenIdentifier + $authenticator = new PrimaryKeySessionAuthenticator(null); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); $this->assertSame(Result::SUCCESS, $result->getStatus()); } + /** + * Test getIdentifier returns default TokenIdentifier when none configured + * + * @return void + */ + public function testGetIdentifierReturnsDefaultWhenNotConfigured() + { + $authenticator = new PrimaryKeySessionAuthenticator(null); + $identifier = $authenticator->getIdentifier(); + + $this->assertInstanceOf(TokenIdentifier::class, $identifier); + $this->assertSame('id', $identifier->getConfig('tokenField')); + $this->assertSame('key', $identifier->getConfig('dataField')); + } + + /** + * Test custom idField/identifierKey config propagates to default identifier + * + * @return void + */ + public function testGetIdentifierUsesCustomConfig() + { + $authenticator = new PrimaryKeySessionAuthenticator(null, [ + 'idField' => 'uuid', + 'identifierKey' => 'token', + ]); + $identifier = $authenticator->getIdentifier(); + + $this->assertInstanceOf(TokenIdentifier::class, $identifier); + $this->assertSame('uuid', $identifier->getConfig('tokenField')); + $this->assertSame('token', $identifier->getConfig('dataField')); + } + /** * Test authentication * @@ -118,20 +166,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 +202,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 +225,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 +243,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 +286,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 +315,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 +356,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 +390,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 +437,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 +474,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 a2590200..88ef913b 100644 --- a/tests/TestCase/Authenticator/SessionAuthenticatorTest.php +++ b/tests/TestCase/Authenticator/SessionAuthenticatorTest.php @@ -19,15 +19,12 @@ use ArrayObject; use Authentication\Authenticator\Result; use Authentication\Authenticator\SessionAuthenticator; -use Authentication\Identifier\IdentifierCollection; -use Authentication\Identifier\PasswordIdentifier; use Authentication\Test\TestCase\AuthenticationTestCase as TestCase; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Response; use Cake\Http\ServerRequestFactory; use Cake\Http\Session; use Cake\ORM\TableRegistry; -use PHPUnit\Runner\Version; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -43,11 +40,6 @@ class SessionAuthenticatorTest extends TestCase 'core.Users', ]; - /** - * @var \Authentication\Identifier\IdentifierCollection - */ - protected $identifiers; - protected $sessionMock; /** @@ -57,10 +49,6 @@ public function setUp(): void { parent::setUp(); - $this->identifiers = new IdentifierCollection([ - 'Authentication.Password', - ]); - $this->sessionMock = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() ->onlyMethods(['read', 'write', 'delete', 'renew', 'check']) @@ -86,91 +74,7 @@ public function testAuthenticateSuccess() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers); - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::SUCCESS, $result->getStatus()); - } - - /** - * Test authentication - * - * @return void - */ - public function testAuthenticateSuccessWithoutCollection() - { - $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); - - $this->sessionMock->expects($this->once()) - ->method('read') - ->with('Auth') - ->willReturn([ - 'username' => 'mariano', - 'password' => 'password', - ]); - - $request = $request->withAttribute('session', $this->sessionMock); - - $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ - 'identifier' => 'Authentication.Password', - ]); - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::SUCCESS, $result->getStatus()); - } - - /** - * Test authentication - * - * @return void - */ - public function testAuthenticateSuccessWithoutCollectionButObject() - { - $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); - - $this->sessionMock->expects($this->once()) - ->method('read') - ->with('Auth') - ->willReturn([ - 'username' => 'mariano', - 'password' => 'password', - ]); - - $request = $request->withAttribute('session', $this->sessionMock); - - $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ - 'identifier' => new PasswordIdentifier(), - ]); - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::SUCCESS, $result->getStatus()); - } - - /** - * Test authentication - * - * @return void - */ - public function testAuthenticateSuccessWithDirectCollection() - { - $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); - - $this->sessionMock->expects($this->once()) - ->method('read') - ->with('Auth') - ->willReturn([ - 'username' => 'mariano', - 'password' => 'password', - ]); - - $request = $request->withAttribute('session', $this->sessionMock); - - $authenticator = new SessionAuthenticator(new IdentifierCollection(), [ - 'identifier' => new IdentifierCollection(['Authentication.Password']), - ]); + $authenticator = new SessionAuthenticator(); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); @@ -193,85 +97,13 @@ public function testAuthenticateFailure() $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator(); $result = $authenticator->authenticate($request); $this->assertInstanceOf(Result::class, $result); $this->assertSame(Result::FAILURE_IDENTITY_NOT_FOUND, $result->getStatus()); } - /** - * Test successful session data verification by database lookup - * - * @return void - * @deprecated The `identify` option is deprecated. - */ - public function testVerifyByDatabaseSuccess() - { - $this->skipIf( - version_compare(Version::id(), '11.0', '<'), - 'For some reason PHPUnit doesn\'t pick up the deprecation on v10', - ); - - $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); - - $this->sessionMock->expects($this->once()) - ->method('read') - ->with('Auth') - ->willReturn([ - 'username' => 'mariano', - 'password' => 'h45h', - ]); - - $request = $request->withAttribute('session', $this->sessionMock); - - $authenticator = new SessionAuthenticator($this->identifiers, [ - 'identify' => true, - ]); - $this->deprecated(function () use ($authenticator, $request) { - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::SUCCESS, $result->getStatus()); - }); - } - - /** - * Test session data verification by database lookup failure - * - * @return void - * @deprecated The `identify` option is deprecated. - */ - public function testVerifyByDatabaseFailure() - { - $this->skipIf( - version_compare(Version::id(), '11.0', '<'), - 'For some reason PHPUnit doesn\'t pick up the deprecation on v10', - ); - - $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); - - $this->sessionMock->expects($this->once()) - ->method('read') - ->with('Auth') - ->willReturn([ - 'username' => 'does-not', - 'password' => 'exist', - ]); - - $request = $request->withAttribute('session', $this->sessionMock); - - $authenticator = new SessionAuthenticator($this->identifiers, [ - 'identify' => true, - ]); - $this->deprecated(function () use ($authenticator, $request) { - $result = $authenticator->authenticate($request); - - $this->assertInstanceOf(Result::class, $result); - $this->assertSame(Result::FAILURE_CREDENTIALS_INVALID, $result->getStatus()); - }); - } - /** * testPersistIdentity * @@ -282,7 +114,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(); $data = new ArrayObject(['username' => 'florian']); @@ -325,7 +157,7 @@ public function testClearIdentity() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator(); $this->sessionMock->expects($this->once()) ->method('delete') @@ -354,7 +186,7 @@ public function testImpersonate() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator(); $AuthUsers = TableRegistry::getTableLocator()->get('AuthUsers'); $impersonator = $AuthUsers->newEntity([ 'username' => 'mariano', @@ -393,7 +225,7 @@ public function testImpersonateAlreadyImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator(); $impersonator = new ArrayObject([ 'username' => 'mariano', 'password' => 'password', @@ -427,7 +259,7 @@ public function testStopImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator(); $impersonator = new ArrayObject([ 'username' => 'mariano', @@ -474,7 +306,7 @@ public function testStopImpersonatingNotImpersonating() $request = $request->withAttribute('session', $this->sessionMock); $response = new Response(); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator(); $this->sessionMock->expects($this->once()) ->method('check') @@ -511,7 +343,7 @@ public function testIsImpersonating() $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']); $request = $request->withAttribute('session', $this->sessionMock); - $authenticator = new SessionAuthenticator($this->identifiers); + $authenticator = new SessionAuthenticator(); $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 fcf898b1..00000000 --- a/tests/TestCase/Identifier/IdentifierCollectionTest.php +++ /dev/null @@ -1,123 +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/IdentityTest.php b/tests/TestCase/IdentityTest.php index 731c0c51..4f8ba363 100644 --- a/tests/TestCase/IdentityTest.php +++ b/tests/TestCase/IdentityTest.php @@ -19,6 +19,7 @@ use ArrayObject; use Authentication\Identity; use BadMethodCallException; +use Cake\ORM\Entity; use Cake\TestSuite\TestCase; class IdentityTest extends TestCase @@ -43,6 +44,23 @@ public function testGetIdentifier() $this->assertSame('florian', $identity->username); } + public function testGet(): void + { + $data = new Entity([ + 'id' => 1, + 'username' => 'florian', + 'account' => new Entity(['id' => 2, 'role' => 'admin']), + ]); + + $identity = new Identity($data); + + $this->assertSame(1, $identity->get('id')); + $this->assertSame('florian', $identity->get('username')); + $this->assertSame('admin', $identity->get('account.role')); + $this->assertNull($identity->get('missing')); + $this->assertSame($data, $identity->get()); + } + /** * Test mapping fields * diff --git a/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php b/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php index 6d5c21f5..b1d14ffa 100644 --- a/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php +++ b/tests/TestCase/Middleware/AuthenticationMiddlewareTest.php @@ -578,13 +578,13 @@ public function testJwtTokenAuthorizationThroughTheMiddlewareStack() 'firstname' => 'larry', ]; - $token = JWT::encode($data, 'secretKey', 'HS256'); + $token = JWT::encode($data, 'secretKey0123456789secretKey0123456789', 'HS256'); $this->service = new AuthenticationService([ 'authenticators' => [ 'Authentication.Form' => ['identifier' => 'Authentication.Password'], 'Authentication.Jwt' => [ - 'secretKey' => 'secretKey', + 'secretKey' => 'secretKey0123456789secretKey0123456789', 'identifier' => 'Authentication.JwtSubject', ], ], 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/StringUrlCheckerTest.php b/tests/TestCase/UrlChecker/StringUrlCheckerTest.php new file mode 100644 index 00000000..f865bc27 --- /dev/null +++ b/tests/TestCase/UrlChecker/StringUrlCheckerTest.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 StringUrlChecker(); + $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 StringUrlChecker(); + $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 StringUrlChecker(); + $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 StringUrlChecker(); + $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); + } +} diff --git a/tests/TestCase/View/Helper/IdentityHelperTest.php b/tests/TestCase/View/Helper/IdentityHelperTest.php index b731f769..b41f0162 100644 --- a/tests/TestCase/View/Helper/IdentityHelperTest.php +++ b/tests/TestCase/View/Helper/IdentityHelperTest.php @@ -90,5 +90,19 @@ public function testWithOutIdentity() $this->assertNull($helper->getId()); $this->assertFalse($helper->is(1)); + + $this->assertNull($helper->getIdentity()); + } + + public function testGetIdentity() + { + $identity = new Identity([ + 'id' => 1, + ]); + $request = (new ServerRequest())->withAttribute('identity', $identity); + $view = new View($request); + + $helper = new IdentityHelper($view); + $this->assertSame($identity, $helper->getIdentity()); } }