diff --git a/README.md b/README.md index 18ff8ae2..b72a7fe0 100644 --- a/README.md +++ b/README.md @@ -12,25 +12,47 @@ A CakePHP plugin to handle authentication and user authorization the easy way. This branch is for **CakePHP 5.1+**. For details see [version map](https://github.com/dereuromark/cakephp-tinyauth/wiki#cakephp-version-map). -## Features - -### Authentication -What are public actions, which ones need login? - -- Powerful default configs to get you started right away. -- [Quick Setup](https://github.com/dereuromark/cakephp-tinyauth/blob/master/docs/Authentication.md#quick-setups) for 5 minute integration. - -### Authorization -Once you are logged in, what actions can you see with your role(s)? - -- Single-role: 1 user has 1 role (users and roles table for example) -- Multi-role: 1 user can have 1...n roles (users, roles and a "roles_users" pivot table for example) -- [Quick Setup](https://github.com/dereuromark/cakephp-tinyauth/blob/master/docs/Authorization.md#quick-setups) for 5 minute integration. - -### Useful helpers -- AuthUser Component and Helper for stateful and stateless "auth data" access. -- Authentication Component and Helper for `isPublic()` check on current other other actions. -- Auth DebugKit panel for detailed insights into current URL and auth status, identity content and more. +## Why use TinyAuth as a wrapper for Authentication/Authorization plugins? + +TinyAuth now acts as a powerful wrapper around CakePHP's official Authentication and Authorization plugins, providing significant advantages: + +### 🚀 Zero-Code Configuration +- **INI-based setup**: Define all your authentication and authorization rules in simple INI files +- **No controller modifications**: Unlike vanilla plugins that require code in every controller +- **Plugin-friendly**: Automatically works with third-party plugins without modifications + +### ⚡ Lightning Fast Setup +- **5-minute integration**: Get authentication and authorization working in minutes, not hours +- **Sensible defaults**: Pre-configured settings that work for 90% of use cases +- **Quick setups**: Built-in configurations for common scenarios (public non-prefixed, admin areas, etc.) + +### 🛠️ Developer Experience +- **Centralized management**: All auth rules in one place, not scattered across controllers +- **Easy maintenance**: Change access rules without touching code +- **Cache optimized**: Built-in caching for maximum performance +- **DebugKit panel**: Visualize auth status, identity, and permissions in real-time + +### 🔧 Flexibility +- **Adapter pattern**: Use INI files, database, or custom adapters for rule storage +- **Progressive enhancement**: Start simple, add complexity only when needed +- **Stand-alone components**: Use AuthUser component/helper independently if needed + +### 📊 When to Choose TinyAuth + +Choose TinyAuth when you want: +- ✅ Simple role-based access control (RBAC) +- ✅ Quick setup without extensive configuration +- ✅ Controller-action level permissions +- ✅ Easy-to-manage access rules +- ✅ Minimal code changes + +Since this plugin just further extends the official ones, you can skip the plugin's authentication and authorization components, and use the original plugins' functionality if you want: +- ❌ Complex policy-based authorization +- ❌ Resource-level permissions (per-entity authorization) +- ❌ Middleware/routing level authentication +- ❌ Custom authentication flows + +You can still use the other helpers of this plugin, however. ## What's the idea? Default CakePHP authentication and authorization depends on code changes in at least each controller, maybe more classes. diff --git a/docs/Authentication.md b/docs/Authentication.md index 24807d6f..03c91092 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -42,8 +42,6 @@ At the same time you can always set up "deny" rules for any allowed prefix to re ## Enabling -**DEPRECATED** Use `TinyAuth.Authentication` instead. Rest of the page is accurate. - Authentication is set up in your controller's `initialize()` method: ```php @@ -52,7 +50,7 @@ Authentication is set up in your controller's `initialize()` method: public function initialize() { parent::initialize(); - $this->loadComponent('TinyAuth.Auth'); + $this->loadComponent('TinyAuth.Authentication'); } ``` @@ -130,14 +128,14 @@ use Cake\Event\EventInterface; public function beforeFilter(EventInterface $event): void { parent::beforeFilter($event); - $this->Auth->allow(['index', 'view']); + $this->Authentication->allowUnauthenticated(['index', 'view']); } ``` This can be interested when migrating slowly to TinyAuth, for example. Once you move such a code based rule into the INI file, you can safely remove those lines of code in your controller :) ### allow() vs deny() -Since 1.11.0 you can also mix it with `deny()` calls. From how the AuthComponent works, all allow() calls need be done before calling deny(). +Since 1.11.0 you can also mix it with `deny()` calls. From how the Authentication component works, all allow() calls need be done before calling deny(). As such TinyAuth injects its list now before `Controller::beforeFilter()` gets called. Note: It is also advised to move away from these controller calls. @@ -180,7 +178,7 @@ echo $this->Form->control('password', ['autocomplete' => 'off']); ## Configuration -TinyAuth AuthComponent supports the following configuration options. +TinyAuth Authentication component supports the following configuration options. Option | Type | Description :----- | :--- | :---------- diff --git a/docs/README.md b/docs/README.md index bed46063..16e406c6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ You can activate an "Auth" DebugKit panel to have useful insights per URL. See [AuthPanel](AuthPanel.md) docs. ## Authentication -This is done via TinyAuth AuthComponent. +This is done via TinyAuth Authentication component. The component plays well together with the authorization part (see below). If you do not have any roles and either all are logged in or not logged in you can also use this stand-alone to make certain pages public. @@ -27,7 +27,7 @@ See [Authentication](Authentication.md) docs. The TinyAuthorize adapter takes care of authorization. The adapter plays well together with the component above. -But if you prefer to control the action whitelisting for authentication via code and `$this->Auth->allow()` calls, you can +But if you prefer to control the action whitelisting for authentication via code and `$this->Authentication->allowUnauthenticated()` calls, you can also just use this adapter stand-alone for the ACL part of your application. There is also an AuthUserComponent and AuthUserHelper to assist you in making role based decisions or displaying role based links in your templates. @@ -82,14 +82,29 @@ See the docs for details: - [TinyAuth and Authentication plugin](AuthenticationPlugin.md) - [TinyAuth and Authorization plugin](AuthorizationPlugin.md) -### When to use the new plugins? -They are super powerful, but they also require a load of config to get them to run. -If you need authentication/authorization on middleware/routing level however, you need -to use them. +### Why use TinyAuth with the new plugins? + +TinyAuth provides a powerful abstraction layer over the official Authentication and Authorization plugins: -If you only need the basic request policy provided by this plugin, and no further ORM or other policies, -then it is best to stick to the Auth component as simple wrapper. -It is then limited to controller scope (no middleware/routing support) as it always has been so far. +**Benefits of using TinyAuth:** +- **Zero-code configuration**: All auth rules in INI files, no controller modifications needed +- **Instant setup**: Working authentication/authorization in under 5 minutes +- **Plugin compatibility**: Works automatically with all plugins without modifications +- **Centralized management**: All rules in one place, not scattered across controllers +- **Performance**: Built-in caching for optimal speed +- **Developer friendly**: DebugKit panel, clear error messages, easy debugging + +**When to use vanilla plugins' functionality directly:** +They are super powerful, but they also require a load of config to get them to run. +Consider using them (partially) directly when you need: +- Authentication/authorization on middleware/routing level +- Complex policy-based authorization (ORM policies, custom voters) +- Per-entity authorization rules +- Custom authentication flows + +**When to use TinyAuth wrapper:** +If you only need the basic request policy provided by this plugin (controller-action level permissions), +then TinyAuth provides a much simpler and faster solution. You can seamlessly upgrade to the new plugins while keeping your INI files. They are also compatible with AuthUser component and helper as well as the Auth panel. diff --git a/phpstan.neon b/phpstan.neon index 57aa2ea8..647c009e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,5 +7,3 @@ parameters: ignoreErrors: - identifier: missingType.iterableValue - identifier: missingType.generics - - '#Constructor of class .+SessionStorage has an unused parameter \$response#' - - '#PHPDoc tag @mixin contains invalid type .+InstanceConfigTrait.#' diff --git a/src/Auth/AbstractPasswordHasher.php b/src/Auth/AbstractPasswordHasher.php deleted file mode 100644 index 1b9f4cf0..00000000 --- a/src/Auth/AbstractPasswordHasher.php +++ /dev/null @@ -1,79 +0,0 @@ - - */ - protected array $_defaultConfig = []; - - /** - * Constructor - * - * @param array $config Array of config. - */ - public function __construct(array $config = []) { - $this->setConfig($config); - } - - /** - * Generates password hash. - * - * @param string $password Plain text password to hash. - * @return string Either the password hash string or false - */ - abstract public function hash(string $password): string; - - /** - * Check hash. Generate hash from user provided password string or data array - * and check against existing hash. - * - * @param string $password Plain text password to hash. - * @param string $hashedPassword Existing hashed password. - * @return bool True if hashes match else false. - */ - abstract public function check(string $password, string $hashedPassword): bool; - - /** - * Returns true if the password need to be rehashed, due to the password being - * created with anything else than the passwords generated by this class. - * - * Returns true by default since the only implementation users should rely - * on is the one provided by default in php 5.5+ or any compatible library - * - * @param string $password The password to verify - * @return bool - */ - public function needsRehash(string $password): bool { - return password_needs_rehash($password, PASSWORD_DEFAULT); - } - -} diff --git a/src/Auth/AclTrait.php b/src/Auth/AclTrait.php index 1e79e677..705d015a 100644 --- a/src/Auth/AclTrait.php +++ b/src/Auth/AclTrait.php @@ -644,6 +644,7 @@ protected function _mapped(array $roles) { continue; } + /** @var string $alias */ $array[$alias] = $role; } diff --git a/src/Auth/AuthUserTrait.php b/src/Auth/AuthUserTrait.php index 7a6c8fc3..b02fd36b 100644 --- a/src/Auth/AuthUserTrait.php +++ b/src/Auth/AuthUserTrait.php @@ -33,7 +33,7 @@ * define('USER_ROLE_KEY', 'role_id'); * ``` * - * Note: This uses AuthComponent internally to work with both stateful and stateless auth. + * Note: This uses Identity from Authentication plugin. * * @author Mark Scherer * @license MIT diff --git a/src/Auth/BaseAuthenticate.php b/src/Auth/BaseAuthenticate.php deleted file mode 100644 index 9f7b460f..00000000 --- a/src/Auth/BaseAuthenticate.php +++ /dev/null @@ -1,259 +0,0 @@ - ['some_finder_option' => 'some_value']] - * - `passwordHasher` Password hasher class. Can be a string specifying class name - * or an array containing `className` key, any other keys will be passed as - * config to the class. Defaults to 'Default'. - * - * @var array - */ - protected $_defaultConfig = [ - 'fields' => [ - 'username' => 'username', - 'password' => 'password', - ], - 'userModel' => 'Users', - 'finder' => 'all', - 'passwordHasher' => 'TinyAuth.Default', - ]; - - /** - * A Component registry, used to get more components. - * - * @var \Cake\Controller\ComponentRegistry - */ - protected $_registry; - - /** - * Password hasher instance. - * - * @var \TinyAuth\Auth\AbstractPasswordHasher|null - */ - protected $_passwordHasher; - - /** - * Whether the user authenticated by this class - * requires their password to be rehashed with another algorithm. - * - * @var bool - */ - protected $_needsPasswordRehash = false; - - /** - * Constructor - * - * @param \Cake\Controller\ComponentRegistry $registry The Component registry used on this request. - * @param array $config Array of config to use. - */ - public function __construct(ComponentRegistry $registry, array $config = []) { - $this->_registry = $registry; - $this->setConfig($config); - } - - /** - * Find a user record using the username and password provided. - * - * Input passwords will be hashed even when a user doesn't exist. This - * helps mitigate timing attacks that are attempting to find valid usernames. - * - * @param string $username The username/identifier. - * @param string|null $password The password, if not provided password checking is skipped - * and result of find is returned. - * @return array|false Either false on failure, or an array of user data. - */ - protected function _findUser(string $username, ?string $password = null) { - $result = $this->_query($username)->first(); - - if ($result === null) { - // Waste time hashing the password, to prevent - // timing side-channels. However, don't hash - // null passwords as authentication systems - // like digest auth don't use passwords - // and hashing *could* create a timing side-channel. - if ($password !== null) { - $hasher = $this->passwordHasher(); - $hasher->hash($password); - } - - return false; - } - - $passwordField = $this->_config['fields']['password']; - if ($password !== null) { - $hasher = $this->passwordHasher(); - $hashedPassword = $result->get($passwordField); - - if ($hashedPassword === null || $hashedPassword === '') { - // Waste time hashing the password, to prevent - // timing side-channels to distinguish whether - // user has password or not. - $hasher->hash($password); - - return false; - } - - if (!$hasher->check($password, $hashedPassword)) { - return false; - } - - $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword); - $result->unset($passwordField); - } - $hidden = $result->getHidden(); - if ($password === null && in_array($passwordField, $hidden, true)) { - $key = array_search($passwordField, $hidden, true); - unset($hidden[$key]); - $result->setHidden($hidden); - } - - return $result->toArray(); - } - - /** - * Get query object for fetching user from database. - * - * @param string $username The username/identifier. - * @return \Cake\ORM\Query\SelectQuery - */ - protected function _query(string $username): SelectQuery { - $config = $this->_config; - $table = $this->getTableLocator()->get($config['userModel']); - - $options = [ - 'conditions' => [$table->aliasField($config['fields']['username']) => $username], - ]; - - $finder = $config['finder']; - if (is_array($finder)) { - $options += current($finder); - $finder = key($finder); - } - - $options['username'] = $options['username'] ?? $username; - - return $table->find($finder, ...$options); - } - - /** - * Return password hasher object - * - * @throws \RuntimeException If password hasher class not found or - * it does not extend AbstractPasswordHasher - * @return \TinyAuth\Auth\AbstractPasswordHasher Password hasher instance - */ - public function passwordHasher(): AbstractPasswordHasher { - if ($this->_passwordHasher !== null) { - return $this->_passwordHasher; - } - - $passwordHasher = $this->_config['passwordHasher']; - - return $this->_passwordHasher = PasswordHasherFactory::build($passwordHasher); - } - - /** - * Returns whether the password stored in the repository for the logged in user - * requires to be rehashed with another algorithm - * - * @return bool - */ - public function needsPasswordRehash(): bool { - return $this->_needsPasswordRehash; - } - - /** - * Authenticate a user based on the request information. - * - * @param \Cake\Http\ServerRequest $request Request to get authentication information from. - * @param \Cake\Http\Response $response A response object that can have headers added. - * @return array|false Either false on failure, or an array of user data on success. - */ - abstract public function authenticate(ServerRequest $request, Response $response); - - /** - * Get a user based on information in the request. Primarily used by stateless authentication - * systems like basic and digest auth. - * - * @param \Cake\Http\ServerRequest $request Request object. - * @return array|false Either false or an array of user information - */ - public function getUser(ServerRequest $request) { - return false; - } - - /** - * Handle unauthenticated access attempt. In implementation valid return values - * can be: - * - * - Null - No action taken, AuthComponent should return appropriate response. - * - \Cake\Http\Response - A response object, which will cause AuthComponent to - * simply return that response. - * - * @param \Cake\Http\ServerRequest $request A request object. - * @param \Cake\Http\Response $response A response object. - * @return \Cake\Http\Response|null|void - */ - public function unauthenticated(ServerRequest $request, Response $response) { - } - - /** - * Returns a list of all events that this authenticate class will listen to. - * - * An authenticate class can listen to following events fired by AuthComponent: - * - * - `Auth.afterIdentify` - Fired after a user has been identified using one of - * configured authenticate class. The callback function should have signature - * like `afterIdentify(EventInterface $event, array $user)` when `$user` is the - * identified user record. - * - * - `Auth.logout` - Fired when AuthComponent::logout() is called. The callback - * function should have signature like `logout(EventInterface $event, array $user)` - * where `$user` is the user about to be logged out. - * - * @return array List of events this class listens to. Defaults to `[]`. - */ - public function implementedEvents(): array { - return []; - } - -} diff --git a/src/Auth/BaseAuthorize.php b/src/Auth/BaseAuthorize.php index 825e26dd..6190503e 100644 --- a/src/Auth/BaseAuthorize.php +++ b/src/Auth/BaseAuthorize.php @@ -22,9 +22,7 @@ use Cake\Http\ServerRequest; /** - * Abstract base authorization adapter for AuthComponent. - * - * @see \Cake\Controller\Component\AuthComponent::$authenticate + * Abstract base authorization adapter for TinyAuth. */ abstract class BaseAuthorize { diff --git a/src/Auth/BasicAuthenticate.php b/src/Auth/BasicAuthenticate.php deleted file mode 100644 index 81f704fd..00000000 --- a/src/Auth/BasicAuthenticate.php +++ /dev/null @@ -1,115 +0,0 @@ -loadComponent('Auth', [ - * 'authenticate' => ['Basic'] - * 'storage' => 'Memory', - * 'unauthorizedRedirect' => false, - * ]); - * ``` - * - * You should set `storage` to `Memory` to prevent CakePHP from sending a - * session cookie to the client. - * - * You should set `unauthorizedRedirect` to `false`. This causes `AuthComponent` to - * throw a `ForbiddenException` exception instead of redirecting to another page. - * - * Since HTTP Basic Authentication is stateless you don't need call `setUser()` - * in your controller. The user credentials will be checked on each request. If - * valid credentials are not provided, required authentication headers will be sent - * by this authentication provider which triggers the login dialog in the browser/client. - * - * @see https://book.cakephp.org/4/en/controllers/components/authentication.html - */ -class BasicAuthenticate extends BaseAuthenticate { - - /** - * Authenticate a user using HTTP auth. Will use the configured User model and attempt a - * login using HTTP auth. - * - * @param \Cake\Http\ServerRequest $request The request to authenticate with. - * @param \Cake\Http\Response $response The response to add headers to. - * @return array|false Either false on failure, or an array of user data on success. - */ - public function authenticate(ServerRequest $request, Response $response) { - return $this->getUser($request); - } - - /** - * Get a user based on information in the request. Used by cookie-less auth for stateless clients. - * - * @param \Cake\Http\ServerRequest $request Request object. - * @return array|false Either false or an array of user information - */ - public function getUser(ServerRequest $request) { - $username = $request->getEnv('PHP_AUTH_USER'); - $pass = $request->getEnv('PHP_AUTH_PW'); - - if (!is_string($username) || $username === '' || !is_string($pass) || $pass === '') { - return false; - } - - return $this->_findUser($username, $pass); - } - - /** - * Handles an unauthenticated access attempt by sending appropriate login headers - * - * @param \Cake\Http\ServerRequest $request A request object. - * @param \Cake\Http\Response $response A response object. - * @throws \Cake\Http\Exception\UnauthorizedException - * @return \Cake\Http\Response|null|void - */ - public function unauthenticated(ServerRequest $request, Response $response) { - $unauthorizedException = new UnauthorizedException(); - $unauthorizedException->setHeaders($this->loginHeaders($request)); - - throw $unauthorizedException; - } - - /** - * Generate the login headers - * - * @param \Cake\Http\ServerRequest $request Request object. - * @return array Headers for logging in. - */ - public function loginHeaders(ServerRequest $request): array { - $realm = $this->getConfig('realm') ?: $request->getEnv('SERVER_NAME'); - - return [ - 'WWW-Authenticate' => sprintf('Basic realm="%s"', $realm), - ]; - } - -} diff --git a/src/Auth/ControllerAuthorize.php b/src/Auth/ControllerAuthorize.php deleted file mode 100644 index 1362bdc0..00000000 --- a/src/Auth/ControllerAuthorize.php +++ /dev/null @@ -1,96 +0,0 @@ -request->getParam('admin')) { - * return $user['role'] === 'admin'; - * } - * return !empty($user); - * } - * ``` - * - * The above is simple implementation that would only authorize users of the - * 'admin' role to access admin routing. - * - * @see \Cake\Controller\Component\AuthComponent::$authenticate - */ -class ControllerAuthorize extends BaseAuthorize { - - /** - * Controller for the request. - * - * @var \Cake\Controller\Controller - */ - protected $_Controller; - - /** - * @inheritDoc - */ - public function __construct(ComponentRegistry $registry, array $config = []) { - parent::__construct($registry, $config); - $this->controller($registry->getController()); - } - - /** - * Get/set the controller this authorize object will be working with. Also - * checks that isAuthorized is implemented. - * - * @param \Cake\Controller\Controller|null $controller null to get, a controller to set. - * @return \Cake\Controller\Controller - */ - public function controller(?Controller $controller = null): Controller { - if ($controller) { - $this->_Controller = $controller; - } - - return $this->_Controller; - } - - /** - * Checks user authorization using a controller callback. - * - * @param \ArrayAccess|array $user Active user data - * @param \Cake\Http\ServerRequest $request Request instance. - * @throws \Cake\Core\Exception\CakeException If controller does not have method `isAuthorized()`. - * @return bool - */ - public function authorize($user, ServerRequest $request): bool { - if (!method_exists($this->_Controller, 'isAuthorized')) { - throw new CakeException(sprintf( - '%s does not implement an isAuthorized() method.', - get_class($this->_Controller), - )); - } - - return (bool)$this->_Controller->isAuthorized($user); - } - -} diff --git a/src/Auth/DefaultPasswordHasher.php b/src/Auth/DefaultPasswordHasher.php deleted file mode 100644 index 3dc5420a..00000000 --- a/src/Auth/DefaultPasswordHasher.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ - protected array $_defaultConfig = [ - 'hashType' => PASSWORD_DEFAULT, - 'hashOptions' => [], - ]; - - /** - * Generates password hash. - * - * @psalm-suppress InvalidNullableReturnType - * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#hashing-passwords - * @param string $password Plain text password to hash. - * @return string Password hash or false on failure - */ - public function hash(string $password): string { - return (string)password_hash( - $password, - $this->_config['hashType'], - $this->_config['hashOptions'], - ); - } - - /** - * Check hash. Generate hash for user provided password and check against existing hash. - * - * @param string $password Plain text password to hash. - * @param string $hashedPassword Existing hashed password. - * @return bool True if hashes match else false. - */ - public function check(string $password, string $hashedPassword): bool { - return password_verify($password, $hashedPassword); - } - - /** - * Returns true if the password need to be rehashed, due to the password being - * created with anything else than the passwords generated by this class. - * - * @param string $password The password to verify - * @return bool - */ - public function needsRehash(string $password): bool { - return password_needs_rehash($password, $this->_config['hashType'], $this->_config['hashOptions']); - } - -} diff --git a/src/Auth/DigestAuthenticate.php b/src/Auth/DigestAuthenticate.php deleted file mode 100644 index e38bc067..00000000 --- a/src/Auth/DigestAuthenticate.php +++ /dev/null @@ -1,289 +0,0 @@ -loadComponent('Auth', [ - * 'authenticate' => ['Digest'], - * 'storage' => 'Memory', - * 'unauthorizedRedirect' => false, - * ]); - * ``` - * - * You should set `storage` to `Memory` to prevent CakePHP from sending a - * session cookie to the client. - * - * You should set `unauthorizedRedirect` to `false`. This causes `AuthComponent` to - * throw a `ForbiddenException` exception instead of redirecting to another page. - * - * Since HTTP Digest Authentication is stateless you don't need call `setUser()` - * in your controller. The user credentials will be checked on each request. If - * valid credentials are not provided, required authentication headers will be sent - * by this authentication provider which triggers the login dialog in the browser/client. - * - * ### Generating passwords compatible with Digest authentication. - * - * DigestAuthenticate requires a special password hash that conforms to RFC2617. - * You can generate this password using `DigestAuthenticate::password()` - * - * ``` - * $digestPass = DigestAuthenticate::password($username, $password, env('SERVER_NAME')); - * ``` - * - * If you wish to use digest authentication alongside other authentication methods, - * it's recommended that you store the digest authentication separately. For - * example `User.digest_pass` could be used for a digest password, while - * `User.password` would store the password hash for use with other methods like - * Basic or Form. - * - * @see https://book.cakephp.org/4/en/controllers/components/authentication.html - */ -class DigestAuthenticate extends BasicAuthenticate { - - /** - * Constructor - * - * Besides the keys specified in BaseAuthenticate::$_defaultConfig, - * DigestAuthenticate uses the following extra keys: - * - * - `secret` The secret to use for nonce validation. Defaults to Security::getSalt(). - * - `realm` The realm authentication is for, Defaults to the servername. - * - `qop` Defaults to 'auth', no other values are supported at this time. - * - `opaque` A string that must be returned unchanged by clients. - * Defaults to `md5($config['realm'])` - * - `nonceLifetime` The number of seconds that nonces are valid for. Defaults to 300. - * - * @param \Cake\Controller\ComponentRegistry $registry The Component registry - * used on this request. - * @param array $config Array of config to use. - */ - public function __construct(ComponentRegistry $registry, array $config = []) { - $this->setConfig([ - 'nonceLifetime' => 300, - 'secret' => Security::getSalt(), - 'realm' => null, - 'qop' => 'auth', - 'opaque' => null, - ]); - - parent::__construct($registry, $config); - } - - /** - * Get a user based on information in the request. Used by cookie-less auth for stateless clients. - * - * @param \Cake\Http\ServerRequest $request Request object. - * @return array|false Either false or an array of user information - */ - public function getUser(ServerRequest $request) { - $digest = $this->_getDigest($request); - if (empty($digest)) { - return false; - } - - $user = $this->_findUser($digest['username']); - if (empty($user)) { - return false; - } - - if (!$this->validNonce($digest['nonce'])) { - return false; - } - - $field = $this->_config['fields']['password']; - $password = $user[$field]; - unset($user[$field]); - - $requestMethod = $request->getEnv('ORIGINAL_REQUEST_METHOD') ?: $request->getMethod(); - $hash = $this->generateResponseHash( - $digest, - $password, - (string)$requestMethod, - ); - if (hash_equals($hash, $digest['response'])) { - return $user; - } - - return false; - } - - /** - * Gets the digest headers from the request/environment. - * - * @param \Cake\Http\ServerRequest $request Request object. - * @return array|null Array of digest information. - */ - protected function _getDigest(ServerRequest $request): ?array { - $digest = $request->getEnv('PHP_AUTH_DIGEST'); - if (empty($digest) && function_exists('apache_request_headers')) { - $headers = apache_request_headers(); - if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') { - $digest = substr($headers['Authorization'], 7); - } - } - if (empty($digest)) { - return null; - } - - return $this->parseAuthData($digest); - } - - /** - * Parse the digest authentication headers and split them up. - * - * @param string $digest The raw digest authentication headers. - * @return array|null An array of digest authentication headers - */ - public function parseAuthData(string $digest): ?array { - if (substr($digest, 0, 7) === 'Digest ') { - $digest = substr($digest, 7); - } - $keys = $match = []; - $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; - preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER); - - foreach ($match as $i) { - $keys[$i[1]] = $i[3]; - unset($req[$i[1]]); - } - - if (empty($req)) { - return $keys; - } - - return null; - } - - /** - * Generate the response hash for a given digest array. - * - * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData(). - * @param string $password The digest hash password generated with DigestAuthenticate::password() - * @param string $method Request method - * @return string Response hash - */ - public function generateResponseHash(array $digest, string $password, string $method): string { - return md5( - $password . - ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . - md5($method . ':' . $digest['uri']), - ); - } - - /** - * Creates an auth digest password hash to store - * - * @param string $username The username to use in the digest hash. - * @param string $password The unhashed password to make a digest hash for. - * @param string $realm The realm the password is for. - * @return string the hashed password that can later be used with Digest authentication. - */ - public static function password(string $username, string $password, string $realm): string { - return md5($username . ':' . $realm . ':' . $password); - } - - /** - * Generate the login headers - * - * @param \Cake\Http\ServerRequest $request Request object. - * @return array Headers for logging in. - */ - public function loginHeaders(ServerRequest $request): array { - $realm = $this->_config['realm'] ?: $request->getEnv('SERVER_NAME'); - - $options = [ - 'realm' => $realm, - 'qop' => $this->_config['qop'], - 'nonce' => $this->generateNonce(), - 'opaque' => $this->_config['opaque'] ?: md5($realm), - ]; - - $digest = $this->_getDigest($request); - if ($digest && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) { - $options['stale'] = true; - } - - $opts = []; - foreach ($options as $k => $v) { - if (is_bool($v)) { - $v = $v ? 'true' : 'false'; - $opts[] = sprintf('%s=%s', $k, $v); - } else { - $opts[] = sprintf('%s="%s"', $k, $v); - } - } - - return [ - 'WWW-Authenticate' => 'Digest ' . implode(',', $opts), - ]; - } - - /** - * Generate a nonce value that is validated in future requests. - * - * @return string - */ - protected function generateNonce(): string { - $expiryTime = microtime(true) + $this->getConfig('nonceLifetime'); - $secret = $this->getConfig('secret'); - $signatureValue = hash_hmac('sha256', $expiryTime . ':' . $secret, $secret); - $nonceValue = $expiryTime . ':' . $signatureValue; - - return base64_encode($nonceValue); - } - - /** - * Check the nonce to ensure it is valid and not expired. - * - * @param string $nonce The nonce value to check. - * @return bool - */ - protected function validNonce(string $nonce): bool { - /** @var string|false $value */ - $value = base64_decode($nonce); - if ($value === false) { - return false; - } - $parts = explode(':', $value); - if (count($parts) !== 2) { - return false; - } - [$expires, $checksum] = $parts; - if ($expires < microtime(true)) { - return false; - } - $secret = $this->getConfig('secret'); - $check = hash_hmac('sha256', $expires . ':' . $secret, $secret); - - return hash_equals($check, $checksum); - } - -} diff --git a/src/Auth/FallbackPasswordHasher.php b/src/Auth/FallbackPasswordHasher.php deleted file mode 100644 index 4a2db8ae..00000000 --- a/src/Auth/FallbackPasswordHasher.php +++ /dev/null @@ -1,103 +0,0 @@ - - */ - protected array $_defaultConfig = [ - 'hashers' => [], - ]; - - /** - * Holds the list of password hasher objects that will be used - * - * @var array<\TinyAuth\Auth\AbstractPasswordHasher> - */ - protected $_hashers = []; - - /** - * Constructor - * - * @param array $config configuration options for this object. Requires the - * `hashers` key to be present in the array with a list of other hashers to be - * used. - */ - public function __construct(array $config = []) { - parent::__construct($config); - foreach ($this->_config['hashers'] as $key => $hasher) { - if (is_array($hasher) && !isset($hasher['className'])) { - $hasher['className'] = $key; - } - $this->_hashers[] = PasswordHasherFactory::build($hasher); - } - } - - /** - * Generates password hash. - * - * Uses the first password hasher in the list to generate the hash - * - * @param string $password Plain text password to hash. - * @return string Password hash or false - */ - public function hash(string $password): string { - return (string)$this->_hashers[0]->hash($password); - } - - /** - * Verifies that the provided password corresponds to its hashed version - * - * This will iterate over all configured hashers until one of them returns - * true. - * - * @param string $password Plain text password to hash. - * @param string $hashedPassword Existing hashed password. - * @return bool True if hashes match else false. - */ - public function check(string $password, string $hashedPassword): bool { - foreach ($this->_hashers as $hasher) { - if ($hasher->check($password, $hashedPassword)) { - return true; - } - } - - return false; - } - - /** - * Returns true if the password need to be rehashed, with the first hasher present - * in the list of hashers - * - * @param string $password The password to verify - * @return bool - */ - public function needsRehash(string $password): bool { - return $this->_hashers[0]->needsRehash($password); - } - -} diff --git a/src/Auth/FormAuthenticate.php b/src/Auth/FormAuthenticate.php deleted file mode 100644 index 45ea7350..00000000 --- a/src/Auth/FormAuthenticate.php +++ /dev/null @@ -1,90 +0,0 @@ -loadComponent('Auth', [ - * 'authenticate' => [ - * 'Form' => [ - * 'fields' => ['username' => 'email', 'password' => 'passwd'], - * 'finder' => 'auth', - * ] - * ] - * ]); - * ``` - * - * When configuring FormAuthenticate you can pass in config to which fields, model and finder - * are used. See `BaseAuthenticate::$_defaultConfig` for more information. - * - * @see https://book.cakephp.org/4/en/controllers/components/authentication.html - */ -class FormAuthenticate extends BaseAuthenticate { - - /** - * Checks the fields to ensure they are supplied. - * - * @param \Cake\Http\ServerRequest $request The request that contains login information. - * @param array $fields The fields to be checked. - * @return bool False if the fields have not been supplied. True if they exist. - */ - protected function _checkFields(ServerRequest $request, array $fields): bool { - foreach ([$fields['username'], $fields['password']] as $field) { - $value = $request->getData($field); - if (empty($value) || !is_string($value)) { - return false; - } - } - - return true; - } - - /** - * Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields` - * to find POST data that is used to find a matching record in the `config.userModel`. Will return false if - * there is no post data, either username or password is missing, or if the scope conditions have not been met. - * - * @param \Cake\Http\ServerRequest $request The request that contains login information. - * @param \Cake\Http\Response $response Unused response object. - * @return array|false False on login failure. An array of User data on success. - */ - public function authenticate(ServerRequest $request, Response $response) { - $fields = $this->_config['fields']; - if (!$this->_checkFields($request, $fields)) { - return false; - } - - return $this->_findUser( - $request->getData($fields['username']), - $request->getData($fields['password']), - ); - } - -} diff --git a/src/Auth/MultiColumnAuthenticate.php b/src/Auth/MultiColumnAuthenticate.php deleted file mode 100644 index 0e6e9caf..00000000 --- a/src/Auth/MultiColumnAuthenticate.php +++ /dev/null @@ -1,89 +0,0 @@ -Auth->setConfig('authenticate', [ - * 'TinyAuth.MultiColumn' => [ - * 'fields' => [ - * 'username' => 'login', - * 'password' => 'password' - * ], - * 'columns' => ['username', 'email'], - * ] - * ]); - * ``` - * - * Licensed under The MIT License - * Copied from discontinued FriendsOfCake/Authenticate - */ -class MultiColumnAuthenticate extends FormAuthenticate { - - /** - * Besides the keys specified in BaseAuthenticate::$_defaultConfig, - * MultiColumnAuthenticate uses the following extra keys: - * - * - 'columns' Array of columns to check username form input against - * - * @param \Cake\Controller\ComponentRegistry $registry The Component registry - * used on this request. - * @param array $config Array of config to use. - */ - public function __construct(ComponentRegistry $registry, array $config) { - $this->setConfig([ - 'columns' => [], - ]); - - parent::__construct($registry, $config); - } - - /** - * Get query object for fetching user from database. - * - * @param string $username The username/identifier. - * @return \Cake\ORM\Query\SelectQuery - */ - protected function _query(string $username): SelectQuery { - $table = TableRegistry::getTableLocator()->get($this->_config['userModel']); - - $columns = []; - foreach ($this->_config['columns'] as $column) { - $columns[] = [$table->aliasField($column) => $username]; - } - $conditions = ['OR' => $columns]; - - $options = [ - 'conditions' => $conditions, - ]; - - if (!empty($this->_config['scope'])) { - $options['conditions'] = array_merge($options['conditions'], $this->_config['scope']); - } - if (!empty($this->_config['contain'])) { - $options['contain'] = $this->_config['contain']; - } - - $finder = $this->_config['finder']; - if (is_array($finder)) { - $options += current($finder); - $finder = key($finder); - } - - if (!isset($options['username'])) { - $options['username'] = $username; - } - - return $table->find($finder, ...$options); - } - -} diff --git a/src/Auth/PasswordHasherFactory.php b/src/Auth/PasswordHasherFactory.php deleted file mode 100644 index 50ae2d47..00000000 --- a/src/Auth/PasswordHasherFactory.php +++ /dev/null @@ -1,60 +0,0 @@ -|string $passwordHasher Name of the password hasher or an array with - * at least the key `className` set to the name of the class to use - * @throws \RuntimeException If password hasher class not found or - * it does not extend {@link \TinyAuth\Auth\AbstractPasswordHasher} - * @return \TinyAuth\Auth\AbstractPasswordHasher Password hasher instance - */ - public static function build($passwordHasher): AbstractPasswordHasher { - $config = []; - if (is_string($passwordHasher)) { - $class = $passwordHasher; - } else { - $class = $passwordHasher['className']; - $config = $passwordHasher; - unset($config['className']); - } - - $className = App::className($class, 'Auth', 'PasswordHasher'); - if ($className === null) { - throw new RuntimeException(sprintf('Password hasher class "%s" was not found.', $class)); - } - - $hasher = new $className($config); - if (!($hasher instanceof AbstractPasswordHasher)) { - throw new RuntimeException('Password hasher must extend AbstractPasswordHasher class.'); - } - - return $hasher; - } - -} diff --git a/src/Auth/Storage/MemoryStorage.php b/src/Auth/Storage/MemoryStorage.php deleted file mode 100644 index 14440f4a..00000000 --- a/src/Auth/Storage/MemoryStorage.php +++ /dev/null @@ -1,79 +0,0 @@ -_user; - } - - /** - * @inheritDoc - */ - public function write($user): void { - $this->_user = $user; - } - - /** - * @inheritDoc - */ - public function delete(): void { - $this->_user = null; - } - - /** - * @inheritDoc - */ - public function redirectUrl($url = null) { - if ($url === null) { - return $this->_redirectUrl; - } - - if ($url === false) { - $this->_redirectUrl = null; - - return null; - } - - $this->_redirectUrl = $url; - - return null; - } - -} diff --git a/src/Auth/Storage/SessionStorage.php b/src/Auth/Storage/SessionStorage.php deleted file mode 100644 index b92ec889..00000000 --- a/src/Auth/Storage/SessionStorage.php +++ /dev/null @@ -1,141 +0,0 @@ - - */ - protected $_defaultConfig = [ - 'key' => 'Auth.User', - 'redirect' => 'Auth.redirect', - ]; - - /** - * Constructor. - * - * @param \Cake\Http\ServerRequest $request Request instance. - * @param \Cake\Http\Response $response Response instance. - * @param array $config Configuration list. - */ - public function __construct(ServerRequest $request, Response $response, array $config = []) { - $this->_session = $request->getSession(); - $this->setConfig($config); - } - - /** - * Read user record from session. - * - * @psalm-suppress InvalidReturnType - * @return \ArrayAccess|array|null User record if available else null. - */ - public function read() { - if ($this->_user !== null) { - return $this->_user ?: null; - } - - /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ - $this->_user = $this->_session->read($this->_config['key']) ?: false; - - /** @psalm-suppress InvalidReturnStatement */ - return $this->_user ?: null; - } - - /** - * Write user record to session. - * - * The session id is also renewed to help mitigate issues with session replays. - * - * @param \ArrayAccess|array $user User record. - * @return void - */ - public function write($user): void { - $this->_user = $user; - - $this->_session->renew(); - $this->_session->write($this->_config['key'], $user); - } - - /** - * Delete user record from session. - * - * The session id is also renewed to help mitigate issues with session replays. - * - * @return void - */ - public function delete(): void { - $this->_user = false; - - $this->_session->delete($this->_config['key']); - $this->_session->renew(); - } - - /** - * @inheritDoc - */ - public function redirectUrl($url = null) { - if ($url === null) { - return $this->_session->read($this->_config['redirect']); - } - - if ($url === false) { - $this->_session->delete($this->_config['redirect']); - - return null; - } - - $this->_session->write($this->_config['redirect'], $url); - - return null; - } - -} diff --git a/src/Auth/Storage/StorageInterface.php b/src/Auth/Storage/StorageInterface.php deleted file mode 100644 index 8fdd41ad..00000000 --- a/src/Auth/Storage/StorageInterface.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ - protected array $_defaultConfig = [ - 'hashType' => null, - ]; - - /** - * @inheritDoc - */ - public function __construct(array $config = []) { - if (Configure::read('debug')) { - Debugger::checkSecurityKeys(); - } - - parent::__construct($config); - } - - /** - * @inheritDoc - */ - public function hash(string $password): string { - return Security::hash($password, $this->_config['hashType'], true); - } - - /** - * Check hash. Generate hash for user provided password and check against existing hash. - * - * @param string $password Plain text password to hash. - * @param string $hashedPassword Existing hashed password. - * @return bool True if hashes match else false. - */ - public function check(string $password, string $hashedPassword): bool { - return $hashedPassword === $this->hash($password); - } - -} diff --git a/src/Command/TinyAuthAddCommand.php b/src/Command/TinyAuthAddCommand.php index ba224b0f..83fd3a53 100644 --- a/src/Command/TinyAuthAddCommand.php +++ b/src/Command/TinyAuthAddCommand.php @@ -6,16 +6,42 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; +use Cake\Core\Configure; use TinyAuth\Sync\Adder; use TinyAuth\Utility\TinyAuth; /** - * Auth and ACL helper + * Command to add specific controller/action entries to ACL configuration. + * + * This command modifies the ACL INI file (default: config/auth_acl.ini) by adding + * or updating specific controller/action permissions for given roles. + * + * Usage examples: + * - `bin/cake tiny_auth_add Articles index user,admin` - Allow users and admins to access Articles::index + * - `bin/cake tiny_auth_add Articles` - Interactive mode, prompts for action and roles + * - `bin/cake tiny_auth_add Articles "*" "*"` - Allow all roles to access all Articles actions + * + * @see config/auth_acl.ini - The file that gets modified by this command */ class TinyAuthAddCommand extends Command { /** - * Main function Prints out the list of shells. + * @inheritDoc + */ + public static function getDescription(): string { + return 'Add or update specific controller/action permissions in the ACL configuration.'; + } + + /** + * Execute the command - adds a specific controller/action/roles entry to the ACL file. + * + * Files modified: + * - config/auth_acl.ini (or custom path via TinyAuth.aclFilePath config) + * + * The command will: + * 1. Read the existing ACL configuration + * 2. Add or update the specified controller/action with the given roles + * 3. Write the updated configuration back to the INI file * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io @@ -40,7 +66,10 @@ public function execute(Arguments $args, ConsoleIo $io) { $roles = $args->getArgument('roles') ?: '*'; $roles = array_map('trim', explode(',', $roles)); $adder->addAcl($controller, $action, $roles, $args, $io); - $io->out('Controllers and ACL synced.'); + + $path = Configure::read('TinyAuth.aclFilePath', ROOT . DS . 'config' . DS); + $file = Configure::read('TinyAuth.aclFile', 'auth_acl.ini'); + $io->success('ACL entry added/updated in: ' . $path . $file); return static::CODE_SUCCESS; } @@ -62,7 +91,19 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption $roles = $this->_getAvailableRoles(); $parser->setDescription( - 'Get the list of controllers and make sure, they are synced into the ACL file.', + static::getDescription() . PHP_EOL . + PHP_EOL . + 'This command modifies: config/auth_acl.ini (or custom path via TinyAuth.aclFilePath)' . PHP_EOL . + PHP_EOL . + 'Examples:' . PHP_EOL . + ' bin/cake tiny_auth_add Articles index user,admin' . PHP_EOL . + ' → Adds: [Articles] index = user, admin' . PHP_EOL . + PHP_EOL . + ' bin/cake tiny_auth_add Articles "*" admin' . PHP_EOL . + ' → Adds: [Articles] * = admin' . PHP_EOL . + PHP_EOL . + ' bin/cake tiny_auth_add MyPlugin.Admin/Articles edit admin' . PHP_EOL . + ' → Adds: [MyPlugin.Admin/Articles] edit = admin', )->addArgument('controller', [ 'help' => 'Controller name (Plugin.Prefix/Name) without Controller suffix.', 'required' => false, diff --git a/src/Command/TinyAuthSyncCommand.php b/src/Command/TinyAuthSyncCommand.php index e7e80006..354bd7ec 100644 --- a/src/Command/TinyAuthSyncCommand.php +++ b/src/Command/TinyAuthSyncCommand.php @@ -6,16 +6,45 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; +use Cake\Core\Configure; use TinyAuth\Sync\Syncer; use TinyAuth\Utility\TinyAuth; /** - * Auth and ACL helper + * Command to synchronize all controllers with ACL configuration. + * + * This command scans your application for all controllers and ensures they + * have entries in the ACL INI file (default: config/auth_acl.ini). + * + * Usage examples: + * - `bin/cake tiny_auth_sync user,admin` - Add all controllers with access for user and admin roles + * - `bin/cake tiny_auth_sync "*" -p all` - Add all controllers (including plugins) with access for all roles + * - `bin/cake tiny_auth_sync user -d` - Dry run, shows what would be added without modifying files + * + * @see config/auth_acl.ini - The file that gets modified by this command */ class TinyAuthSyncCommand extends Command { /** - * Main function Prints out the list of shells. + * @inheritDoc + */ + public static function getDescription(): string { + return 'Scan all controllers and add missing ones to the ACL configuration.'; + } + + /** + * Execute the command - syncs all discovered controllers to the ACL file. + * + * Files modified: + * - config/auth_acl.ini (or custom path via TinyAuth.aclFilePath config) + * + * The command will: + * 1. Scan for all controllers in the application (and plugins if specified) + * 2. Check which controllers don't have ACL entries yet + * 3. Add missing controllers with wildcard action (*) and specified roles + * 4. Write the updated configuration back to the INI file + * + * Note: Existing entries are never modified, only new controllers are added. * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io @@ -24,7 +53,10 @@ class TinyAuthSyncCommand extends Command { public function execute(Arguments $args, ConsoleIo $io) { $syncer = $this->_getSyncer(); $syncer->syncAcl($args, $io); - $io->out('Controllers and ACL synced.'); + + $path = Configure::read('TinyAuth.aclFilePath', ROOT . DS . 'config' . DS); + $file = Configure::read('TinyAuth.aclFile', 'auth_acl.ini'); + $io->success('Controllers synced to: ' . $path . $file); return static::CODE_SUCCESS; } @@ -46,7 +78,24 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption $roles = $this->_getAvailableRoles(); $parser->setDescription( - 'Get the list of controllers and make sure, they are synced into the ACL file.', + static::getDescription() . PHP_EOL . + PHP_EOL . + 'This command modifies: config/auth_acl.ini (or custom path via TinyAuth.aclFilePath)' . PHP_EOL . + PHP_EOL . + 'The command will:' . PHP_EOL . + ' 1. Scan src/Controller/ for all controllers' . PHP_EOL . + ' 2. Add any missing controllers with wildcard (*) access for specified roles' . PHP_EOL . + ' 3. Preserve existing entries (never overwrites)' . PHP_EOL . + PHP_EOL . + 'Examples:' . PHP_EOL . + ' bin/cake tiny_auth_sync user,admin' . PHP_EOL . + ' → Adds all missing controllers with: * = user, admin' . PHP_EOL . + PHP_EOL . + ' bin/cake tiny_auth_sync "*" -p all' . PHP_EOL . + ' → Adds all missing controllers (including plugins) with: * = *' . PHP_EOL . + PHP_EOL . + ' bin/cake tiny_auth_sync admin -d' . PHP_EOL . + ' → Dry run - shows what would be added without modifying files', )->addArgument('roles', [ 'help' => 'Role names, comma separated, e.g. `user,admin`.' . ($roles ? PHP_EOL . 'Available roles: ' . implode(', ', $roles) . '.' : ''), 'required' => true, diff --git a/src/Controller/Component/AuthComponent.php b/src/Controller/Component/AuthComponent.php deleted file mode 100644 index e9c3afb9..00000000 --- a/src/Controller/Component/AuthComponent.php +++ /dev/null @@ -1,75 +0,0 @@ - $config - * @throws \RuntimeException - */ - public function __construct(ComponentRegistry $registry, array $config = []) { - $config += Config::all(); - if ($config && empty($config['className'])) { - $config['className'] = 'TinyAuth.Auth'; - } - - parent::__construct($registry, $config); - if ($registry->has('Authentication') && get_class($registry->get('Authentication')) === AuthenticationComponent::class) { - throw new RuntimeException('You cannot use new TinyAuth.Authentication component and this TinyAuth.Auth component together.'); - } - if ($registry->has('Authorization') && get_class($registry->get('Authorization')) === AuthorizationComponent::class) { - throw new RuntimeException('You cannot use new TinyAuth.Authorization component and this TinyAuth.Auth component together.'); - } - } - - /** - * @param array $config The config data. - * @return void - */ - public function initialize(array $config): void { - parent::initialize($config); - - $params = $this->_registry->getController()->getRequest()->getAttribute('params'); - $this->_prepareAuthentication($params); - } - - /** - * @param array $params - * @return void - */ - protected function _prepareAuthentication(array $params) { - $rule = $this->_getAllowRule($params); - if (!$rule) { - return; - } - - if (in_array('*', $rule['allow'], true)) { - $this->allow(); - } elseif (!empty($rule['allow'])) { - $this->allow($rule['allow']); - } - if (in_array('*', $rule['deny'], true)) { - $this->deny(); - } elseif (!empty($rule['deny'])) { - $this->deny($rule['deny']); - } - } - -} diff --git a/src/Controller/Component/AuthUserComponent.php b/src/Controller/Component/AuthUserComponent.php index dbd04b66..928a1c39 100644 --- a/src/Controller/Component/AuthUserComponent.php +++ b/src/Controller/Component/AuthUserComponent.php @@ -3,7 +3,6 @@ namespace TinyAuth\Controller\Component; use ArrayAccess; -use Authentication\Identity; use Cake\Controller\Component; use Cake\Controller\ComponentRegistry; use Cake\Event\EventInterface; @@ -79,24 +78,9 @@ public function hasAccess(array $url): bool { * @return array */ protected function _getUser(): array { - if (class_exists(Identity::class)) { - $identity = $this->identity(); + $identity = $this->identity(); - return $identity ? $this->_toArray($identity) : []; - } - - // We skip for new plugin(s) - if ($this->getController()->components()->has('Authentication')) { - return []; - } - - // Fallback to old Auth style - if (!$this->getController()->components()->has('Auth')) { - $this->getController()->loadComponent('TinyAuth.Auth'); - } - - /** @phpstan-ignore property.notFound */ - return (array)$this->getController()->Auth->user(); + return $identity ? $this->_toArray($identity) : []; } } diff --git a/src/Controller/Component/AuthenticationComponent.php b/src/Controller/Component/AuthenticationComponent.php index fc5b3790..2042b9df 100644 --- a/src/Controller/Component/AuthenticationComponent.php +++ b/src/Controller/Component/AuthenticationComponent.php @@ -6,7 +6,6 @@ use Cake\Controller\ComponentRegistry; use Cake\Core\Exception\CakeException; use Cake\Routing\Router; -use RuntimeException; use TinyAuth\Auth\AllowTrait; use TinyAuth\Utility\Config; @@ -31,10 +30,6 @@ public function __construct(ComponentRegistry $registry, array $config = []) { $config += Config::all(); parent::__construct($registry, $config); - - if ($registry->has('Auth') && get_class($registry->get('Auth')) === AuthComponent::class) { - throw new RuntimeException('You cannot use TinyAuth.Authentication component and former TinyAuth.Auth component together.'); - } } /** diff --git a/src/Controller/Component/AuthorizationComponent.php b/src/Controller/Component/AuthorizationComponent.php index 2c5c7363..d2f2793b 100644 --- a/src/Controller/Component/AuthorizationComponent.php +++ b/src/Controller/Component/AuthorizationComponent.php @@ -4,7 +4,6 @@ use Authorization\Controller\Component\AuthorizationComponent as CakeAuthorizationComponent; use Cake\Controller\ComponentRegistry; -use RuntimeException; use TinyAuth\Auth\AclTrait; use TinyAuth\Auth\AllowTrait; use TinyAuth\Utility\Config; @@ -37,10 +36,6 @@ public function __construct(ComponentRegistry $registry, array $config = []) { parent::__construct($registry, $config); - if ($registry->has('Auth') && get_class($registry->get('Auth')) === AuthComponent::class) { - throw new RuntimeException('You cannot use TinyAuth.Authorization component and former TinyAuth.Auth component together.'); - } - if ($registry->getController()->components()->has('Authentication')) { /** @var \TinyAuth\Controller\Component\AuthenticationComponent $authentication */ $authentication = $registry->getController()->components()->get('Authentication'); diff --git a/src/Controller/Component/LegacyAuthComponent.php b/src/Controller/Component/LegacyAuthComponent.php deleted file mode 100644 index 62774e69..00000000 --- a/src/Controller/Component/LegacyAuthComponent.php +++ /dev/null @@ -1,989 +0,0 @@ -Auth->setConfig('authenticate', [ - * 'Form' => [ - * 'userModel' => 'Users.Users' - * ] - * ]); - * ``` - * - * Using the class name without 'Authenticate' as the key, you can pass in an - * array of config for each authentication object. Additionally, you can define - * config that should be set to all authentications objects using the 'all' key: - * - * ``` - * $this->Auth->setConfig('authenticate', [ - * AuthComponent::ALL => [ - * 'userModel' => 'Users.Users', - * 'scope' => ['Users.active' => 1] - * ], - * 'Form', - * 'Basic' - * ]); - * ``` - * - * - `authorize` - An array of authorization objects to use for authorizing users. - * You can configure multiple adapters and they will be checked sequentially - * when authorization checks are done. - * - * ``` - * $this->Auth->setConfig('authorize', [ - * 'Crud' => [ - * 'actionPath' => 'controllers/' - * ] - * ]); - * ``` - * - * Using the class name without 'Authorize' as the key, you can pass in an array - * of config for each authorization object. Additionally you can define config - * that should be set to all authorization objects using the AuthComponent::ALL key: - * - * ``` - * $this->Auth->setConfig('authorize', [ - * AuthComponent::ALL => [ - * 'actionPath' => 'controllers/' - * ], - * 'Crud', - * 'CustomAuth' - * ]); - * ``` - * - * - `flash` - Settings to use when Auth needs to do a flash message with - * FlashComponent::set(). Available keys are: - * - * - `key` - The message domain to use for flashes generated by this component, - * defaults to 'auth'. - * - `element` - Flash element to use, defaults to 'default'. - * - `params` - The array of additional params to use, defaults to ['class' => 'error'] - * - * - `loginAction` - A URL (defined as a string or array) to the controller action - * that handles logins. Defaults to `/users/login`. - * - * - `loginRedirect` - Normally, if a user is redirected to the `loginAction` page, - * the location they were redirected from will be stored in the session so that - * they can be redirected back after a successful login. If this session value - * is not set, redirectUrl() method will return the URL specified in `loginRedirect`. - * - * - `logoutRedirect` - The default action to redirect to after the user is logged out. - * While AuthComponent does not handle post-logout redirection, a redirect URL - * will be returned from `AuthComponent::logout()`. Defaults to `loginAction`. - * - * - `authError` - Error to display when user attempts to access an object or - * action to which they do not have access. - * - * - `unauthorizedRedirect` - Controls handling of unauthorized access. - * - * - For default value `true` unauthorized user is redirected to the referrer URL - * or `$loginRedirect` or '/'. - * - If set to a string or array the value is used as a URL to redirect to. - * - If set to false a `ForbiddenException` exception is thrown instead of redirecting. - * - * - `storage` - Storage class to use for persisting user record. When using - * stateless authenticator you should set this to 'Memory'. Defaults to 'Session'. - * - * - `checkAuthIn` - Name of event for which initial auth checks should be done. - * Defaults to 'Controller.startup'. You can set it to 'Controller.initialize' - * if you want the check to be done before controller's beforeFilter() is run. - * - * @var array - */ - protected array $_defaultConfig = [ - 'authenticate' => null, - 'authorize' => null, - 'flash' => null, - 'loginAction' => null, - 'loginRedirect' => null, - 'logoutRedirect' => null, - 'authError' => null, - 'unauthorizedRedirect' => true, - 'storage' => 'TinyAuth.Session', - 'checkAuthIn' => 'Controller.startup', - ]; - - /** - * Other components utilized by AuthComponent - * - * @var array - */ - protected array $components = ['RequestHandler', 'Flash']; - - /** - * Objects that will be used for authentication checks. - * - * @var array<\TinyAuth\Auth\BaseAuthenticate> - */ - protected array $_authenticateObjects = []; - - /** - * Objects that will be used for authorization checks. - * - * @var array - */ - protected array $_authorizeObjects = []; - - /** - * Storage object. - * - * @var \TinyAuth\Auth\Storage\StorageInterface|null - */ - protected $_storage; - - /** - * Controller actions for which user validation is not required. - * - * @var array - * @see \Cake\Controller\Component\AuthComponent::allow() - */ - public $allowedActions = []; - - /** - * The instance of the Authenticate provider that was used for - * successfully logging in the current user after calling `login()` - * in the same request - * - * @var \TinyAuth\Auth\BaseAuthenticate|null - */ - protected $_authenticationProvider; - - /** - * The instance of the Authorize provider that was used to grant - * access to the current user to the URL they are requesting. - * - * @var \TinyAuth\Auth\BaseAuthorize|null - */ - protected $_authorizationProvider; - - /** - * Initialize properties. - * - * @param array $config The config data. - * @return void - */ - public function initialize(array $config): void { - $controller = $this->_registry->getController(); - $this->setEventManager($controller->getEventManager()); - } - - /** - * Callback for Controller.startup event. - * - * @param \Cake\Event\EventInterface $event Event instance. - * @return void - */ - public function startup(EventInterface $event): void { - $check = $this->authCheck($event); - if ($check !== null) { - $event->setResult($check); - } - } - - /** - * Main execution method, handles initial authentication check and redirection - * of invalid users. - * - * The auth check is done when event name is same as the one configured in - * `checkAuthIn` config. - * - * @param \Cake\Event\EventInterface $event Event instance. - * @throws \ReflectionException - * @return \Cake\Http\Response|null - */ - public function authCheck(EventInterface $event): ?Response { - if ($this->_config['checkAuthIn'] !== $event->getName()) { - return null; - } - - /** @var \Cake\Controller\Controller $controller */ - $controller = $event->getSubject(); - - $action = $controller->getRequest()->getParam('action'); - if ($action === null || !$controller->isAction($action)) { - return null; - } - - $this->_setDefaults(); - - if ($this->_isAllowed($controller)) { - return null; - } - - $isLoginAction = $this->_isLoginAction($controller); - - if (!$this->_getUser()) { - if ($isLoginAction) { - return null; - } - $result = $this->_unauthenticated($controller); - if ($result instanceof Response) { - $event->stopPropagation(); - } - - return $result; - } - - if ( - $isLoginAction || - empty($this->_config['authorize']) || - $this->isAuthorized($this->user()) - ) { - return null; - } - - $event->stopPropagation(); - - return $this->_unauthorized($controller); - } - - /** - * Events supported by this component. - * - * @return array - */ - public function implementedEvents(): array { - return [ - 'Controller.initialize' => 'authCheck', - 'Controller.startup' => 'startup', - ]; - } - - /** - * Checks whether current action is accessible without authentication. - * - * @param \Cake\Controller\Controller $controller A reference to the instantiating - * controller object - * @return bool True if action is accessible without authentication else false - */ - protected function _isAllowed(Controller $controller): bool { - $action = strtolower($controller->getRequest()->getParam('action', '')); - - return in_array($action, array_map('strtolower', $this->allowedActions), true); - } - - /** - * Handles unauthenticated access attempt. First the `unauthenticated()` method - * of the last authenticator in the chain will be called. The authenticator can - * handle sending response or redirection as appropriate and return `true` to - * indicate no further action is necessary. If authenticator returns null this - * method redirects user to login action. - * - * @param \Cake\Controller\Controller $controller A reference to the controller object. - * @throws \Cake\Core\Exception\CakeException - * @return \Cake\Http\Response|null Null if current action is login action - * else response object returned by authenticate object or Controller::redirect(). - */ - protected function _unauthenticated(Controller $controller): ?Response { - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - $response = $controller->getResponse(); - $auth = end($this->_authenticateObjects); - if ($auth === false) { - throw new CakeException('At least one authenticate object must be available.'); - } - $result = $auth->unauthenticated($controller->getRequest(), $response); - if ($result !== null) { - return $result; - } - - if (!$controller->getRequest()->is('ajax')) { - $this->flash($this->_config['authError']); - - return $controller->redirect($this->_loginActionRedirectUrl()); - } - - return $response->withStatus(403); - } - - /** - * Returns the URL of the login action to redirect to. - * - * This includes the redirect query string if applicable. - * - * @return array|string - */ - protected function _loginActionRedirectUrl() { - $urlToRedirectBackTo = $this->_getUrlToRedirectBackTo(); - - $loginAction = $this->_config['loginAction']; - if ($urlToRedirectBackTo === '/') { - return $loginAction; - } - - if (is_array($loginAction)) { - $loginAction['?'][static::QUERY_STRING_REDIRECT] = $urlToRedirectBackTo; - } else { - $char = strpos($loginAction, '?') === false ? '?' : '&'; - $loginAction .= $char . static::QUERY_STRING_REDIRECT . '=' . urlencode($urlToRedirectBackTo); - } - - return $loginAction; - } - - /** - * Normalizes config `loginAction` and checks if current request URL is same as login action. - * - * @param \Cake\Controller\Controller $controller A reference to the controller object. - * @return bool True if current action is login action else false. - */ - protected function _isLoginAction(Controller $controller): bool { - $uri = $controller->getRequest()->getUri(); - $url = Router::normalize($uri->getPath()); - $loginAction = Router::normalize($this->_config['loginAction']); - - return $loginAction === $url; - } - - /** - * Handle unauthorized access attempt - * - * @param \Cake\Controller\Controller $controller A reference to the controller object - * @throws \Cake\Http\Exception\ForbiddenException - * @return \Cake\Http\Response|null - */ - protected function _unauthorized(Controller $controller): ?Response { - if ($this->_config['unauthorizedRedirect'] === false) { - throw new ForbiddenException($this->_config['authError']); - } - - $this->flash($this->_config['authError']); - if ($this->_config['unauthorizedRedirect'] === true) { - $default = '/'; - if (!empty($this->_config['loginRedirect'])) { - $default = $this->_config['loginRedirect']; - } - if (is_array($default)) { - $default['_base'] = false; - } - $url = $controller->referer($default, true); - } else { - $url = $this->_config['unauthorizedRedirect']; - } - - return $controller->redirect($url); - } - - /** - * Sets defaults for configs. - * - * @return void - */ - protected function _setDefaults(): void { - $defaults = [ - 'authenticate' => ['TinyAuth.Form'], - 'flash' => [ - 'element' => 'error', - 'key' => 'flash', - 'params' => ['class' => 'error'], - ], - 'loginAction' => [ - 'controller' => 'Users', - 'action' => 'login', - 'plugin' => null, - ], - 'logoutRedirect' => $this->_config['loginAction'], - 'authError' => __d('cake', 'You are not authorized to access that location.'), - ]; - - $config = $this->getConfig(); - foreach ($config as $key => $value) { - if ($value !== null) { - unset($defaults[$key]); - } - } - $this->setConfig($defaults); - } - - /** - * Check if the provided user is authorized for the request. - * - * Uses the configured Authorization adapters to check whether a user is authorized. - * Each adapter will be checked in sequence, if any of them return true, then the user will - * be authorized for the request. - * - * @param \ArrayAccess|array|null $user The user to check the authorization of. - * If empty the user fetched from storage will be used. - * @param \Cake\Http\ServerRequest|null $request The request to authenticate for. - * If empty, the current request will be used. - * @return bool True if $user is authorized, otherwise false - */ - public function isAuthorized($user = null, ?ServerRequest $request = null): bool { - if (!$user && !$this->user()) { - return false; - } - if (!$user) { - $user = $this->user(); - } - if (!$request) { - $request = $this->getController()->getRequest(); - } - if (!$this->_authorizeObjects) { - $this->constructAuthorize(); - } - foreach ($this->_authorizeObjects as $authorizer) { - if ($authorizer->authorize($user, $request) === true) { - $this->_authorizationProvider = $authorizer; - - return true; - } - } - - return false; - } - - /** - * Loads the authorization objects configured. - * - * @throws \Cake\Core\Exception\CakeException - * @return array|null The loaded authorization objects, or null when authorize is empty. - */ - public function constructAuthorize(): ?array { - if (empty($this->_config['authorize'])) { - return null; - } - $this->_authorizeObjects = []; - $authorize = Hash::normalize((array)$this->_config['authorize']); - $global = []; - if (isset($authorize[static::ALL])) { - $global = $authorize[static::ALL]; - unset($authorize[static::ALL]); - } - foreach ($authorize as $alias => $config) { - if (!empty($config['className'])) { - $class = $config['className']; - unset($config['className']); - } else { - $class = $alias; - } - $className = App::className($class, 'Auth', 'Authorize'); - if ($className === null) { - throw new CakeException(sprintf('Authorization adapter "%s" was not found.', $class)); - } - if (!method_exists($className, 'authorize')) { - throw new CakeException('Authorization objects must implement an authorize() method.'); - } - $config = (array)$config + $global; - /** @var \TinyAuth\Auth\BaseAuthorize $authorizeObject */ - $authorizeObject = new $className($this->_registry, $config); - $this->_authorizeObjects[$alias] = $authorizeObject; - } - - return $this->_authorizeObjects; - } - - /** - * Getter for authorize objects. Will return a particular authorize object. - * - * @param string $alias Alias for the authorize object - * @return \TinyAuth\Auth\BaseAuthorize|null - */ - public function getAuthorize(string $alias): ?BaseAuthorize { - if (empty($this->_authorizeObjects)) { - $this->constructAuthorize(); - } - - return $this->_authorizeObjects[$alias] ?? null; - } - - /** - * Takes a list of actions in the current controller for which authentication is not required, or - * no parameters to allow all actions. - * - * You can use allow with either an array or a simple string. - * - * ``` - * $this->Auth->allow('view'); - * $this->Auth->allow(['edit', 'add']); - * ``` - * or to allow all actions - * ``` - * $this->Auth->allow(); - * ``` - * - * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#making-actions-public - * @param array|string|null $actions Controller action name or array of actions - * @return void - */ - public function allow($actions = null): void { - if ($actions === null) { - $controller = $this->_registry->getController(); - $this->allowedActions = get_class_methods($controller); - - return; - } - $this->allowedActions = array_merge($this->allowedActions, (array)$actions); - } - - /** - * Removes items from the list of allowed/no authentication required actions. - * - * You can use deny with either an array or a simple string. - * - * ``` - * $this->Auth->deny('view'); - * $this->Auth->deny(['edit', 'add']); - * ``` - * or - * ``` - * $this->Auth->deny(); - * ``` - * to remove all items from the allowed list - * - * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#making-actions-require-authorization - * @see \Cake\Controller\Component\AuthComponent::allow() - * @param array|string|null $actions Controller action name or array of actions - * @return void - */ - public function deny($actions = null): void { - if ($actions === null) { - $this->allowedActions = []; - - return; - } - foreach ((array)$actions as $action) { - $i = array_search($action, $this->allowedActions, true); - if (is_int($i)) { - unset($this->allowedActions[$i]); - } - } - $this->allowedActions = array_values($this->allowedActions); - } - - /** - * Set provided user info to storage as logged in user. - * - * The storage class is configured using `storage` config key or passing - * instance to AuthComponent::storage(). - * - * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#identifying-users-and-logging-them-in - * @param \ArrayAccess|array $user User data. - * @return void - */ - public function setUser($user): void { - $this->storage()->write($user); - } - - /** - * Log a user out. - * - * Returns the logout action to redirect to. Triggers the `Auth.logout` event - * which the authenticate classes can listen for and perform custom logout logic. - * - * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#logging-users-out - * @return string Normalized config `logoutRedirect` - */ - public function logout(): string { - $this->_setDefaults(); - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - $user = (array)$this->user(); - $this->dispatchEvent('Auth.logout', [$user]); - $this->storage()->delete(); - - return Router::normalize($this->_config['logoutRedirect']); - } - - /** - * Get the current user from storage. - * - * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#accessing-the-logged-in-user - * @param string|null $key Field to retrieve. Leave null to get entire User record. - * @return mixed|null Either User record or null if no user is logged in, or retrieved field if key is specified. - */ - public function user(?string $key = null) { - $user = $this->storage()->read(); - if (!$user) { - return null; - } - - if ($key === null) { - return $user; - } - - return Hash::get($user, $key); - } - - /** - * Similar to AuthComponent::user() except if user is not found in - * configured storage, connected authentication objects will have their - * getUser() methods called. - * - * This lets stateless authentication methods function correctly. - * - * @return bool true If a user can be found, false if one cannot. - */ - protected function _getUser(): bool { - $user = $this->user(); - if ($user) { - return true; - } - - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - foreach ($this->_authenticateObjects as $auth) { - $result = $auth->getUser($this->getController()->getRequest()); - if ($result) { - $this->_authenticationProvider = $auth; - /** @var \Cake\Event\Event $event */ - $event = $this->dispatchEvent('Auth.afterIdentify', [$result, $auth]); - if ($event->getResult() !== null) { - $result = $event->getResult(); - } - $this->storage()->write($result); - - return true; - } - } - - return false; - } - - /** - * Get the URL a user should be redirected to upon login. - * - * Pass a URL in to set the destination a user should be redirected to upon - * logging in. - * - * If no parameter is passed, gets the authentication redirect URL. The URL - * returned is as per following rules: - * - * - Returns the normalized redirect URL from storage if it is - * present and for the same domain the current app is running on. - * - If there is no URL returned from storage and there is a config - * `loginRedirect`, the `loginRedirect` value is returned. - * - If there is no session and no `loginRedirect`, / is returned. - * - * @param array|string|null $url Optional URL to write as the login redirect URL. - * @return string Redirect URL - */ - public function redirectUrl($url = null): string { - $redirectUrl = $this->getController()->getRequest()->getQuery(static::QUERY_STRING_REDIRECT); - if ($redirectUrl && (substr($redirectUrl, 0, 1) !== '/' || substr($redirectUrl, 0, 2) === '//')) { - $redirectUrl = null; - } - - if ($url !== null) { - $redirectUrl = $url; - } elseif ($redirectUrl) { - if ( - $this->_config['loginAction'] - && Router::normalize($redirectUrl) === Router::normalize($this->_config['loginAction']) - ) { - $redirectUrl = $this->_config['loginRedirect']; - } - } elseif ($this->_config['loginRedirect']) { - $redirectUrl = $this->_config['loginRedirect']; - } else { - $redirectUrl = '/'; - } - if (is_array($redirectUrl)) { - return Router::url($redirectUrl + ['_base' => false]); - } - - return $redirectUrl; - } - - /** - * Use the configured authentication adapters, and attempt to identify the user - * by credentials contained in $request. - * - * Triggers `Auth.afterIdentify` event which the authenticate classes can listen - * to. - * - * @return array|false User record data, or false, if the user could not be identified. - */ - public function identify() { - $this->_setDefaults(); - - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - foreach ($this->_authenticateObjects as $auth) { - $result = $auth->authenticate( - $this->getController()->getRequest(), - $this->getController()->getResponse(), - ); - if (!empty($result)) { - $this->_authenticationProvider = $auth; - $event = $this->dispatchEvent('Auth.afterIdentify', [$result, $auth]); - if ($event->getResult() !== null) { - return $event->getResult(); - } - - return $result; - } - } - - return false; - } - - /** - * Loads the configured authentication objects. - * - * @throws \Cake\Core\Exception\CakeException - * @return array|null The loaded authorization objects, or null on empty authenticate value. - */ - public function constructAuthenticate(): ?array { - if (empty($this->_config['authenticate'])) { - return null; - } - $this->_authenticateObjects = []; - $authenticate = Hash::normalize((array)$this->_config['authenticate']); - $global = []; - if (isset($authenticate[static::ALL])) { - $global = $authenticate[static::ALL]; - unset($authenticate[static::ALL]); - } - foreach ($authenticate as $alias => $config) { - if (!empty($config['className'])) { - $class = $config['className']; - unset($config['className']); - } else { - $class = $alias; - } - $className = App::className($class, 'Auth', 'Authenticate'); - if ($className === null) { - throw new CakeException(sprintf('Authentication adapter "%s" was not found.', $class)); - } - if (!method_exists($className, 'authenticate')) { - throw new CakeException('Authentication objects must implement an authenticate() method.'); - } - $config = array_merge($global, (array)$config); - /** @var \TinyAuth\Auth\BaseAuthenticate $authenticateObject */ - $authenticateObject = new $className($this->_registry, $config); - $this->_authenticateObjects[$alias] = $authenticateObject; - $this->getEventManager()->on($this->_authenticateObjects[$alias]); - } - - return $this->_authenticateObjects; - } - - /** - * Set user record storage object. - * - * @param \TinyAuth\Auth\Storage\StorageInterface $storage - * @return void - */ - public function setStorage(StorageInterface $storage): void { - $this->_storage = $storage; - } - - /** - * @return bool - */ - public function hasStorage(): bool { - return $this->_storage !== null; - } - - /** - * Get/set user record storage object. - * - * @return \TinyAuth\Auth\Storage\StorageInterface - */ - public function storage(): StorageInterface { - if ($this->_storage) { - return $this->_storage; - } - - $config = $this->_config['storage']; - if (is_string($config)) { - $class = $config; - $config = []; - } else { - $class = $config['className']; - unset($config['className']); - } - $className = App::className($class, 'Auth/Storage', 'Storage'); - if ($className === null) { - throw new CakeException(sprintf('Auth storage adapter "%s" was not found.', $class)); - } - $request = $this->getController()->getRequest(); - $response = $this->getController()->getResponse(); - /** @var \TinyAuth\Auth\Storage\StorageInterface $storage */ - $storage = new $className($request, $response, $config); - - $this->_storage = $storage; - - return $storage; - } - - /** - * Magic accessor for backward compatibility for property `$sessionKey`. - * - * @param string $name Property name - * @return \Cake\Controller\Component|null - */ - public function __get(string $name): ?Component { - if ($name === 'sessionKey') { - return $this->storage()->getConfig('key'); - } - - return parent::__get($name); - } - - /** - * Magic setter for backward compatibility for property `$sessionKey`. - * - * @param string $name Property name. - * @param mixed $value Value to set. - * @return void - */ - public function __set(string $name, $value): void { - if ($name === 'sessionKey') { - $this->_storage = null; - - if ($value === false) { - $this->setConfig('storage', 'Memory'); - - return; - } - - $this->setConfig('storage', 'Session'); - $this->storage()->setConfig('key', $value); - - return; - } - - $this->{$name} = $value; - } - - /** - * Getter for authenticate objects. Will return a particular authenticate object. - * - * @param string $alias Alias for the authenticate object - * @return \TinyAuth\Auth\BaseAuthenticate|null - */ - public function getAuthenticate(string $alias): ?BaseAuthenticate { - if (empty($this->_authenticateObjects)) { - $this->constructAuthenticate(); - } - - return $this->_authenticateObjects[$alias] ?? null; - } - - /** - * Set a flash message. Uses the Flash component with values from `flash` config. - * - * @param string|false $message The message to set. False to skip. - * @return void - */ - public function flash($message): void { - if ($message === false) { - return; - } - - $this->Flash->set($message, $this->_config['flash']); - } - - /** - * If login was called during this request and the user was successfully - * authenticated, this function will return the instance of the authentication - * object that was used for logging the user in. - * - * @return \TinyAuth\Auth\BaseAuthenticate|null - */ - public function authenticationProvider(): ?BaseAuthenticate { - return $this->_authenticationProvider; - } - - /** - * If there was any authorization processing for the current request, this function - * will return the instance of the Authorization object that granted access to the - * user to the current address. - * - * @return \TinyAuth\Auth\BaseAuthorize|null - */ - public function authorizationProvider(): ?BaseAuthorize { - return $this->_authorizationProvider; - } - - /** - * Returns the URL to redirect back to or / if not possible. - * - * This method takes the referrer into account if the - * request is not of type GET. - * - * @return string - */ - protected function _getUrlToRedirectBackTo(): string { - $urlToRedirectBackTo = $this->getController()->getRequest()->getRequestTarget(); - if (!$this->getController()->getRequest()->is('get')) { - $urlToRedirectBackTo = $this->getController()->referer(); - } - - return $urlToRedirectBackTo; - } - -} diff --git a/src/Sync/Adder.php b/src/Sync/Adder.php index 991345fa..6fbe94f3 100644 --- a/src/Sync/Adder.php +++ b/src/Sync/Adder.php @@ -10,6 +10,14 @@ use TinyAuth\Filesystem\Folder; use TinyAuth\Utility\Utility; +/** + * Helper class for adding specific ACL entries to the INI configuration file. + * + * Used by TinyAuthAddCommand to modify auth_acl.ini file with new or updated + * controller/action/role mappings. + * + * @internal + */ class Adder { /** @@ -31,9 +39,22 @@ public function __construct() { protected $authAllow; /** - * @param string $controller - * @param string $action - * @param array $roles + * Adds or updates a controller/action entry in the ACL INI file. + * + * Files modified: + * - config/auth_acl.ini (default) or custom path from TinyAuth.aclFilePath + * + * File format example: + * ```ini + * [Articles] + * index = user, admin + * add = admin + * * = admin + * ``` + * + * @param string $controller Controller name (e.g., 'Articles' or 'MyPlugin.Admin/Articles') + * @param string $action Action name (e.g., 'index') or '*' for all actions + * @param array $roles Role names to grant access (e.g., ['user', 'admin']) * @param \Cake\Console\Arguments $args * @param \Cake\Console\ConsoleIo $io * diff --git a/src/Sync/Syncer.php b/src/Sync/Syncer.php index a73c6dea..36007a68 100644 --- a/src/Sync/Syncer.php +++ b/src/Sync/Syncer.php @@ -18,8 +18,26 @@ class Syncer { protected $authAllow; /** - * @param \Cake\Console\Arguments $args - * @param \Cake\Console\ConsoleIo $io + * Synchronizes all discovered controllers with the ACL INI file. + * + * Files modified: + * - config/auth_acl.ini (default) or custom path from TinyAuth.aclFilePath + * + * Process: + * 1. Scans src/Controller/ (and plugin controllers if specified) + * 2. Finds all controllers that don't have ACL entries + * 3. Adds missing controllers with wildcard (*) action and specified roles + * + * Example result in auth_acl.ini: + * ```ini + * [NewController] + * * = user, admin + * ``` + * + * Note: Existing controller entries are preserved and not modified. + * + * @param \Cake\Console\Arguments $args Command arguments including roles + * @param \Cake\Console\ConsoleIo $io Console I/O for output * @return void */ public function syncAcl(Arguments $args, ConsoleIo $io) { diff --git a/tests/TestCase/Auth/FormAuthenticateTest.php b/tests/TestCase/Auth/FormAuthenticateTest.php deleted file mode 100644 index b2b299aa..00000000 --- a/tests/TestCase/Auth/FormAuthenticateTest.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ - protected array $fixtures = [ - 'plugin.TinyAuth.Users', - ]; - - /** - * @return void - */ - public function testAuthenticate() { - $request = new ServerRequest(); - $request = $request->withData('username', 'dereuromark'); - $request = $request->withData('password', '123'); - - $object = new FormAuthenticate(new ComponentRegistry(new Controller($request))); - $result = $object->authenticate($request, new Response()); - $this->assertTrue((bool)$result); - } - -} diff --git a/tests/TestCase/Auth/MultiColumnAuthenticateTest.php b/tests/TestCase/Auth/MultiColumnAuthenticateTest.php deleted file mode 100644 index c0b0983a..00000000 --- a/tests/TestCase/Auth/MultiColumnAuthenticateTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - protected array $fixtures = [ - 'plugin.TinyAuth.Users', - ]; - - /** - * @return void - */ - public function testAuthenticate() { - $request = new ServerRequest(); - $request = $request->withData('login', 'dereuromark'); - $request = $request->withData('password', '123'); - - $object = new MultiColumnAuthenticate(new ComponentRegistry(new Controller($request)), [ - 'fields' => [ - 'username' => 'login', - 'password' => 'password', - ], - 'columns' => ['username', 'email'], - ]); - $result = $object->authenticate($request, new Response()); - $this->assertTrue((bool)$result); - } - -} diff --git a/tests/TestCase/Controller/Component/AuthComponentTest.php b/tests/TestCase/Controller/Component/AuthComponentTest.php deleted file mode 100644 index b22314c6..00000000 --- a/tests/TestCase/Controller/Component/AuthComponentTest.php +++ /dev/null @@ -1,257 +0,0 @@ -componentConfig = [ - 'allowFilePath' => Plugin::path('TinyAuth') . 'tests' . DS . 'test_files' . DS, - 'autoClearCache' => true, - ]; - - $builder = Router::createRouteBuilder('/'); - $builder->setRouteClass(DashedRoute::class); - $builder->scope('/', function (RouteBuilder $routes): void { - $routes->fallbacks(); - }); - } - - /** - * @return void - */ - public function testValid() { - $request = new ServerRequest([ - 'params' => [ - 'controller' => 'Users', - 'action' => 'view', - 'plugin' => null, - '_ext' => null, - 'pass' => [1], - ]]); - $controller = $this->getControllerMock($request); - - $registry = new ComponentRegistry($controller); - $this->AuthComponent = new AuthComponent($registry, $this->componentConfig); - - $config = []; - $this->AuthComponent->initialize($config); - - $event = new Event('Controller.startup', $controller); - $this->AuthComponent->startup($event); - - $response = $event->getResult(); - $this->assertNull($response); - } - - /** - * @return void - */ - public function testValidAnyAction() { - $request = new ServerRequest([ - 'params' => [ - 'plugin' => 'Extras', - 'controller' => 'Offers', - 'action' => 'index', - '_ext' => null, - 'pass' => [1], - ]]); - $controller = new OffersController($request); - - $registry = new ComponentRegistry($controller); - $this->AuthComponent = new AuthComponent($registry, $this->componentConfig); - - $config = []; - $this->AuthComponent->initialize($config); - - $event = new Event('Controller.startup', $controller); - $this->AuthComponent->startup($event); - - $response = $event->getResult(); - $this->assertNull($response); - } - - /** - * @return void - */ - public function testDeniedActionInController() { - $request = new ServerRequest([ - 'params' => [ - 'plugin' => 'Extras', - 'controller' => 'Offers', - 'action' => 'denied', - '_ext' => null, - 'pass' => [1], - ]]); - $controller = new OffersController($request); - $controller->loadComponent('TinyAuth.Auth', $this->componentConfig); - - $config = []; - $controller->Auth->initialize($config); - - $event = new Event('Controller.beforeFilter', $controller); - $controller->beforeFilter($event); - - $event = new Event('Controller.startup', $controller); - $controller->Auth->startup($event); - - $response = $event->getResult(); - $this->assertInstanceOf(Response::class, $response); - $this->assertSame(302, $response->getStatusCode()); - } - - /** - * @return void - */ - public function testDeniedAction() { - $request = new ServerRequest([ - 'params' => [ - 'plugin' => 'Extras', - 'controller' => 'Offers', - 'action' => 'superPrivate', - '_ext' => null, - 'pass' => [1], - ], - ]); - $controller = new OffersController($request); - $controller->loadComponent('TinyAuth.Auth', $this->componentConfig); - - $config = []; - $controller->Auth->initialize($config); - - $event = new Event('Controller.beforeFilter', $controller); - $controller->beforeFilter($event); - - $event = new Event('Controller.startup', $controller); - $controller->Auth->startup($event); - - $response = $event->getResult(); - $this->assertInstanceOf(Response::class, $response); - $this->assertSame(302, $response->getStatusCode()); - } - - /** - * @return void - */ - public function testValidActionNestedPrefix() { - $request = new ServerRequest([ - 'params' => [ - 'plugin' => null, - 'prefix' => 'Admin/MyPrefix', - 'controller' => 'MyTest', - 'action' => 'myPublic', - ]]); - $controller = new MyTestController($request); - - $registry = new ComponentRegistry($controller); - $this->AuthComponent = new AuthComponent($registry, $this->componentConfig); - - $config = []; - $this->AuthComponent->initialize($config); - - $event = new Event('Controller.startup', $controller); - $this->AuthComponent->startup($event); - - $response = $event->getResult(); - $this->assertNull($response); - } - - /** - * @return void - */ - public function testDeniedActionNestedPrefix() { - $request = new ServerRequest([ - 'params' => [ - 'plugin' => null, - 'prefix' => 'admin/my_prefix', - 'controller' => 'MyTest', - 'action' => 'myAll', - ]]); - $controller = new MyTestController($request); - $controller->loadComponent('TinyAuth.Auth', $this->componentConfig); - - $config = []; - $controller->Auth->initialize($config); - - $event = new Event('Controller.beforeFilter', $controller); - $controller->beforeFilter($event); - - $event = new Event('Controller.startup', $controller); - $controller->Auth->startup($event); - - $response = $event->getResult(); - $this->assertInstanceOf(Response::class, $response); - $this->assertSame(302, $response->getStatusCode()); - } - - /** - * @return void - */ - public function testInvalid() { - $request = new ServerRequest([ - 'params' => [ - 'controller' => 'FooBar', - 'action' => 'index', - 'plugin' => null, - '_ext' => null, - 'pass' => [], - ]]); - $controller = $this->getControllerMock($request); - - $registry = new ComponentRegistry($controller); - $this->AuthComponent = new AuthComponent($registry, $this->componentConfig); - - $config = []; - $this->AuthComponent->initialize($config); - - $event = new Event('Controller.startup', $controller); - $this->AuthComponent->startup($event); - - $response = $event->getResult(); - $this->assertInstanceOf(Response::class, $response); - $this->assertSame(302, $response->getStatusCode()); - } - - /** - * @param \Cake\Http\ServerRequest $request - * @return \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject - */ - protected function getControllerMock(ServerRequest $request) { - $controller = $this->getMockBuilder(Controller::class) - ->setConstructorArgs([$request]) - ->onlyMethods(['isAction']) - ->getMock(); - - $controller->expects($this->once())->method('isAction')->willReturn(true); - - return $controller; - } - -} diff --git a/tests/TestCase/Controller/Component/AuthUserComponentTest.php b/tests/TestCase/Controller/Component/AuthUserComponentTest.php index 0dfc962b..a69df4ec 100644 --- a/tests/TestCase/Controller/Component/AuthUserComponentTest.php +++ b/tests/TestCase/Controller/Component/AuthUserComponentTest.php @@ -12,7 +12,6 @@ use Cake\Controller\ComponentRegistry; use Cake\Controller\Controller; use Cake\Core\Configure; -use Cake\Core\Plugin; use Cake\Event\Event; use Cake\Http\ServerRequest; use Cake\ORM\Entity; @@ -20,7 +19,6 @@ use InvalidArgumentException; use IteratorAggregate; use TestApp\Controller\Component\TestAuthUserComponent; -use TinyAuth\Controller\Component\AuthComponent; use TinyAuth\Utility\Cache; use Traversable; @@ -54,10 +52,6 @@ public function setUp(): void { $this->controller = new Controller(new ServerRequest()); $componentRegistry = new ComponentRegistry($this->controller); $this->AuthUser = new TestAuthUserComponent($componentRegistry); - $this->controller->loadComponent('TinyAuth.Auth', [ - 'allowFilePath' => Plugin::path('TinyAuth') . 'tests' . DS . 'test_files' . DS, - ]); - $this->controller->Auth = $this->getMockBuilder(AuthComponent::class)->onlyMethods(['user'])->setConstructorArgs([$componentRegistry, $config])->getMock(); Configure::write('Roles', [ 'user' => 1, diff --git a/tests/TestCase/Panel/AuthPanelTest.php b/tests/TestCase/Panel/AuthPanelTest.php index c079477b..cd8ef31c 100644 --- a/tests/TestCase/Panel/AuthPanelTest.php +++ b/tests/TestCase/Panel/AuthPanelTest.php @@ -62,7 +62,7 @@ public function tearDown(): void { */ public function testPanelRestrictedAction() { $controller = new Controller(new ServerRequest()); - $controller->loadComponent('TinyAuth.Auth'); + $controller->loadComponent('TinyAuth.Authentication'); $event = new Event('event', $controller); $this->panel->shutdown($event); @@ -88,7 +88,7 @@ public function testPanelPublicAction() { $request = new ServerRequest(['url' => '/users']); $request = $request->withAttribute('params', $url); $controller = new Controller($request); - $controller->loadComponent('TinyAuth.Auth'); + $controller->loadComponent('TinyAuth.Authentication'); $event = new Event('event', $controller); $this->panel->shutdown($event); @@ -118,7 +118,7 @@ public function testPanelAclRestricted() { $request = new ServerRequest(['url' => '/tags']); $request = $request->withAttribute('params', $url); $controller = new Controller($request); - $controller->loadComponent('TinyAuth.Auth'); + $controller->loadComponent('TinyAuth.Authentication'); $event = new Event('event', $controller); $this->panel->shutdown($event); @@ -155,7 +155,7 @@ public function testPanelAclAllowed() { $request = $request->withAttribute('identity', $identity); $controller = new Controller($request); - $controller->loadComponent('TinyAuth.Auth'); + $controller->loadComponent('TinyAuth.Authentication'); $event = new Event('event', $controller); $this->panel->shutdown($event);