├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── composer.json ├── config ├── Migrations │ └── 20160815092405_TinyAuthMultiSample.php ├── auth_acl.default.ini └── auth_allow.default.ini ├── docs ├── AuthPanel.md ├── Authentication.md ├── AuthenticationAdapter.md ├── AuthenticationPlugin.md ├── Authorization.md ├── AuthorizationAdapter.md ├── AuthorizationPlugin.md ├── Multi-role.md ├── README.md └── img │ ├── auth_public.png │ ├── auth_restricted.png │ ├── panel.png │ └── panel_guest.png ├── phpcs.xml ├── phpstan.neon ├── src ├── Auth │ ├── AbstractPasswordHasher.php │ ├── AclAdapter │ │ ├── AclAdapterInterface.php │ │ └── IniAclAdapter.php │ ├── AclTrait.php │ ├── AllowAdapter │ │ ├── AllowAdapterInterface.php │ │ └── IniAllowAdapter.php │ ├── AllowTrait.php │ ├── AuthUserTrait.php │ ├── BaseAuthenticate.php │ ├── BaseAuthorize.php │ ├── BasicAuthenticate.php │ ├── ControllerAuthorize.php │ ├── DefaultPasswordHasher.php │ ├── DigestAuthenticate.php │ ├── FallbackPasswordHasher.php │ ├── FormAuthenticate.php │ ├── MultiColumnAuthenticate.php │ ├── PasswordHasherFactory.php │ ├── Storage │ │ ├── MemoryStorage.php │ │ ├── SessionStorage.php │ │ └── StorageInterface.php │ ├── TinyAuthorize.php │ └── WeakPasswordHasher.php ├── Authenticator │ └── PrimaryKeySessionAuthenticator.php ├── Command │ ├── TinyAuthAddCommand.php │ └── TinyAuthSyncCommand.php ├── Controller │ └── Component │ │ ├── AuthComponent.php │ │ ├── AuthUserComponent.php │ │ ├── AuthenticationComponent.php │ │ ├── AuthorizationComponent.php │ │ └── LegacyAuthComponent.php ├── Filesystem │ └── Folder.php ├── Middleware │ ├── RequestAuthorizationMiddleware.php │ └── UnauthorizedHandler │ │ ├── ForbiddenCakeRedirectHandler.php │ │ └── ForbiddenRedirectHandler.php ├── Panel │ └── AuthPanel.php ├── Policy │ └── RequestPolicy.php ├── Sync │ ├── Adder.php │ └── Syncer.php ├── TinyAuthPlugin.php ├── Utility │ ├── Cache.php │ ├── Config.php │ ├── SessionCache.php │ ├── TinyAuth.php │ └── Utility.php └── View │ └── Helper │ ├── AuthUserHelper.php │ └── AuthenticationHelper.php ├── templates └── element │ └── auth_panel.php └── tests ├── Fixture ├── DatabaseRolesFixture.php ├── DatabaseRolesUsersFixture.php ├── DatabaseUserRolesFixture.php ├── EmptyRolesFixture.php ├── RolesUsersFixture.php └── UsersFixture.php ├── config ├── bootstrap.php └── routes.php ├── phpstan.neon └── schema.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | testsuite: 10 | runs-on: ubuntu-22.04 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php-version: ['8.1', '8.4'] 15 | db-type: [sqlite, mysql, pgsql] 16 | prefer-lowest: [''] 17 | include: 18 | - php-version: '8.1' 19 | db-type: 'sqlite' 20 | prefer-lowest: 'prefer-lowest' 21 | 22 | services: 23 | postgres: 24 | image: postgres 25 | ports: 26 | - 5432:5432 27 | env: 28 | POSTGRES_PASSWORD: postgres 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Setup Service 34 | if: matrix.db-type == 'mysql' 35 | run: | 36 | sudo service mysql start 37 | mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE cakephp;' 38 | - name: Setup PHP 39 | uses: shivammathur/setup-php@v2 40 | with: 41 | php-version: ${{ matrix.php-version }} 42 | extensions: mbstring, intl, pdo_${{ matrix.db-type }} 43 | coverage: pcov 44 | 45 | - name: Get composer cache directory 46 | id: composercache 47 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 48 | 49 | - name: Cache dependencies 50 | uses: actions/cache@v4 51 | with: 52 | path: ${{ steps.composercache.outputs.dir }} 53 | key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} 54 | 55 | - name: Composer install 56 | run: | 57 | composer --version 58 | if ${{ matrix.prefer-lowest == 'prefer-lowest' }} 59 | then 60 | composer update --prefer-lowest --prefer-stable 61 | composer require --dev dereuromark/composer-prefer-lowest:dev-master 62 | else 63 | composer install --no-progress --prefer-dist --optimize-autoloader 64 | fi 65 | - name: Run PHPUnit 66 | run: | 67 | if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi 68 | if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi 69 | if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi 70 | if [[ ${{ matrix.php-version }} == '8.1' ]]; then 71 | vendor/bin/phpunit --coverage-clover=coverage.xml 72 | else 73 | vendor/bin/phpunit 74 | fi 75 | - name: Validate prefer-lowest 76 | if: matrix.prefer-lowest == 'prefer-lowest' 77 | run: vendor/bin/validate-prefer-lowest -m 78 | 79 | - name: Upload coverage reports to Codecov 80 | if: success() && matrix.php-version == '8.1' 81 | uses: codecov/codecov-action@v4 82 | with: 83 | token: ${{ secrets.CODECOV_TOKEN }} 84 | 85 | validation: 86 | name: Coding Standard & Static Analysis 87 | runs-on: ubuntu-22.04 88 | 89 | steps: 90 | - uses: actions/checkout@v4 91 | 92 | - name: Setup PHP 93 | uses: shivammathur/setup-php@v2 94 | with: 95 | php-version: '8.1' 96 | extensions: mbstring, intl 97 | coverage: none 98 | 99 | - name: Get composer cache directory 100 | id: composercache 101 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 102 | 103 | - name: Cache dependencies 104 | uses: actions/cache@v4 105 | with: 106 | path: ${{ steps.composercache.outputs.dir }} 107 | key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} 108 | 109 | - name: Composer Install 110 | run: composer stan-setup 111 | 112 | - name: Run phpstan 113 | run: composer stan 114 | 115 | - name: Run phpcs 116 | run: composer cs-check 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mark Scherer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP TinyAuth Plugin 2 | 3 | [![CI](https://github.com/dereuromark/cakephp-tinyauth/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dereuromark/cakephp-tinyauth/actions/workflows/ci.yml?query=branch%3Amaster) 4 | [![Latest Stable Version](https://poser.pugx.org/dereuromark/cakephp-tinyauth/v/stable.svg)](https://packagist.org/packages/dereuromark/cakephp-tinyauth) 5 | [![Coverage Status](https://img.shields.io/codecov/c/github/dereuromark/cakephp-tinyauth/master.svg)](https://codecov.io/github/dereuromark/cakephp-tinyauth/branch/master) 6 | [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/) 7 | [![License](https://poser.pugx.org/dereuromark/cakephp-tinyauth/license.svg)](LICENSE) 8 | [![Total Downloads](https://poser.pugx.org/dereuromark/cakephp-tinyauth/d/total.svg)](https://packagist.org/packages/dereuromark/cakephp-tinyauth) 9 | [![Coding Standards](https://img.shields.io/badge/cs-PSR--2--R-yellow.svg)](https://github.com/php-fig-rectified/fig-rectified-standards) 10 | 11 | A CakePHP plugin to handle authentication and user authorization the easy way. 12 | 13 | This branch is for **CakePHP 5.1+**. For details see [version map](https://github.com/dereuromark/cakephp-tinyauth/wiki#cakephp-version-map). 14 | 15 | ## Features 16 | 17 | ### Authentication 18 | What are public actions, which ones need login? 19 | 20 | - Powerful default configs to get you started right away. 21 | - [Quick Setup](https://github.com/dereuromark/cakephp-tinyauth/blob/master/docs/Authentication.md#quick-setups) for 5 minute integration. 22 | 23 | ### Authorization 24 | Once you are logged in, what actions can you see with your role(s)? 25 | 26 | - Single-role: 1 user has 1 role (users and roles table for example) 27 | - Multi-role: 1 user can have 1...n roles (users, roles and a "roles_users" pivot table for example) 28 | - [Quick Setup](https://github.com/dereuromark/cakephp-tinyauth/blob/master/docs/Authorization.md#quick-setups) for 5 minute integration. 29 | 30 | ### Useful helpers 31 | - AuthUser Component and Helper for stateful and stateless "auth data" access. 32 | - Authentication Component and Helper for `isPublic()` check on current other other actions. 33 | - Auth DebugKit panel for detailed insights into current URL and auth status, identity content and more. 34 | 35 | ## What's the idea? 36 | Default CakePHP authentication and authorization depends on code changes in at least each controller, maybe more classes. 37 | This plugin hooks in with a single line of change and manages all that using config files and there is no need to touch all those controllers, including plugin controllers. 38 | 39 | It is also possible to manage the config files without the need to code. 40 | And it can with adapters also be moved completely to the DB and managed by CRUD backend. 41 | 42 | Ask yourself: Do you need the overhead and complexity involved with a full blown (RBAC DB) ACL or very specific Policy approaches? 43 | See also my post [acl-access-control-lists-revised/](https://www.dereuromark.de/2015/01/06/acl-access-control-lists-revised/). 44 | If not, then this plugin could very well be your answer and a super quick solution to your auth problem :) 45 | 46 | But even if you don't leverage the full authentication or authorization potential, the available AuthUserComponent and AuthUserHelper 47 | can be very useful when dealing with role based decisions in your controller or view level. They also work stand-alone. 48 | 49 | 50 | ## Demo 51 | See https://sandbox.dereuromark.de/auth-sandbox 52 | 53 | ### auth_allow.ini 54 | Define the public actions (accessible by anyone) per controller: 55 | ```ini 56 | Users = index,view 57 | Admin/Maintenance = pingCheck 58 | PluginName.SomeController = * 59 | MyPlugin.Api/V1 = * 60 | ``` 61 | 62 | ### auth_acl.ini 63 | Define what actions may be accessed by what logged-in user role: 64 | ```ini 65 | [Users] 66 | index = * 67 | add,edit = user,super-user 68 | 69 | [Admin/Users] 70 | * = admin 71 | 72 | [Translate.Admin/Languages] 73 | * = * 74 | ``` 75 | 76 | ### AuthUser component and helper 77 | ```php 78 | $currentId = $this->AuthUser->id(); 79 | 80 | $isMe = $this->AuthUser->isMe($userEntity->id); 81 | 82 | if ($this->AuthUser->hasRole('mod')) { 83 | } 84 | 85 | if ($this->AuthUser->hasAccess(['action' => 'secretArea'])) { 86 | } 87 | 88 | // Helper only 89 | echo $this->AuthUser->link('Admin Backend', ['prefix' => 'Admin', 'action' => 'index']); 90 | echo $this->AuthUser->postLink('Delete', ['action' => 'delete', $id], ['confirm' => 'Sure?']); 91 | ``` 92 | 93 | ## Installation 94 | Including the plugin is pretty much as with every other CakePHP plugin: 95 | 96 | ```bash 97 | composer require dereuromark/cakephp-tinyauth 98 | ``` 99 | 100 | Then, to load the plugin: 101 | 102 | ```sh 103 | bin/cake plugin load TinyAuth 104 | ``` 105 | 106 | That's it. It should be up and running. 107 | 108 | ## Docs 109 | For setup and usage see [Docs](/docs). 110 | 111 | Also note the original [blog post](https://www.dereuromark.de/2011/12/18/tinyauth-the-fastest-and-easiest-authorization-for-cake2/) and how it all started. 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dereuromark/cakephp-tinyauth", 3 | "description": "A CakePHP plugin to handle user authentication and authorization the easy way.", 4 | "license": "MIT", 5 | "type": "cakephp-plugin", 6 | "keywords": [ 7 | "cakephp", 8 | "plugin", 9 | "tinyauth", 10 | "authentication", 11 | "authorization", 12 | "roles" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Mark Scherer", 17 | "homepage": "https://www.dereuromark.de", 18 | "role": "Maintainer" 19 | } 20 | ], 21 | "homepage": "https://github.com/dereuromark/cakephp-tinyauth", 22 | "support": { 23 | "source": "https://github.com/dereuromark/cakephp-tinyauth" 24 | }, 25 | "require": { 26 | "php": ">=8.1", 27 | "cakephp/cakephp": "^5.1.1" 28 | }, 29 | "require-dev": { 30 | "cakephp/authentication": "^3.0.1", 31 | "cakephp/authorization": "^3.0.1", 32 | "cakephp/debug_kit": "^5.0.1", 33 | "composer/semver": "^3.0", 34 | "fig-r/psr2r-sniffer": "dev-master", 35 | "phpunit/phpunit": "^10.5 || ^11.5 || ^12.1" 36 | }, 37 | "minimum-stability": "stable", 38 | "prefer-stable": true, 39 | "autoload": { 40 | "psr-4": { 41 | "TinyAuth\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Cake\\Test\\": "vendor/cakephp/cakephp/tests/", 47 | "TestApp\\": "tests/test_app/", 48 | "TinyAuth\\Test\\": "tests/" 49 | } 50 | }, 51 | "config": { 52 | "allow-plugins": { 53 | "dealerdirect/phpcodesniffer-composer-installer": true 54 | } 55 | }, 56 | "scripts": { 57 | "cs-check": "phpcs --extensions=php", 58 | "cs-fix": "phpcbf --extensions=php", 59 | "lowest": "validate-prefer-lowest", 60 | "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json", 61 | "stan": "phpstan analyse", 62 | "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^2.0.0 && mv composer.backup composer.json", 63 | "test": "phpunit", 64 | "test-coverage": "phpunit --log-junit tmp/coverage/unitreport.xml --coverage-html tmp/coverage --coverage-clover tmp/coverage/coverage.xml" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/Migrations/20160815092405_TinyAuthMultiSample.php: -------------------------------------------------------------------------------- 1 | table('roles') 14 | ->addColumn('name', 'string', [ 15 | 'default' => null, 16 | 'limit' => 64, 17 | 'null' => false, 18 | ]) 19 | ->addColumn('description', 'string', [ 20 | 'default' => null, 21 | 'limit' => 255, 22 | 'null' => false, 23 | ]) 24 | ->addColumn('alias', 'string', [ 25 | 'default' => null, 26 | 'limit' => 20, 27 | 'null' => false, 28 | ]) 29 | ->addColumn('created', 'datetime', [ 30 | 'default' => null, 31 | 'limit' => null, 32 | 'null' => true, 33 | ]) 34 | ->addColumn('modified', 'datetime', [ 35 | 'default' => null, 36 | 'limit' => null, 37 | 'null' => true, 38 | ]) 39 | ->create(); 40 | 41 | $this->table('users') 42 | ->addColumn('username', 'string', [ 43 | 'default' => null, 44 | 'limit' => 255, 45 | 'null' => true, 46 | ]) 47 | ->addColumn('password', 'string', [ 48 | 'default' => null, 49 | 'limit' => 255, 50 | 'null' => true, 51 | ]) 52 | ->addColumn('created', 'datetime', [ 53 | 'default' => null, 54 | 'limit' => null, 55 | 'null' => true, 56 | ]) 57 | ->addColumn('modified', 'datetime', [ 58 | 'default' => null, 59 | 'limit' => null, 60 | 'null' => true, 61 | ]) 62 | ->create(); 63 | 64 | $this->table('roles_users') 65 | ->addColumn('user_id', 'integer', [ 66 | 'default' => null, 67 | 'limit' => 11, 68 | 'null' => true, 69 | ]) 70 | ->addColumn('role_id', 'integer', [ 71 | 'default' => null, 72 | 'limit' => 11, 73 | 'null' => true, 74 | ]) 75 | ->create(); 76 | } 77 | 78 | /** 79 | * Drops tables 80 | * 81 | * @return void 82 | */ 83 | public function down() 84 | { 85 | $this->table('roles')->drop()->save(); 86 | $this->table('users')->drop()->save(); 87 | $this->table('roles_users')->drop()->save(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /config/auth_acl.default.ini: -------------------------------------------------------------------------------- 1 | ; If you use plugins like dereuromark/cakephp-databaselog 2 | [DatabaseLog.Admin/DatabaseLog] 3 | * = admin 4 | [DatabaseLog.Admin/Logs] 5 | * = admin 6 | 7 | ; If you use dereuromark/cakephp-queue 8 | [Queue.Admin/Queue] 9 | * = admin 10 | [Queue.Admin/QueuedJobs] 11 | * = admin 12 | [Queue.Admin/QueueProcesses] 13 | * = admin 14 | -------------------------------------------------------------------------------- /config/auth_allow.default.ini: -------------------------------------------------------------------------------- 1 | ; By default all pages are public 2 | Pages = * 3 | 4 | ; Login and alike need public access, too 5 | Users = login,logout 6 | 7 | ; If you have plugins installed like DebugKit 8 | DebugKit.Dashboard = * 9 | DebugKit.Requests = * 10 | DebugKit.Panels = * 11 | DebugKit.Toolbar = * 12 | 13 | ; If you use cakedc/mixer as require-dev 14 | CakeDC/Mixer.Mixer 15 | 16 | ; If you use dereuromark/cakephp-test-helper as require-dev 17 | TestHelper.TestCases = * 18 | TestHelper.TestHelper = * 19 | 20 | ; If you use dereuromark/cakephp-feedback 21 | Feedback.Feedback = save 22 | 23 | ; ... 24 | -------------------------------------------------------------------------------- /docs/AuthPanel.md: -------------------------------------------------------------------------------- 1 | ## DebugKit Auth Panel 2 | The TinyAuth plugin ships with a useful DebugKit panel to show quickly if the current action 3 | - is public (allowed in auth_allow.ini) or protected 4 | - if protected what roles have access to it 5 | 6 | Also: 7 | - auth status of current user (guest, logged in, ...) 8 | - if logged in your current role(s) 9 | 10 | Public action (quick icon): 11 | 12 | ![public](img/auth_public.png) 13 | 14 | Protected action (quick icon): 15 | 16 | ![public](img/auth_restricted.png) 17 | 18 | Panel showcase once opened as "guest": 19 | 20 | ![panel](img/panel_guest.png) 21 | 22 | Panel showcase as "logged in user": 23 | 24 | ![panel](img/panel.png) 25 | 26 | ### Enable the panel 27 | Activate the panel in your config: 28 | 29 | ```php 30 | 'DebugKit' => [ 31 | 'panels' => [ 32 | ... 33 | 'TinyAuth.Auth' => true, 34 | ], 35 | ], 36 | ``` 37 | 38 | Now it should be visible in your DebugKit panel list. 39 | 40 | Note: If you only use TinyAuth authentication or authorization (and not both) it will usually detect this and not display the unused part. 41 | Make sure you enabled the documented components and helpers here to have all features enabled. 42 | 43 | ### Notes 44 | The allow/ACL data only works correctly, if you don't use any Controller allow()/deny() injections. 45 | You should first migrate away from those before using this panel. 46 | -------------------------------------------------------------------------------- /docs/Authentication.md: -------------------------------------------------------------------------------- 1 | # TinyAuth Authentication 2 | The fast and easy way for user authentication in CakePHP applications. 3 | 4 | Use TinyAuth Component if you want to add instant (and easy) action whitelisting to your application. 5 | You can allow/deny per controller action or with wildcards also per controller and more. 6 | 7 | ## Basic Features 8 | - INI file (static) based access rights (controller-action setup) 9 | - Lightweight and incredibly fast 10 | 11 | Do NOT use if 12 | - you want to dynamically adjust what pages are public inside controllers 13 | 14 | ## Quick setups 15 | TinyAuth, to live up to its name, offers a few quick setups. 16 | 17 | ### Allow non-prefixed 18 | If you have non-prefixed controllers that you want to make public and keep prefixed ones protected: 19 | ```php 20 | 'allowNonPrefixed' => true, 21 | ``` 22 | Any such action will now be public by default. 23 | 24 | You can always set up "deny" rules for any action to protect a specific one from public access. 25 | 26 | ### Prefix based allow 27 | If you want to allow certain prefixes on top, you can use: 28 | ```php 29 | 'allowPrefixes' => [ 30 | 'MyPrefix', 31 | 'Nested/Prefix', 32 | ], 33 | ``` 34 | 35 | >**Note:** Prefixes are always `CamelCased` (even if routing makes them to `dashed-ones` in the URL). 36 | 37 | Careful: Nested prefixes currently also match (and inherit) by parent. 38 | So if `MyPrefix` is allowed, `MyPrefix/Sub` and other nested ones would also automatically be allowed. 39 | You would need to explicitly set up ACL rules here to deny those if needed. 40 | 41 | At the same time you can always set up "deny" rules for any allowed prefix to revoke the set default. 42 | 43 | ## Enabling 44 | 45 | **DEPRECATED** Use `TinyAuth.Authentication` instead. Rest of the page is accurate. 46 | 47 | Authentication is set up in your controller's `initialize()` method: 48 | 49 | ```php 50 | // src/Controller/AppController 51 | 52 | public function initialize() { 53 | parent::initialize(); 54 | 55 | $this->loadComponent('TinyAuth.Auth'); 56 | } 57 | ``` 58 | 59 | That is basically already all for the code changes :-) Super-easy! 60 | 61 | ## auth_allow.ini 62 | 63 | TinyAuth expects an ``auth_allow.ini`` file in your config directory. 64 | Use it to specify what actions are not protected by authentication. 65 | 66 | The section key syntax follows the CakePHP naming convention: 67 | ``` 68 | PluginName.MyPrefix/MyController 69 | ``` 70 | 71 | Make sure to create an entry for each action you want to expose and use: 72 | 73 | - one or more action names 74 | - the ``*`` wildcard to grant access to all actions of that controller 75 | - use `"!actionName"` (quotes are important then) to deny certain actions 76 | 77 | ```ini 78 | ; ---------------------------------------------------------- 79 | ; UsersController 80 | ; ---------------------------------------------------------- 81 | Users = index 82 | ; ---------------------------------------------------------- 83 | ; UsersController using /api prefixed route 84 | ; ---------------------------------------------------------- 85 | Api/Users = index, view, edit 86 | ; ---------------------------------------------------------- 87 | ; UsersController using /admin prefixed route 88 | ; ---------------------------------------------------------- 89 | Admin/Users = * 90 | ; ---------------------------------------------------------- 91 | ; AccountsController in plugin named Accounts 92 | ; ---------------------------------------------------------- 93 | Accounts.Accounts = view, edit 94 | ; ---------------------------------------------------------- 95 | ; AccountsController in plugin named Accounts using /my-admin 96 | ; prefixed route (assuming you are using recommended DashedRoute class) 97 | ; ---------------------------------------------------------- 98 | Accounts.MyAdmin/Accounts = index 99 | ``` 100 | 101 | >**Note:** Prefixes are always `CamelCased`. The route inflects to the final casing if needed. 102 | Nested prefixes are joined using `/`, e.g. `MyAdmin/Nested`. 103 | 104 | Using only "granting" is recommended for security reasons. 105 | Careful with denying, as this can accidentally open up more than desired actions. If you really want to use it: 106 | 107 | ```ini 108 | Users = "!secret",* 109 | ``` 110 | Meaning: Grant public access to all "Users" controller actions by default, but keep authentication required for "secret" action. 111 | 112 | Note that denying always trumps granting, if both are declared for an action. 113 | 114 | ### Multiple files and merging 115 | You can specify multiple paths in your config, e.g. when you have plugins and separated the definitions across them. 116 | Make sure you are using each key only once, though. The first definition will be kept and all others for the same key are ignored. 117 | 118 | ### Template with defaults 119 | See the `config/` folder and the default template for popular plugins. 120 | You can copy out any default rules you want to use in your project. 121 | 122 | ## Mixing with code 123 | It is possible to have mixed INI and code rules. Those will get merged prior to authentication. 124 | So in case any of your controllers (or plugin controllers) contain such a statement, it will merge itself with your INI whitelist: 125 | ```php 126 | // In your controller 127 | use Cake\Event\EventInterface; 128 | ... 129 | 130 | public function beforeFilter(EventInterface $event): void { 131 | parent::beforeFilter($event); 132 | 133 | $this->Auth->allow(['index', 'view']); 134 | } 135 | ``` 136 | This can be interested when migrating slowly to TinyAuth, for example. 137 | Once you move such a code based rule into the INI file, you can safely remove those lines of code in your controller :) 138 | 139 | ### allow() vs deny() 140 | 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(). 141 | As such TinyAuth injects its list now before `Controller::beforeFilter()` gets called. 142 | 143 | Note: It is also advised to move away from these controller calls. 144 | 145 | ## Caching 146 | 147 | TinyAuth makes heavy use of caching to achieve optimal performance. 148 | By default, it will not use caching in debug mode, though. 149 | 150 | To modify the caching behavior set the ``autoClearCache`` configuration option: 151 | ```php 152 | $this->loadComponent('TinyAuth.Auth', [ 153 | 'autoClearCache' => true|false 154 | )] 155 | ``` 156 | 157 | ## Multi Column authentication 158 | Often times you want to allow logging in with either username or email. 159 | In this case you can use the built-in adapter for it: 160 | 161 | ```php 162 | 'authenticate' => [ 163 | 'TinyAuth.MultiColumn' => [ 164 | 'fields' => [ 165 | 'username' => 'login', 166 | 'password' => 'password', 167 | ], 168 | 'columns' => ['username', 'email'], 169 | 'userModel' => 'Users', 170 | ], 171 | ], 172 | ``` 173 | which you can pass in to Auth component as options. 174 | 175 | Your form would then contain e.g. 176 | ```php 177 | echo $this->Form->control('login', ['label' => 'Your username or email']); 178 | echo $this->Form->control('password', ['autocomplete' => 'off']); 179 | ``` 180 | 181 | ## Configuration 182 | 183 | TinyAuth AuthComponent supports the following configuration options. 184 | 185 | Option | Type | Description 186 | :----- | :--- | :---------- 187 | autoClearCache|bool|True will generate a new ACL cache file every time. 188 | allowFilePath|string|Full path to the INI file. Can also be an array of paths. Defaults to `ROOT . DS . 'config' . DS`. 189 | allowFile|string|Name of the INI file. Defaults to `auth_allow.ini`. 190 | allowAdapter|string|Class name, defaults to `IniAllowAdapter::class`. 191 | -------------------------------------------------------------------------------- /docs/AuthenticationAdapter.md: -------------------------------------------------------------------------------- 1 | ### Authentication Adapters 2 | For adapters to define allow/deny (public/protected) per controller action. 3 | 4 | Implement the AllowAdapterInterface and make sure your `getAllow()` method returns an array like so: 5 | ```php 6 | // normal controller 7 | 'Users' => [ 8 | 'plugin' => null, 9 | 'prefix' => null, 10 | 'controller' => 'Users', 11 | 'deny' => [], 12 | 'allow' => [ 13 | 'index', 14 | 'view', 15 | ] 16 | ], 17 | // or with admin prefix 18 | 'admin/Users' => [ 19 | 'plugin' => null, 20 | 'prefix' => 'Admin', 21 | 'controller' => 'Users', 22 | 'deny' => [], 23 | 'allow' => [ 24 | 'index', 25 | ], 26 | ], 27 | // plugin controller 28 | 'Extras.Offers' => [ 29 | 'plugin' => 'Extras', 30 | 'prefix' => null, 31 | 'controller' => 'Offers', 32 | 'deny' => [ 33 | 'superPrivate', 34 | ], 35 | 'allow' => [ 36 | '*', 37 | ], 38 | ], 39 | ... 40 | ``` 41 | 42 | Unique array keys due to the internal `PluginName.Prefix/ControllerName` syntax. 43 | URL elements and then an array of actions mapped to their role id(s). 44 | The `*` action key means "any" action. 45 | 46 | With this you can easily built your own database adapter and manage your ACL via backend. 47 | Make sure you bust the cache with each update/change, though. 48 | 49 | Note: Some adapters may contain `map` data which is for debugging only (returns the original data). 50 | -------------------------------------------------------------------------------- /docs/AuthenticationPlugin.md: -------------------------------------------------------------------------------- 1 | ### Authentication plugin support 2 | 3 | Support for [Authentication plugin](https://github.com/cakephp/authentication) usage. 4 | 5 | Instead of the core Auth component you load the Authentication component: 6 | 7 | ```php 8 | $this->loadComponent('TinyAuth.Authentication', [ 9 | ... 10 | ]); 11 | ``` 12 | 13 | Make sure you load the middleware in your `Application` class: 14 | ```php 15 | use Authentication\Middleware\AuthenticationMiddleware; 16 | 17 | // in Application::middleware() 18 | $middlewareQueue->add(new AuthenticationMiddleware($this)); 19 | ``` 20 | 21 | This plugin ships with an improved session authenticator: 22 | 23 | - PrimaryKeySession authenticator (extending the Authentication.Session one): 24 | stores only the ID and fetches the rest from DB (keeping it always up to date) 25 | 26 | It also ships with an enhanced redirect handler: 27 | 28 | - ForbiddenCakeRedirect: Allows an `unauthorizedMessage` to be set as error flash message. 29 | 30 | 31 | Now let's set up `getAuthenticationService()` and make sure to load all needed Authenticators, e.g.: 32 | 33 | ```php 34 | /** 35 | * @param \Psr\Http\Message\ServerRequestInterface $request Request 36 | * 37 | * @return \Authentication\AuthenticationServiceInterface 38 | */ 39 | public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface 40 | { 41 | $service = new AuthenticationService(); 42 | 43 | // Define where users should be redirected to when they are not authenticated 44 | $service->setConfig([ 45 | 'unauthenticatedRedirect' => Router::url([ 46 | 'prefix' => false, 47 | 'plugin' => false, 48 | 'controller' => 'Account', 49 | 'action' => 'login', 50 | ]), 51 | 'queryParam' => 'redirect', 52 | ]); 53 | 54 | $fields = [ 55 | AbstractIdentifier::CREDENTIAL_USERNAME => 'email', 56 | AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', 57 | ]; 58 | 59 | // Load identifiers 60 | $service->loadIdentifier('Authentication.Password', [ 61 | 'fields' => $fields, 62 | 'resolver' => [ 63 | 'className' => 'Authentication.Orm', 64 | 'finder' => 'active', 65 | ], 66 | ]); 67 | $service->loadIdentifier('Authentication.Token', [ 68 | 'tokenField' => 'id', 69 | 'dataField' => 'key', 70 | 'resolver' => [ 71 | 'className' => 'Authentication.Orm', 72 | 'finder' => 'active', 73 | ], 74 | ]); 75 | 76 | // Load the authenticators. Session should be first. 77 | $service->loadAuthenticator('TinyAuth.PrimaryKeySession', [ 78 | 'urlChecker' => 'Authentication.CakeRouter', 79 | ]); 80 | $service->loadAuthenticator('Authentication.Form', [ 81 | 'urlChecker' => 'Authentication.CakeRouter', 82 | 'fields' => $fields, 83 | 'loginUrl' => [ 84 | 'prefix' => false, 85 | 'plugin' => false, 86 | 'controller' => 'Account', 87 | 'action' => 'login', 88 | ], 89 | ]); 90 | $service->loadAuthenticator('Authentication.Cookie', [ 91 | 'urlChecker' => 'Authentication.CakeRouter', 92 | 'rememberMeField' => 'remember_me', 93 | 'fields' => [ 94 | 'username' => 'email', 95 | 'password' => 'password', 96 | ], 97 | 'loginUrl' => [ 98 | 'prefix' => false, 99 | 'plugin' => false, 100 | 'controller' => 'Account', 101 | 'action' => 'login', 102 | ], 103 | ]); 104 | 105 | // This is a one click token login as optional addition 106 | $service->loadIdentifier('Tools.LoginLink', [ 107 | 'resolver' => [ 108 | 'className' => 'Authentication.Orm', 109 | 'finder' => 'active', 110 | ], 111 | 'preCallback' => function (int $id) { 112 | TableRegistry::getTableLocator()->get('Users')->confirmEmail($id); 113 | }, 114 | ]); 115 | $service->loadAuthenticator('Tools.LoginLink', [ 116 | 'urlChecker' => 'Authentication.CakeRouter', 117 | 'loginUrl' => [ 118 | 'prefix' => false, 119 | 'plugin' => false, 120 | 'controller' => 'Account', 121 | 'action' => 'login', 122 | ], 123 | ]); 124 | 125 | return $service; 126 | } 127 | ``` 128 | 129 | 130 | You can always get the identity result (User entity) from the AuthUser component and helper: 131 | ```php 132 | $this->AuthUser->identity(); 133 | ``` 134 | 135 | 136 | ### Caching 137 | Especially when you use the PrimaryKeySession authenticator and always pulling the live data 138 | from DB, you might want to consider adding a short-lived cache in between. 139 | The authenticator supports this directly: 140 | 141 | In this case you need to manually invalidate the session cache every time a user modifies some of their 142 | data that is part of the session (e.g. username, email, roles, birthday, ...). 143 | For that you can use the following after the change was successful: 144 | ```php 145 | use TinyAuth\Utility\SessionCache; 146 | 147 | SessionCache::delete($userId); 148 | ``` 149 | This will force the session to be pulled (the ID), and the cache refilled with up-to-date data. 150 | 151 | 152 | --- 153 | 154 | 155 | For all the rest, follow the plugin's documentation. 156 | 157 | Then you use the [Authentication documentation](Authentication.md) to fill your INI config file. 158 | -------------------------------------------------------------------------------- /docs/AuthorizationAdapter.md: -------------------------------------------------------------------------------- 1 | ### Authorization Adapters 2 | For RBAC ACL adapters. 3 | 4 | Implement the AclAdapterInterface and make sure your `getAcl()` method returns an array like so: 5 | ```php 6 | // normal controller 7 | 'Posts' => [ 8 | 'plugin' => null, 9 | 'prefix' => null, 10 | 'controller' => 'Posts', 11 | 'allow' => [ 12 | // action to role id mapping 13 | ], 14 | 'deny => [ 15 | // action to role id mapping 16 | ], 17 | ], 18 | // or plugin with admin prefix 19 | 'Queue.admin/QueuedJobs' => [ 20 | 'plugin' => 'Queue', 21 | 'prefix' => 'Admin', 22 | 'controller' => 'QueuedJobs', 23 | 'allow' => [ 24 | 'index' => [ 25 | 'user' => 1, 26 | ], 27 | 'view' => [ 28 | 'user' => 1, 29 | ], 30 | '*' => [ 31 | 'admin' => 3, 32 | ], 33 | ], 34 | ], 35 | ... 36 | ``` 37 | 38 | Unique array keys due to the internal `PluginName.Prefix/ControllerName` syntax. 39 | URL elements and then an array of actions mapped to their role id(s). 40 | The `*` action key means 'any'. 41 | 42 | With this you can easily built your own database adapter and manage your ACL via backend. 43 | Make sure you bust the cache with each update/change, though. 44 | 45 | Note: Some adapters may contain `map` data which is for debugging only (returns the original data). 46 | -------------------------------------------------------------------------------- /docs/AuthorizationPlugin.md: -------------------------------------------------------------------------------- 1 | ### Authorization plugin support 2 | 3 | Support for [Authorization plugin](https://github.com/cakephp/authorization) usage. 4 | 5 | Instead of the core Auth component you load the Authorization component: 6 | 7 | ```php 8 | $this->loadComponent('TinyAuth.Authorization', [ 9 | ... 10 | ]); 11 | ``` 12 | 13 | And in your `Application` class you need to load both `Authorization` and TinyAuth specific 14 | `RequestAuthorization` middlewares in that order: 15 | 16 | ```php 17 | use Authorization\Middleware\AuthorizationMiddleware; 18 | use TinyAuth\Middleware\RequestAuthorizationMiddleware; 19 | 20 | // in Application::middleware() 21 | $middlewareQueue->add(new AuthorizationMiddleware($this, [ 22 | 'unauthorizedHandler' => [ 23 | 'className' => 'Authorization.CakeRedirect', 24 | 'url' => [ 25 | ... 26 | ], 27 | ], 28 | ])); 29 | $middlewareQueue->add(new RequestAuthorizationMiddleware([ 30 | 'unauthorizedHandler' => [ 31 | 'className' => 'TinyAuth.ForbiddenCakeRedirect', 32 | 'url' => [ 33 | ... 34 | ], 35 | 'unauthorizedMessage' => '...', 36 | ], 37 | ]))); 38 | ``` 39 | 40 | For all the rest just follow the plugin's documentation. 41 | 42 | For your resolver you need to use this map inside `Application::getAuthorizationService()`: 43 | ```php 44 | use TinyAuth\Policy\RequestPolicy; 45 | 46 | /** 47 | * @param \Psr\Http\Message\ServerRequestInterface $request 48 | * @param \Psr\Http\Message\ResponseInterface $response 49 | * 50 | * @return \Authorization\AuthorizationServiceInterface 51 | */ 52 | public function getAuthorizationService(ServerRequestInterface $request, ResponseInterface $response) { 53 | $map = [ 54 | ServerRequest::class => new RequestPolicy(), 55 | ]; 56 | $resolver = new MapResolver($map); 57 | 58 | return new AuthorizationService($resolver); 59 | } 60 | ``` 61 | 62 | Then you use the [Authorization documentation](Authorization.md) to set up roles and fill your INI config file. 63 | 64 | #### Tips 65 | 66 | You can add loginUpdate() method to your UsersTable to update the user's data here accordingly: 67 | 68 | ```php 69 | /** 70 | * @param \Authentication\Authenticator\ResultInterface $result 71 | * 72 | * @return void 73 | */ 74 | public function loginUpdate(ResultInterface $result): void 75 | { 76 | /** @var \App\Model\Entity\User $user */ 77 | $user = $result->getData(); 78 | $this->updateAll(['last_login' => new DateTime()], ['id' => $user->id]); 79 | } 80 | ``` 81 | Then hook it in: 82 | ```php 83 | // Inside your AccountController::login() method 84 | $result = $this->Authentication->getResult(); 85 | // If the user is logged in send them away. 86 | if ($result && $result->isValid()) { 87 | $this->Users->loginUpdate($result); 88 | $target = $this->Authentication->getLoginRedirect() ?? '/'; 89 | $this->Flash->success(__('You are now logged in.')); 90 | 91 | return $this->redirect($target); 92 | } 93 | ``` 94 | 95 | #### Controller specific Authorization 96 | 97 | In some cases with default fallback routing in place, it can make more sense to have the authorization part more coupled to your controllers (extending AppController). 98 | In that case only keep Authentication/Authorization middlewares in Application, and move RequestAuthorizationMiddleware to `AppController::initialize()`: 99 | 100 | ```php 101 | $this->middleware(function (ServerRequest $request, $handler): ResponseInterface { 102 | $config = [ 103 | 'unauthorizedHandler' => [ 104 | 'className' => 'TinyAuth.ForbiddenCakeRedirect', 105 | 'url' => [ 106 | 'prefix' => false, 107 | 'plugin' => false, 108 | 'controller' => 'Account', 109 | 'action' => 'login', 110 | ], 111 | ], 112 | ]; 113 | $middleware = new RequestAuthorizationMiddleware($config); 114 | $result = $middleware->process($request, $handler); 115 | 116 | return $result; 117 | }); 118 | ``` 119 | In case there are redirect loops, you might have to wrap the whole thing in `if ($this->AuthUser->id()) {}`. 120 | Since authentication needs to trigger first anway, the methods are protected. Only once there is a valid user, the authorization makes sense anyway. 121 | 122 | -------------------------------------------------------------------------------- /docs/Multi-role.md: -------------------------------------------------------------------------------- 1 | ## Configuration Multi-role 2 | 3 | ```php 4 | // in your app.php 5 | 'TinyAuth' => [ 6 | 'multiRole' => true, 7 | ] 8 | ``` 9 | 10 | ```php 11 | // in your AppController.php 12 | $this->loadComponent('TinyAuth.Auth', [ 13 | 'autoClearCache' => true, 14 | 'authorize' => ['TinyAuth.Tiny'], 15 | ... 16 | ]); 17 | ``` 18 | 19 | ### auth_allow.ini 20 | ```ini 21 | // in config folder 22 | ; ---------------------------------------------------------- 23 | ; PagesController 24 | ; ---------------------------------------------------------- 25 | Pages = display 26 | ; ---------------------------------------------------------- 27 | ; UsersController 28 | ; ---------------------------------------------------------- 29 | Users = login 30 | ``` 31 | 32 | ### auth_acl.ini 33 | ```ini 34 | // in config folder 35 | ; ---------------------------------------------------------- 36 | ; RolesController 37 | ; ---------------------------------------------------------- 38 | [Roles] 39 | * = admin 40 | ; ---------------------------------------------------------- 41 | ; UsersController 42 | ; ---------------------------------------------------------- 43 | [Users] 44 | edit, index, logout = author 45 | * = admin 46 | ; ---------------------------------------------------------- 47 | ; ArticlesController 48 | ; ---------------------------------------------------------- 49 | [Articles] 50 | * = author, admin 51 | ; ---------------------------------------------------------- 52 | ; CategoriesController 53 | ; ---------------------------------------------------------- 54 | [Categories] 55 | * = author, admin 56 | ; ---------------------------------------------------------- 57 | ; TagsController 58 | ; ---------------------------------------------------------- 59 | [Tags] 60 | * = author, admin 61 | ; ---------------------------------------------------------- 62 | ; ImagesController 63 | ; ---------------------------------------------------------- 64 | [Images] 65 | * = author, admin 66 | ; ---------------------------------------------------------- 67 | ; MenusController 68 | ; ---------------------------------------------------------- 69 | [Menus] 70 | * = admin 71 | ; ---------------------------------------------------------- 72 | ; SettingsController 73 | ; ---------------------------------------------------------- 74 | [Settings] 75 | * = admin 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # TinyAuth Authentication and Authorization 2 | 3 | This plugin ships with both 4 | - Authentication: Always comes first - "who is it"? 5 | - Authorization: Afterwards - "What can this person see"? 6 | 7 | For the first one usually declares as a whitelist of actions per controller that will not require any authentication. 8 | If an action is not whitelisted, it will trigger the login process. 9 | 10 | The second only gets invoked once the person is already logged in. 11 | In that case the role of this logged in user decides if that action is allowed to be accessed. 12 | 13 | ## DebugKit Panel 14 | You can activate an "Auth" DebugKit panel to have useful insights per URL. 15 | 16 | See [AuthPanel](AuthPanel.md) docs. 17 | 18 | ## Authentication 19 | This is done via TinyAuth AuthComponent. 20 | 21 | The component plays well together with the authorization part (see below). 22 | 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. 23 | 24 | See [Authentication](Authentication.md) docs. 25 | 26 | ## Authorization 27 | The TinyAuthorize adapter takes care of authorization. 28 | 29 | The adapter plays well together with the component above. 30 | But if you prefer to control the action whitelisting for authentication via code and `$this->Auth->allow()` calls, you can 31 | also just use this adapter stand-alone for the ACL part of your application. 32 | 33 | There is also an AuthUserComponent and AuthUserHelper to assist you in making role based decisions or displaying role based links in your templates. 34 | 35 | See [Authorization](Authorization.md) docs. 36 | 37 | 38 | ## Configuration 39 | Those classes most likely share quite a few configs, in that case you definitely should use Configure to define those centrally: 40 | ```php 41 | // in your app.php 42 | 'TinyAuth' => [ 43 | 'multiRole' => true, 44 | ... 45 | ], 46 | ``` 47 | This way you keep it DRY. 48 | 49 | ## Cache busting 50 | In general, it is advised to clear the cache after each deploy, e.g. using 51 | ``` 52 | bin/cake clear cache 53 | ``` 54 | In debug mode this happens automatically for each request. 55 | 56 | By default, the cache engine used is `_cake_core_`, the prefix is `tiny_auth_`. 57 | You can also clear the cache from code using `TinyAuth\Utility\Cache::clear()` method for specifically this. 58 | 59 | ## Custom Allow or ACL adapters 60 | You can easily switch out the INI file adapters for both using `allowAdapter` and `aclAdapter` config. 61 | This way you can also read from DB or provide any other API driven backend to read the data from for your authentication or authorization. 62 | 63 | Current customizations: 64 | - [TinyAuthBackend plugin](https://github.com/dereuromark/cakephp-tinyauth-backend) as backend GUI for "allow" and "ACL". 65 | 66 | ## Troubleshooting 67 | First of all: Isolate the issue. Never mix **authentication** and **authorization** (read the top part again). 68 | 69 | If you want to use both, first attach authentication and make sure you can log in and you can log out. By default all actions are now protected unless you make them "public". So make sure the non-public actions are not accessible without being logged in and they are afterwards. 70 | You just verified: authentication is working now fine - it doesn't matter who logged in as long as someone did. 71 | 72 | Only if that is working, attach an Auth adapter (which now means authorization comes into play), in this case probably `Tiny`. 73 | By default it will now deny all logged in users any access to any protected action. Only by specifically whitelisting actions/controllers now in the ACL definition, a specific user can access a specific action again. 74 | Make sure that the session contains the correct data structure, also make sure the roles are configured or in the database and can be found as expected. The user with the right role should get access now to the corresponding action (make also sure cache is cleared). 75 | You then verified: authorization is working fine, as well - only with the correct role a user can now access protected actions. 76 | 77 | ## Working with new plugins 78 | If you are using [Authentication](https://github.com/cakephp/authentication) or [Authorization](https://github.com/cakephp/authorization) plugin, you will need to use the 79 | Authentication/Authorization components of this plugin instead for them to work with TinyAuth. 80 | 81 | See the docs for details: 82 | - [TinyAuth and Authentication plugin](AuthenticationPlugin.md) 83 | - [TinyAuth and Authorization plugin](AuthorizationPlugin.md) 84 | 85 | ### When to use the new plugins? 86 | They are super powerful, but they also require a load of config to get them to run. 87 | If you need authentication/authorization on middleware/routing level however, you need 88 | to use them. 89 | 90 | If you only need the basic request policy provided by this plugin, and no further ORM or other policies, 91 | then it is best to stick to the Auth component as simple wrapper. 92 | It is then limited to controller scope (no middleware/routing support) as it always has been so far. 93 | 94 | You can seamlessly upgrade to the new plugins while keeping your INI files. 95 | They are also compatible with AuthUser component and helper as well as the Auth panel. 96 | 97 | ## Upgrade notes 98 | Coming from CakePHP 4.x the following major changes will affect your app: 99 | - Cake\Auth namespace has been removed and is now migrated to TinyAuth\Auth, that includes the 100 | Authentication and Authorization classes, hashers and alike. 101 | 102 | ## Contributing 103 | Feel free to fork and pull request. 104 | 105 | There are a few guidelines: 106 | 107 | - Coding standards passing: `composer cs-check` to check and `composer cs-fix` to fix. 108 | - Tests passing: `composer test` to run them. 109 | -------------------------------------------------------------------------------- /docs/img/auth_public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dereuromark/cakephp-tinyauth/1518a823c22339b268d3af5f09d74f310d4faf46/docs/img/auth_public.png -------------------------------------------------------------------------------- /docs/img/auth_restricted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dereuromark/cakephp-tinyauth/1518a823c22339b268d3af5f09d74f310d4faf46/docs/img/auth_restricted.png -------------------------------------------------------------------------------- /docs/img/panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dereuromark/cakephp-tinyauth/1518a823c22339b268d3af5f09d74f310d4faf46/docs/img/panel.png -------------------------------------------------------------------------------- /docs/img/panel_guest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dereuromark/cakephp-tinyauth/1518a823c22339b268d3af5f09d74f310d4faf46/docs/img/panel_guest.png -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | config/ 7 | src/ 8 | tests/ 9 | /tests/test_files/ 10 | /config/Migrations/ 11 | 12 | 13 | */config/Migrations/* 14 | 15 | 16 | */config/Migrations/* 17 | 18 | 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src/ 5 | bootstrapFiles: 6 | - tests/bootstrap.php 7 | ignoreErrors: 8 | - identifier: missingType.iterableValue 9 | - identifier: missingType.generics 10 | - '#Constructor of class .+SessionStorage has an unused parameter \$response#' 11 | - '#PHPDoc tag @mixin contains invalid type .+InstanceConfigTrait.#' 12 | -------------------------------------------------------------------------------- /src/Auth/AbstractPasswordHasher.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected array $_defaultConfig = []; 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @param array $config Array of config. 42 | */ 43 | public function __construct(array $config = []) { 44 | $this->setConfig($config); 45 | } 46 | 47 | /** 48 | * Generates password hash. 49 | * 50 | * @param string $password Plain text password to hash. 51 | * @return string Either the password hash string or false 52 | */ 53 | abstract public function hash(string $password): string; 54 | 55 | /** 56 | * Check hash. Generate hash from user provided password string or data array 57 | * and check against existing hash. 58 | * 59 | * @param string $password Plain text password to hash. 60 | * @param string $hashedPassword Existing hashed password. 61 | * @return bool True if hashes match else false. 62 | */ 63 | abstract public function check(string $password, string $hashedPassword): bool; 64 | 65 | /** 66 | * Returns true if the password need to be rehashed, due to the password being 67 | * created with anything else than the passwords generated by this class. 68 | * 69 | * Returns true by default since the only implementation users should rely 70 | * on is the one provided by default in php 5.5+ or any compatible library 71 | * 72 | * @param string $password The password to verify 73 | * @return bool 74 | */ 75 | public function needsRehash(string $password): bool { 76 | return password_needs_rehash($password, PASSWORD_DEFAULT); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Auth/AclAdapter/AclAdapterInterface.php: -------------------------------------------------------------------------------- 1 | $config Current TinyAuth configuration values. 12 | * 13 | * @return array 14 | */ 15 | public function getAcl(array $availableRoles, array $config): array; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Auth/AclAdapter/IniAclAdapter.php: -------------------------------------------------------------------------------- 1 | $array) { 20 | $acl[$key] = Utility::deconstructIniKey($key); 21 | if (Configure::read('debug')) { 22 | $acl[$key]['map'] = $array; 23 | } 24 | $acl[$key]['deny'] = []; 25 | $acl[$key]['allow'] = []; 26 | 27 | foreach ($array as $actions => $roles) { 28 | // Get all roles used in the current INI section 29 | $roles = explode(',', $roles); 30 | $actions = explode(',', $actions); 31 | 32 | $deniedRoles = []; 33 | foreach ($roles as $roleId => $role) { 34 | $role = trim($role); 35 | if (!$role) { 36 | continue; 37 | } 38 | $denied = mb_substr($role, 0, 1) === '!'; 39 | if ($denied) { 40 | $role = mb_substr($role, 1); 41 | if (!array_key_exists($role, $availableRoles)) { 42 | unset($roles[$roleId]); 43 | 44 | continue; 45 | } 46 | 47 | unset($roles[$roleId]); 48 | $deniedRoles[] = $role; 49 | 50 | continue; 51 | } 52 | 53 | // Prevent undefined roles appearing in the iniMap 54 | if (!array_key_exists($role, $availableRoles) && $role !== '*') { 55 | unset($roles[$roleId]); 56 | 57 | continue; 58 | } 59 | if ($role === '*') { 60 | unset($roles[$roleId]); 61 | $roles = array_merge($roles, array_keys($availableRoles)); 62 | } 63 | } 64 | 65 | foreach ($actions as $action) { 66 | $action = trim($action); 67 | if (!$action) { 68 | continue; 69 | } 70 | 71 | foreach ($roles as $role) { 72 | $role = trim($role); 73 | if (!$role) { 74 | continue; 75 | } 76 | $roleName = strtolower($role); 77 | 78 | // Lookup role id by name in roles array 79 | $newRole = $availableRoles[$roleName]; 80 | $acl[$key]['allow'][$action][$roleName] = $newRole; 81 | } 82 | foreach ($deniedRoles as $role) { 83 | $role = trim($role); 84 | if (!$role) { 85 | continue; 86 | } 87 | $roleName = strtolower($role); 88 | 89 | // Lookup role id by name in roles array 90 | $newRole = $availableRoles[$roleName]; 91 | $acl[$key]['deny'][$action][$roleName] = $newRole; 92 | } 93 | } 94 | } 95 | } 96 | 97 | return $acl; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/Auth/AclTrait.php: -------------------------------------------------------------------------------- 1 | |null 29 | */ 30 | protected $_roles; 31 | 32 | /** 33 | * @var array|null 34 | */ 35 | protected $_prefixMap; 36 | 37 | /** 38 | * @var array|null 39 | */ 40 | protected $_userRoles; 41 | 42 | /** 43 | * @var \TinyAuth\Auth\AclAdapter\AclAdapterInterface|null 44 | */ 45 | protected $_aclAdapter; 46 | 47 | /** 48 | * @var array|null 49 | */ 50 | protected $auth; 51 | 52 | /** 53 | * Finds the authorization adapter to use for this request. 54 | * 55 | * @param string $adapter Acl adapter to load. 56 | * @throws \Cake\Core\Exception\CakeException 57 | * @throws \InvalidArgumentException 58 | * @return \TinyAuth\Auth\AclAdapter\AclAdapterInterface 59 | */ 60 | protected function _loadAclAdapter($adapter) { 61 | if ($this->_aclAdapter !== null) { 62 | return $this->_aclAdapter; 63 | } 64 | 65 | if (!class_exists($adapter)) { 66 | throw new CakeException(sprintf('The Acl Adapter class "%s" was not found.', $adapter)); 67 | } 68 | 69 | $adapterInstance = new $adapter(); 70 | if (!($adapterInstance instanceof AclAdapterInterface)) { 71 | throw new InvalidArgumentException(sprintf( 72 | 'TinyAuth Acl adapters have to implement %s.', 73 | AclAdapterInterface::class, 74 | )); 75 | } 76 | 77 | $this->_aclAdapter = $adapterInstance; 78 | 79 | return $adapterInstance; 80 | } 81 | 82 | /** 83 | * Checks the URL to the role(s). 84 | * 85 | * Allows single or multi role based authorization 86 | * 87 | * @param \ArrayAccess|array $user User data 88 | * @param array $params Request params 89 | * @throws \Cake\Core\Exception\CakeException 90 | * @return bool Success 91 | */ 92 | protected function _checkUser(ArrayAccess|array $user, array $params): bool { 93 | if ($this->getConfig('includeAuthentication') && $this->_isPublic($params)) { 94 | return true; 95 | } 96 | 97 | if (!$user) { 98 | return false; 99 | } 100 | 101 | if ($this->getConfig('superAdmin')) { 102 | $superAdminColumn = $this->getConfig('superAdminColumn'); 103 | if (!$superAdminColumn) { 104 | $superAdminColumn = $this->getConfig('idColumn'); 105 | } 106 | if (!isset($user[$superAdminColumn])) { 107 | throw new CakeException('Missing super admin column `' . $superAdminColumn . '` in user table'); 108 | } 109 | if ($user[$superAdminColumn] === $this->getConfig('superAdmin')) { 110 | return true; 111 | } 112 | } 113 | 114 | // Give any logged-in user access to ALL actions when `allowLoggedIn` is 115 | // enabled except when the `protectedPrefix` is being used. 116 | if ($this->getConfig('allowLoggedIn')) { 117 | if (empty($params['prefix'])) { 118 | return true; 119 | } 120 | $protectedPrefixes = (array)$this->getConfig('protectedPrefix'); 121 | if (!$this->_isProtectedPrefix($params['prefix'], $protectedPrefixes)) { 122 | return true; 123 | } 124 | } 125 | 126 | $userRoles = $this->_getUserRoles($user); 127 | 128 | return $this->_check($userRoles, $params); 129 | } 130 | 131 | /** 132 | * @param string $prefix 133 | * @param array $protectedPrefixes 134 | * 135 | * @return bool 136 | */ 137 | protected function _isProtectedPrefix($prefix, array $protectedPrefixes) { 138 | foreach ($protectedPrefixes as $protectedPrefix) { 139 | if ($prefix === $protectedPrefix || strpos($prefix, $protectedPrefix . '/') === 0) { 140 | return true; 141 | } 142 | } 143 | 144 | return false; 145 | } 146 | 147 | /** 148 | * @param array $userRoles 149 | * @param array $params 150 | * 151 | * @return bool 152 | */ 153 | protected function _check(array $userRoles, array $params) { 154 | // Allow access to all prefixed actions for users belonging to 155 | // the specified role that matches the prefix. 156 | $prefixMap = $this->getConfig('authorizeByPrefix'); 157 | if (!empty($params['prefix']) && $prefixMap) { 158 | $roles = $this->_getAvailableRoles(); 159 | $prefixMap = $this->_prefixMap($roles); 160 | if ($prefixMap && $this->_isAuthorizedByPrefix($params['prefix'], $prefixMap, $userRoles, $roles)) { 161 | return true; 162 | } 163 | } 164 | 165 | // Allow logged in super admins access to all resources 166 | if ($this->getConfig('superAdminRole')) { 167 | foreach ($userRoles as $userRole) { 168 | if ($userRole === $this->getConfig('superAdminRole')) { 169 | return true; 170 | } 171 | } 172 | } 173 | 174 | if ($this->_acl === null) { 175 | $this->_acl = $this->_getAcl($this->getConfig('aclFilePath')); 176 | } 177 | 178 | $iniKey = $this->_constructIniKey($params); 179 | if (empty($this->_acl[$iniKey])) { 180 | return false; 181 | } 182 | 183 | $action = $params['action']; 184 | if (!empty($this->_acl[$iniKey]['deny'][$action])) { 185 | $matchArray = $this->_acl[$iniKey]['deny'][$action]; 186 | foreach ($userRoles as $userRole) { 187 | if (in_array($userRole, $matchArray, true)) { 188 | return false; 189 | } 190 | } 191 | } 192 | 193 | // Allow access if user has a role with wildcard access to the resource 194 | if (isset($this->_acl[$iniKey]['allow']['*'])) { 195 | $matchArray = $this->_acl[$iniKey]['allow']['*']; 196 | foreach ($userRoles as $userRole) { 197 | if (in_array($userRole, $matchArray, true)) { 198 | return true; 199 | } 200 | } 201 | } 202 | 203 | // Allow access if user has been granted access to the specific resource 204 | if (array_key_exists($action, $this->_acl[$iniKey]['allow']) && !empty($this->_acl[$iniKey]['allow'][$action])) { 205 | $matchArray = $this->_acl[$iniKey]['allow'][$action]; 206 | foreach ($userRoles as $userRole) { 207 | if (in_array($userRole, $matchArray, true)) { 208 | return true; 209 | } 210 | } 211 | } 212 | 213 | return false; 214 | } 215 | 216 | /** 217 | * @param array $roles 218 | * @return array 219 | */ 220 | protected function _prefixMap(array $roles): array { 221 | if ($this->_prefixMap !== null) { 222 | return $this->_prefixMap; 223 | } 224 | 225 | /** @var array|bool $prefixMap */ 226 | $prefixMap = $this->getConfig('authorizeByPrefix'); 227 | if (!$prefixMap) { 228 | return []; 229 | } 230 | 231 | if ($prefixMap === true) { 232 | $prefixMap = $this->_prefixesFromRoles($roles); 233 | } else { 234 | $prefixMap = $this->_normalizePrefixes($prefixMap); 235 | } 236 | 237 | $this->_prefixMap = $prefixMap; 238 | 239 | return $this->_prefixMap; 240 | } 241 | 242 | /** 243 | * Gets the [PrefixName => roleName] pairs from existing roles. 244 | * 245 | * @param array $roles 246 | * 247 | * @return array 248 | */ 249 | protected function _prefixesFromRoles(array $roles) { 250 | $names = array_keys($roles); 251 | $prefixMap = []; 252 | foreach ($names as $name) { 253 | $prefix = Inflector::camelize(Inflector::underscore($name)); 254 | $prefixMap[$prefix] = $name; 255 | } 256 | 257 | return $prefixMap; 258 | } 259 | 260 | /** 261 | * @param string $prefix 262 | * @param array $prefixMap 263 | * @param array $userRoles 264 | * @param array $availableRoles 265 | * 266 | * @return bool 267 | */ 268 | protected function _isAuthorizedByPrefix($prefix, array $prefixMap, array $userRoles, array $availableRoles) { 269 | if (!$userRoles || !$prefixMap || !$availableRoles) { 270 | return false; 271 | } 272 | 273 | if (empty($prefixMap[$prefix])) { 274 | return false; 275 | } 276 | 277 | $prefixRoleSlugs = (array)$prefixMap[$prefix]; 278 | foreach ($prefixRoleSlugs as $prefixRoleSlug) { 279 | $role = $availableRoles[$prefixRoleSlug] ?? null; 280 | if ($role && in_array($role, $userRoles, true)) { 281 | return true; 282 | } 283 | } 284 | 285 | return false; 286 | } 287 | 288 | /** 289 | * Can be [PrefixOne, PrefixTwo => roleOne, PrefixTwo => [roleOne, roleTwo]] 290 | * 291 | * @param array $prefixes 292 | * 293 | * @return array 294 | */ 295 | protected function _normalizePrefixes(array $prefixes) { 296 | $normalized = []; 297 | foreach ($prefixes as $prefix => $role) { 298 | if (is_int($prefix)) { 299 | $prefix = $role; 300 | $role = Inflector::dasherize(Inflector::underscore($role)); 301 | } 302 | 303 | $normalized[$prefix] = $role; 304 | } 305 | 306 | return $normalized; 307 | } 308 | 309 | /** 310 | * @param array $params 311 | * 312 | * @return bool 313 | */ 314 | protected function _isPublic(array $params) { 315 | $authentication = $this->_getAuth(); 316 | 317 | foreach ($authentication as $rule) { 318 | if ($params['plugin'] && $params['plugin'] !== $rule['plugin']) { 319 | continue; 320 | } 321 | if (!empty($params['prefix']) && $params['prefix'] !== $rule['prefix']) { 322 | continue; 323 | } 324 | if ($params['controller'] !== $rule['controller']) { 325 | continue; 326 | } 327 | 328 | $action = $params['action']; 329 | 330 | if (!empty($rule['deny']) && in_array($action, $rule['deny'], true)) { 331 | return false; 332 | } 333 | 334 | if (in_array('*', $rule['allow'], true)) { 335 | return true; 336 | } 337 | 338 | return in_array($params['action'], $rule['allow'], true); 339 | } 340 | 341 | return false; 342 | } 343 | 344 | /** 345 | * Hack to get the auth data here for hasAccess(). 346 | * We re-use the cached data for performance reasons. 347 | * 348 | * @throws \Cake\Core\Exception\CakeException 349 | * @return array 350 | */ 351 | protected function _getAuth() { 352 | if ($this->auth) { 353 | return $this->auth; 354 | } 355 | 356 | $authAllow = $this->_getAllow(); 357 | 358 | $this->auth = $authAllow; 359 | 360 | return $authAllow; 361 | } 362 | 363 | /** 364 | * Parses INI file and returns the allowed roles per action. 365 | * 366 | * Uses cache for maximum performance. 367 | * Improved speed by several actions before caching: 368 | * - Resolves role slugs to their primary key / identifier 369 | * - Resolves wildcards to their verbose translation 370 | * 371 | * @param array|string|null $path 372 | * @return array 373 | */ 374 | protected function _getAcl($path = null) { 375 | if ($this->getConfig('autoClearCache') && Configure::read('debug')) { 376 | Cache::clear(Cache::KEY_ACL); 377 | } 378 | $acl = Cache::read(Cache::KEY_ACL); 379 | if ($acl !== null) { 380 | return $acl; 381 | } 382 | 383 | if ($path === null) { 384 | $path = $this->getConfig('aclFilePath'); 385 | } 386 | $config = $this->getConfig(); 387 | $config['filePath'] = $path; 388 | $config['file'] = $config['aclFile']; 389 | unset($config['aclFilePath']); 390 | unset($config['aclFile']); 391 | 392 | $acl = $this->_loadAclAdapter($config['aclAdapter'])->getAcl($this->_getAvailableRoles(), $config); 393 | Cache::write(Cache::KEY_ACL, $acl); 394 | 395 | return $acl; 396 | } 397 | 398 | /** 399 | * Returns the found INI file(s) as an array. 400 | * 401 | * @param array|string|null $paths Paths to look for INI files. 402 | * @param string $file INI file name. 403 | * @return array List with all found files. 404 | */ 405 | protected function _parseFiles($paths, $file) { 406 | return Utility::parseFiles($paths, $file); 407 | } 408 | 409 | /** 410 | * Deconstructs an ACL INI section key into a named array with ACL parts. 411 | * 412 | * @param string $key INI section key as found in acl.ini 413 | * @return array Array with named keys for controller, plugin and prefix 414 | */ 415 | protected function _deconstructIniKey($key) { 416 | return Utility::deconstructIniKey($key); 417 | } 418 | 419 | /** 420 | * Constructs an ACL INI section key from a given Request. 421 | * 422 | * @param array $params The request params 423 | * @return string Hash with named keys for controller, plugin and prefix 424 | */ 425 | protected function _constructIniKey($params) { 426 | $res = $params['controller']; 427 | if (!empty($params['prefix'])) { 428 | $res = $params['prefix'] . "/$res"; 429 | } 430 | if (!empty($params['plugin'])) { 431 | $res = $params['plugin'] . ".$res"; 432 | } 433 | 434 | return $res; 435 | } 436 | 437 | /** 438 | * Returns a list of all available roles. 439 | * 440 | * Will look for a roles array in 441 | * Configure first, tries database roles table next. 442 | * 443 | * @throws \Cake\Core\Exception\CakeException 444 | * @return array List with all available roles 445 | */ 446 | protected function _getAvailableRoles() { 447 | if ($this->_roles !== null) { 448 | return $this->_roles; 449 | } 450 | 451 | $rolesTableKey = $this->getConfig('rolesTable'); 452 | if (!$rolesTableKey) { 453 | throw new CakeException('Invalid/missing rolesTable config'); 454 | } 455 | 456 | $roles = Configure::read($rolesTableKey); 457 | if (is_array($roles)) { 458 | if ($this->getConfig('superAdminRole')) { 459 | $key = $this->getConfig('superAdmin') ?: 'superadmin'; 460 | $roles[$key] = $this->getConfig('superAdminRole'); 461 | } 462 | 463 | if (!$roles) { 464 | throw new CakeException('Invalid roles config: No roles found in config.'); 465 | } 466 | 467 | return $roles; 468 | } 469 | 470 | $roles = $this->_getRolesFromDb($rolesTableKey); 471 | if ($this->getConfig('superAdminRole')) { 472 | $key = $this->getConfig('superAdmin') ?: 'superadmin'; 473 | /** @var int $value */ 474 | $value = $this->getConfig('superAdminRole'); 475 | $roles[$key] = $value; 476 | } 477 | 478 | if (count($roles) < 1) { 479 | throw new CakeException('Invalid TinyAuth role setup (roles table `' . $rolesTableKey . '` has no roles)'); 480 | } 481 | 482 | $this->_roles = $roles; 483 | 484 | return $roles; 485 | } 486 | 487 | /** 488 | * @param string $rolesTableKey 489 | * 490 | * @throws \Cake\Core\Exception\CakeException 491 | * 492 | * @return array 493 | */ 494 | protected function _getRolesFromDb(string $rolesTableKey): array { 495 | try { 496 | $rolesTable = TableRegistry::getTableLocator()->get($rolesTableKey); 497 | $result = $rolesTable->find()->formatResults(function (ResultSetInterface $results) { 498 | return $results->combine($this->getConfig('aliasColumn'), 'id'); 499 | }); 500 | } catch (RuntimeException $e) { 501 | throw new CakeException('Invalid roles config: DB table `' . $rolesTableKey . '` cannot be found/accessed (`' . $e->getMessage() . '`).', null, $e); 502 | } 503 | 504 | /** @var array */ 505 | return $result->toArray(); 506 | } 507 | 508 | /** 509 | * Returns a list of all roles belonging to the authenticated user. 510 | * 511 | * Lookup in the following order: 512 | * - single role id using the roleColumn in single-role mode 513 | * - direct lookup in the pivot table (to support both Configure and Model 514 | * in multi-role mode) 515 | * 516 | * @param \ArrayAccess|array $user The user to get the roles for 517 | * @return array List with all role ids belonging to the user 518 | */ 519 | protected function _getUserRoles(ArrayAccess|array $user) { 520 | // Single-role from session 521 | if (!$this->getConfig('multiRole')) { 522 | $roleColumn = $this->getConfig('roleColumn'); 523 | if (!$roleColumn) { 524 | throw new CakeException('Invalid TinyAuth config, `roleColumn` config missing.'); 525 | } 526 | 527 | if (!array_key_exists($roleColumn, (array)$user)) { 528 | throw new CakeException(sprintf('Missing TinyAuth role id field (%s) in user session', 'Auth.User.' . $this->getConfig('roleColumn'))); 529 | } 530 | if (!isset($user[$this->getConfig('roleColumn')])) { 531 | return []; 532 | } 533 | 534 | $role = $user[$this->getConfig('roleColumn')]; 535 | 536 | return $this->_mapped([$role]); 537 | } 538 | 539 | // Multi-role from session 540 | if (isset($user[$this->getConfig('rolesTable')])) { 541 | $userRoles = $user[$this->getConfig('rolesTable')]; 542 | if (isset($userRoles[0]['id'])) { 543 | $userRoles = Hash::extract($userRoles, '{n}.id'); 544 | } 545 | 546 | return $this->_mapped((array)$userRoles); 547 | } 548 | 549 | // Multi-role from session via pivot table 550 | $pivotTableName = $this->_pivotTableName(); 551 | if (isset($user[$pivotTableName])) { 552 | $userRoles = $user[$pivotTableName]; 553 | if (isset($userRoles[0][$this->getConfig('roleColumn')])) { 554 | $userRoles = Hash::extract($userRoles, '{n}.' . $this->getConfig('roleColumn')); 555 | } 556 | 557 | return $this->_mapped((array)$userRoles); 558 | } 559 | 560 | // Multi-role from DB: load the pivot table 561 | $roles = $this->_getRolesFromJunction($pivotTableName, $user[$this->getConfig('idColumn')]); 562 | if (!$roles) { 563 | return []; 564 | } 565 | 566 | return $this->_mapped($roles); 567 | } 568 | 569 | /** 570 | * @return string 571 | */ 572 | protected function _pivotTableName() { 573 | $pivotTableName = $this->getConfig('pivotTable'); 574 | if (!$pivotTableName) { 575 | [, $rolesTableName] = pluginSplit($this->getConfig('rolesTable')); 576 | [, $usersTableName] = pluginSplit($this->getConfig('usersTable')); 577 | $tables = [ 578 | $usersTableName, 579 | $rolesTableName, 580 | ]; 581 | asort($tables); 582 | $pivotTableName = implode('', $tables); 583 | } 584 | 585 | return $pivotTableName; 586 | } 587 | 588 | /** 589 | * @param string $pivotTableName 590 | * @param int $id User ID 591 | * @return array 592 | */ 593 | protected function _getRolesFromJunction($pivotTableName, $id) { 594 | if (isset($this->_userRoles[$id])) { 595 | return $this->_userRoles[$id]; 596 | } 597 | 598 | $pivotTable = TableRegistry::getTableLocator()->get($pivotTableName); 599 | $roleColumn = $this->getConfig('roleColumn'); 600 | $roles = $pivotTable->find() 601 | ->select($roleColumn) 602 | ->where([$this->getConfig('userColumn') => $id]) 603 | ->all() 604 | ->extract($roleColumn) 605 | ->toArray(); 606 | 607 | $this->_userRoles[$id] = $roles; 608 | 609 | return $roles; 610 | } 611 | 612 | /** 613 | * Returns current roles as [alias => id] pairs. 614 | * 615 | * @param array $roles 616 | * @return array 617 | */ 618 | protected function _mapped(array $roles) { 619 | $availableRoles = $this->_getAvailableRoles(); 620 | 621 | $array = []; 622 | foreach ($roles as $role) { 623 | if ($role instanceof BackedEnum) { 624 | $alias = $role instanceof EnumLabelInterface ? $role->label() : $role->name; 625 | $value = $role->value; 626 | $array[$alias] = $value; 627 | 628 | continue; 629 | } 630 | 631 | $alias = array_keys($availableRoles, $role); 632 | $alias = array_shift($alias); 633 | if (!$alias || !is_string($alias)) { 634 | continue; 635 | } 636 | 637 | $array[$alias] = $role; 638 | } 639 | 640 | return $array; 641 | } 642 | 643 | } 644 | -------------------------------------------------------------------------------- /src/Auth/AllowAdapter/AllowAdapterInterface.php: -------------------------------------------------------------------------------- 1 | $config Current TinyAuth configuration values. 11 | * 12 | * @return array 13 | */ 14 | public function getAllow(array $config): array; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Auth/AllowAdapter/IniAllowAdapter.php: -------------------------------------------------------------------------------- 1 | $actions) { 20 | $auth[$key] = Utility::deconstructIniKey($key); 21 | 22 | $actions = explode(',', $actions); 23 | foreach ($actions as $k => $action) { 24 | $action = trim($action); 25 | if ($action === '') { 26 | unset($actions[$k]); 27 | 28 | continue; 29 | } 30 | $actions[$k] = $action; 31 | } 32 | 33 | if (Configure::read('debug')) { 34 | $auth[$key]['map'] = $actions; 35 | } 36 | $auth[$key]['deny'] = []; 37 | $auth[$key]['allow'] = []; 38 | 39 | foreach ($actions as $action) { 40 | $denied = mb_substr($action, 0, 1) === '!'; 41 | if ($denied) { 42 | $auth[$key]['deny'][] = mb_substr($action, 1); 43 | 44 | continue; 45 | } 46 | 47 | $auth[$key]['allow'][] = $action; 48 | } 49 | } 50 | 51 | return $auth; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Auth/AllowTrait.php: -------------------------------------------------------------------------------- 1 | _getAllow($this->getConfig('allowFilePath')); 29 | 30 | $allowDefaults = $this->_getAllowDefaultsForCurrentParams($params); 31 | 32 | foreach ($rules as $rule) { 33 | if (isset($params['plugin'])) { 34 | if ($params['plugin'] !== $rule['plugin']) { 35 | continue; 36 | } 37 | } else { 38 | if (!empty($rule['plugin'])) { 39 | continue; 40 | } 41 | } 42 | if (isset($params['prefix'])) { 43 | if ($params['prefix'] !== $rule['prefix']) { 44 | continue; 45 | } 46 | } else { 47 | if (!empty($rule['prefix'])) { 48 | continue; 49 | } 50 | } 51 | if ($params['controller'] !== $rule['controller']) { 52 | continue; 53 | } 54 | 55 | if ($allowDefaults) { 56 | $rule['allow'] = array_merge($rule['allow'], $allowDefaults); 57 | } 58 | 59 | return $rule; 60 | } 61 | 62 | return [ 63 | 'allow' => $allowDefaults, 64 | 'deny' => [], 65 | ]; 66 | } 67 | 68 | /** 69 | * @param array $rule 70 | * @param string $action 71 | * 72 | * @return bool 73 | */ 74 | protected function _isActionAllowed(array $rule, $action) { 75 | $rule += [ 76 | 'deny' => [], 77 | 'allow' => [], 78 | ]; 79 | 80 | if (in_array($action, $rule['deny'], true) || in_array('*', $rule['deny'], true)) { 81 | return false; 82 | } 83 | 84 | if (!in_array($action, $rule['allow'], true) && !in_array('*', $rule['allow'], true)) { 85 | return false; 86 | } 87 | 88 | return true; 89 | } 90 | 91 | /** 92 | * @param array $params 93 | * @return array 94 | */ 95 | protected function _getAllowDefaultsForCurrentParams(array $params) { 96 | if ($this->getConfig('allowNonPrefixed') && empty($params['prefix'])) { 97 | return ['*']; 98 | } 99 | 100 | if (empty($params['prefix'])) { 101 | return []; 102 | } 103 | 104 | /** @var array $allowedPrefixes */ 105 | $allowedPrefixes = (array)$this->getConfig('allowPrefixes'); 106 | 107 | $result = []; 108 | if ($allowedPrefixes) { 109 | foreach ($allowedPrefixes as $allowedPrefix) { 110 | if ($params['prefix'] === $allowedPrefix || strpos($params['prefix'], $allowedPrefix . '/') === 0) { 111 | return ['*']; 112 | } 113 | } 114 | } 115 | 116 | return $result; 117 | } 118 | 119 | /** 120 | * @param string|null $path 121 | * @return array 122 | */ 123 | protected function _getAllow($path = null) { 124 | if ($this->getConfig('autoClearCache') && Configure::read('debug')) { 125 | Cache::clear(Cache::KEY_ALLOW); 126 | } 127 | $auth = Cache::read(Cache::KEY_ALLOW); 128 | if ($auth !== null) { 129 | return $auth; 130 | } 131 | 132 | if ($path === null) { 133 | $path = $this->getConfig('allowFilePath'); 134 | } 135 | 136 | $config = $this->getConfig(); 137 | $config['filePath'] = $path; 138 | $config['file'] = $config['allowFile']; 139 | unset($config['allowFilePath']); 140 | unset($config['allowFile']); 141 | 142 | $auth = $this->_loadAllowAdapter($config['allowAdapter'])->getAllow($config); 143 | 144 | Cache::write(Cache::KEY_ALLOW, $auth); 145 | 146 | return $auth; 147 | } 148 | 149 | /** 150 | * Finds the authentication adapter to use for this request. 151 | * 152 | * @param string $adapter Acl adapter to load. 153 | * @throws \Cake\Core\Exception\CakeException 154 | * @throws \InvalidArgumentException 155 | * @return \TinyAuth\Auth\AllowAdapter\AllowAdapterInterface 156 | */ 157 | protected function _loadAllowAdapter($adapter) { 158 | if ($this->_allowAdapter !== null) { 159 | return $this->_allowAdapter; 160 | } 161 | 162 | if (!class_exists($adapter)) { 163 | throw new CakeException(sprintf('The Acl Adapter class "%s" was not found.', $adapter)); 164 | } 165 | 166 | $adapterInstance = new $adapter(); 167 | if (!($adapterInstance instanceof AllowAdapterInterface)) { 168 | throw new InvalidArgumentException(sprintf( 169 | 'TinyAuth Acl adapters have to implement %s.', 170 | AllowAdapterInterface::class, 171 | )); 172 | } 173 | $this->_allowAdapter = $adapterInstance; 174 | 175 | return $adapterInstance; 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/Auth/AuthUserTrait.php: -------------------------------------------------------------------------------- 1 | getConfig('idColumn'); 52 | 53 | return $this->user($field); 54 | } 55 | 56 | /** 57 | * This check can be used to tell if a record that belongs to some user is the 58 | * current logged in user 59 | * 60 | * @param string|int $userId 61 | * @return bool 62 | */ 63 | public function isMe($userId): bool { 64 | $field = $this->getConfig('idColumn'); 65 | 66 | return $userId && (string)$userId === (string)$this->user($field); 67 | } 68 | 69 | /** 70 | * Get the user data of the current session. 71 | * 72 | * @param string|null $key Key in dot syntax. 73 | * @return mixed Data 74 | */ 75 | public function user(?string $key = null) { 76 | $user = $this->_getUser(); 77 | if ($key === null) { 78 | return $user; 79 | } 80 | 81 | return Hash::get($user, $key); 82 | } 83 | 84 | /** 85 | * Get the role(s) of the current session. 86 | * 87 | * It will return the single role for single role setup, and a flat 88 | * list of roles for multi role setup. 89 | * 90 | * @return array Array of roles 91 | */ 92 | public function roles(): array { 93 | $user = $this->user(); 94 | if (!$user) { 95 | return []; 96 | } 97 | 98 | $roles = $this->_getUserRoles($user); 99 | 100 | return $roles; 101 | } 102 | 103 | /** 104 | * Check if the current session has this role. 105 | * 106 | * @param mixed $expectedRole 107 | * @param mixed|null $providedRoles 108 | * @return bool Success 109 | */ 110 | public function hasRole($expectedRole, $providedRoles = null) { 111 | if ($providedRoles !== null) { 112 | $roles = (array)$providedRoles; 113 | } else { 114 | $roles = $this->roles(); 115 | } 116 | 117 | if (!$roles) { 118 | return false; 119 | } 120 | 121 | if (array_key_exists($expectedRole, $roles) || in_array($expectedRole, $roles)) { 122 | return true; 123 | } 124 | 125 | return false; 126 | } 127 | 128 | /** 129 | * Check if the current session has one of these roles. 130 | * 131 | * You can either require one of the roles (default), or you can require all 132 | * roles to match. 133 | * 134 | * @param mixed $expectedRoles 135 | * @param bool $oneRoleIsEnough (if all $roles have to match instead of just one) 136 | * @param mixed|null $providedRoles 137 | * @return bool Success 138 | */ 139 | public function hasRoles($expectedRoles, $oneRoleIsEnough = true, $providedRoles = null): bool { 140 | if ($providedRoles !== null) { 141 | $roles = $providedRoles; 142 | } else { 143 | $roles = $this->roles(); 144 | } 145 | 146 | $expectedRoles = (array)$expectedRoles; 147 | if (!$expectedRoles) { 148 | return false; 149 | } 150 | 151 | $count = 0; 152 | foreach ($expectedRoles as $expectedRole) { 153 | if ($this->hasRole($expectedRole, $roles)) { 154 | if ($oneRoleIsEnough) { 155 | return true; 156 | } 157 | $count++; 158 | } else { 159 | if (!$oneRoleIsEnough) { 160 | return false; 161 | } 162 | } 163 | } 164 | 165 | if ($count === count($expectedRoles)) { 166 | return true; 167 | } 168 | 169 | return false; 170 | } 171 | 172 | /** 173 | * @param \ArrayAccess|array $identity 174 | * @return array 175 | */ 176 | protected function _toArray(ArrayAccess|array $identity): array { 177 | if (is_array($identity)) { 178 | return $identity; 179 | } 180 | 181 | if (method_exists($identity, 'toArray')) { 182 | return $identity->toArray(); 183 | } 184 | if ($identity instanceof Traversable) { 185 | return iterator_to_array($identity); 186 | } 187 | if (!$identity instanceof \Countable) { 188 | throw new InvalidArgumentException('You cannot use a pure ArrayAccess object as identity. Use Entity or ArrayObject at least.'); 189 | } 190 | 191 | return (array)$identity; 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /src/Auth/BaseAuthenticate.php: -------------------------------------------------------------------------------- 1 | ['some_finder_option' => 'some_value']] 44 | * - `passwordHasher` Password hasher class. Can be a string specifying class name 45 | * or an array containing `className` key, any other keys will be passed as 46 | * config to the class. Defaults to 'Default'. 47 | * 48 | * @var array 49 | */ 50 | protected $_defaultConfig = [ 51 | 'fields' => [ 52 | 'username' => 'username', 53 | 'password' => 'password', 54 | ], 55 | 'userModel' => 'Users', 56 | 'finder' => 'all', 57 | 'passwordHasher' => 'TinyAuth.Default', 58 | ]; 59 | 60 | /** 61 | * A Component registry, used to get more components. 62 | * 63 | * @var \Cake\Controller\ComponentRegistry 64 | */ 65 | protected $_registry; 66 | 67 | /** 68 | * Password hasher instance. 69 | * 70 | * @var \TinyAuth\Auth\AbstractPasswordHasher|null 71 | */ 72 | protected $_passwordHasher; 73 | 74 | /** 75 | * Whether the user authenticated by this class 76 | * requires their password to be rehashed with another algorithm. 77 | * 78 | * @var bool 79 | */ 80 | protected $_needsPasswordRehash = false; 81 | 82 | /** 83 | * Constructor 84 | * 85 | * @param \Cake\Controller\ComponentRegistry $registry The Component registry used on this request. 86 | * @param array $config Array of config to use. 87 | */ 88 | public function __construct(ComponentRegistry $registry, array $config = []) { 89 | $this->_registry = $registry; 90 | $this->setConfig($config); 91 | } 92 | 93 | /** 94 | * Find a user record using the username and password provided. 95 | * 96 | * Input passwords will be hashed even when a user doesn't exist. This 97 | * helps mitigate timing attacks that are attempting to find valid usernames. 98 | * 99 | * @param string $username The username/identifier. 100 | * @param string|null $password The password, if not provided password checking is skipped 101 | * and result of find is returned. 102 | * @return array|false Either false on failure, or an array of user data. 103 | */ 104 | protected function _findUser(string $username, ?string $password = null) { 105 | $result = $this->_query($username)->first(); 106 | 107 | if ($result === null) { 108 | // Waste time hashing the password, to prevent 109 | // timing side-channels. However, don't hash 110 | // null passwords as authentication systems 111 | // like digest auth don't use passwords 112 | // and hashing *could* create a timing side-channel. 113 | if ($password !== null) { 114 | $hasher = $this->passwordHasher(); 115 | $hasher->hash($password); 116 | } 117 | 118 | return false; 119 | } 120 | 121 | $passwordField = $this->_config['fields']['password']; 122 | if ($password !== null) { 123 | $hasher = $this->passwordHasher(); 124 | $hashedPassword = $result->get($passwordField); 125 | 126 | if ($hashedPassword === null || $hashedPassword === '') { 127 | // Waste time hashing the password, to prevent 128 | // timing side-channels to distinguish whether 129 | // user has password or not. 130 | $hasher->hash($password); 131 | 132 | return false; 133 | } 134 | 135 | if (!$hasher->check($password, $hashedPassword)) { 136 | return false; 137 | } 138 | 139 | $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword); 140 | $result->unset($passwordField); 141 | } 142 | $hidden = $result->getHidden(); 143 | if ($password === null && in_array($passwordField, $hidden, true)) { 144 | $key = array_search($passwordField, $hidden, true); 145 | unset($hidden[$key]); 146 | $result->setHidden($hidden); 147 | } 148 | 149 | return $result->toArray(); 150 | } 151 | 152 | /** 153 | * Get query object for fetching user from database. 154 | * 155 | * @param string $username The username/identifier. 156 | * @return \Cake\ORM\Query\SelectQuery 157 | */ 158 | protected function _query(string $username): SelectQuery { 159 | $config = $this->_config; 160 | $table = $this->getTableLocator()->get($config['userModel']); 161 | 162 | $options = [ 163 | 'conditions' => [$table->aliasField($config['fields']['username']) => $username], 164 | ]; 165 | 166 | $finder = $config['finder']; 167 | if (is_array($finder)) { 168 | $options += current($finder); 169 | $finder = key($finder); 170 | } 171 | 172 | $options['username'] = $options['username'] ?? $username; 173 | 174 | return $table->find($finder, ...$options); 175 | } 176 | 177 | /** 178 | * Return password hasher object 179 | * 180 | * @throws \RuntimeException If password hasher class not found or 181 | * it does not extend AbstractPasswordHasher 182 | * @return \TinyAuth\Auth\AbstractPasswordHasher Password hasher instance 183 | */ 184 | public function passwordHasher(): AbstractPasswordHasher { 185 | if ($this->_passwordHasher !== null) { 186 | return $this->_passwordHasher; 187 | } 188 | 189 | $passwordHasher = $this->_config['passwordHasher']; 190 | 191 | return $this->_passwordHasher = PasswordHasherFactory::build($passwordHasher); 192 | } 193 | 194 | /** 195 | * Returns whether the password stored in the repository for the logged in user 196 | * requires to be rehashed with another algorithm 197 | * 198 | * @return bool 199 | */ 200 | public function needsPasswordRehash(): bool { 201 | return $this->_needsPasswordRehash; 202 | } 203 | 204 | /** 205 | * Authenticate a user based on the request information. 206 | * 207 | * @param \Cake\Http\ServerRequest $request Request to get authentication information from. 208 | * @param \Cake\Http\Response $response A response object that can have headers added. 209 | * @return array|false Either false on failure, or an array of user data on success. 210 | */ 211 | abstract public function authenticate(ServerRequest $request, Response $response); 212 | 213 | /** 214 | * Get a user based on information in the request. Primarily used by stateless authentication 215 | * systems like basic and digest auth. 216 | * 217 | * @param \Cake\Http\ServerRequest $request Request object. 218 | * @return array|false Either false or an array of user information 219 | */ 220 | public function getUser(ServerRequest $request) { 221 | return false; 222 | } 223 | 224 | /** 225 | * Handle unauthenticated access attempt. In implementation valid return values 226 | * can be: 227 | * 228 | * - Null - No action taken, AuthComponent should return appropriate response. 229 | * - \Cake\Http\Response - A response object, which will cause AuthComponent to 230 | * simply return that response. 231 | * 232 | * @param \Cake\Http\ServerRequest $request A request object. 233 | * @param \Cake\Http\Response $response A response object. 234 | * @return \Cake\Http\Response|null|void 235 | */ 236 | public function unauthenticated(ServerRequest $request, Response $response) { 237 | } 238 | 239 | /** 240 | * Returns a list of all events that this authenticate class will listen to. 241 | * 242 | * An authenticate class can listen to following events fired by AuthComponent: 243 | * 244 | * - `Auth.afterIdentify` - Fired after a user has been identified using one of 245 | * configured authenticate class. The callback function should have signature 246 | * like `afterIdentify(EventInterface $event, array $user)` when `$user` is the 247 | * identified user record. 248 | * 249 | * - `Auth.logout` - Fired when AuthComponent::logout() is called. The callback 250 | * function should have signature like `logout(EventInterface $event, array $user)` 251 | * where `$user` is the user about to be logged out. 252 | * 253 | * @return array List of events this class listens to. Defaults to `[]`. 254 | */ 255 | public function implementedEvents(): array { 256 | return []; 257 | } 258 | 259 | } 260 | -------------------------------------------------------------------------------- /src/Auth/BaseAuthorize.php: -------------------------------------------------------------------------------- 1 | 44 | */ 45 | protected $_defaultConfig = []; 46 | 47 | /** 48 | * Constructor 49 | * 50 | * @param \Cake\Controller\ComponentRegistry $registry The controller for this request. 51 | * @param array $config An array of config. This class does not use any config. 52 | */ 53 | public function __construct(ComponentRegistry $registry, array $config = []) { 54 | $this->_registry = $registry; 55 | $this->setConfig($config); 56 | } 57 | 58 | /** 59 | * Checks user authorization. 60 | * 61 | * @param \ArrayAccess|array $user Active user data 62 | * @param \Cake\Http\ServerRequest $request Request instance. 63 | * @return bool 64 | */ 65 | abstract public function authorize($user, ServerRequest $request): bool; 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/Auth/BasicAuthenticate.php: -------------------------------------------------------------------------------- 1 | loadComponent('Auth', [ 36 | * 'authenticate' => ['Basic'] 37 | * 'storage' => 'Memory', 38 | * 'unauthorizedRedirect' => false, 39 | * ]); 40 | * ``` 41 | * 42 | * You should set `storage` to `Memory` to prevent CakePHP from sending a 43 | * session cookie to the client. 44 | * 45 | * You should set `unauthorizedRedirect` to `false`. This causes `AuthComponent` to 46 | * throw a `ForbiddenException` exception instead of redirecting to another page. 47 | * 48 | * Since HTTP Basic Authentication is stateless you don't need call `setUser()` 49 | * in your controller. The user credentials will be checked on each request. If 50 | * valid credentials are not provided, required authentication headers will be sent 51 | * by this authentication provider which triggers the login dialog in the browser/client. 52 | * 53 | * @see https://book.cakephp.org/4/en/controllers/components/authentication.html 54 | */ 55 | class BasicAuthenticate extends BaseAuthenticate { 56 | 57 | /** 58 | * Authenticate a user using HTTP auth. Will use the configured User model and attempt a 59 | * login using HTTP auth. 60 | * 61 | * @param \Cake\Http\ServerRequest $request The request to authenticate with. 62 | * @param \Cake\Http\Response $response The response to add headers to. 63 | * @return array|false Either false on failure, or an array of user data on success. 64 | */ 65 | public function authenticate(ServerRequest $request, Response $response) { 66 | return $this->getUser($request); 67 | } 68 | 69 | /** 70 | * Get a user based on information in the request. Used by cookie-less auth for stateless clients. 71 | * 72 | * @param \Cake\Http\ServerRequest $request Request object. 73 | * @return array|false Either false or an array of user information 74 | */ 75 | public function getUser(ServerRequest $request) { 76 | $username = $request->getEnv('PHP_AUTH_USER'); 77 | $pass = $request->getEnv('PHP_AUTH_PW'); 78 | 79 | if (!is_string($username) || $username === '' || !is_string($pass) || $pass === '') { 80 | return false; 81 | } 82 | 83 | return $this->_findUser($username, $pass); 84 | } 85 | 86 | /** 87 | * Handles an unauthenticated access attempt by sending appropriate login headers 88 | * 89 | * @param \Cake\Http\ServerRequest $request A request object. 90 | * @param \Cake\Http\Response $response A response object. 91 | * @throws \Cake\Http\Exception\UnauthorizedException 92 | * @return \Cake\Http\Response|null|void 93 | */ 94 | public function unauthenticated(ServerRequest $request, Response $response) { 95 | $unauthorizedException = new UnauthorizedException(); 96 | $unauthorizedException->setHeaders($this->loginHeaders($request)); 97 | 98 | throw $unauthorizedException; 99 | } 100 | 101 | /** 102 | * Generate the login headers 103 | * 104 | * @param \Cake\Http\ServerRequest $request Request object. 105 | * @return array Headers for logging in. 106 | */ 107 | public function loginHeaders(ServerRequest $request): array { 108 | $realm = $this->getConfig('realm') ?: $request->getEnv('SERVER_NAME'); 109 | 110 | return [ 111 | 'WWW-Authenticate' => sprintf('Basic realm="%s"', $realm), 112 | ]; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/Auth/ControllerAuthorize.php: -------------------------------------------------------------------------------- 1 | request->getParam('admin')) { 34 | * return $user['role'] === 'admin'; 35 | * } 36 | * return !empty($user); 37 | * } 38 | * ``` 39 | * 40 | * The above is simple implementation that would only authorize users of the 41 | * 'admin' role to access admin routing. 42 | * 43 | * @see \Cake\Controller\Component\AuthComponent::$authenticate 44 | */ 45 | class ControllerAuthorize extends BaseAuthorize { 46 | 47 | /** 48 | * Controller for the request. 49 | * 50 | * @var \Cake\Controller\Controller 51 | */ 52 | protected $_Controller; 53 | 54 | /** 55 | * @inheritDoc 56 | */ 57 | public function __construct(ComponentRegistry $registry, array $config = []) { 58 | parent::__construct($registry, $config); 59 | $this->controller($registry->getController()); 60 | } 61 | 62 | /** 63 | * Get/set the controller this authorize object will be working with. Also 64 | * checks that isAuthorized is implemented. 65 | * 66 | * @param \Cake\Controller\Controller|null $controller null to get, a controller to set. 67 | * @return \Cake\Controller\Controller 68 | */ 69 | public function controller(?Controller $controller = null): Controller { 70 | if ($controller) { 71 | $this->_Controller = $controller; 72 | } 73 | 74 | return $this->_Controller; 75 | } 76 | 77 | /** 78 | * Checks user authorization using a controller callback. 79 | * 80 | * @param \ArrayAccess|array $user Active user data 81 | * @param \Cake\Http\ServerRequest $request Request instance. 82 | * @throws \Cake\Core\Exception\CakeException If controller does not have method `isAuthorized()`. 83 | * @return bool 84 | */ 85 | public function authorize($user, ServerRequest $request): bool { 86 | if (!method_exists($this->_Controller, 'isAuthorized')) { 87 | throw new CakeException(sprintf( 88 | '%s does not implement an isAuthorized() method.', 89 | get_class($this->_Controller), 90 | )); 91 | } 92 | 93 | return (bool)$this->_Controller->isAuthorized($user); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Auth/DefaultPasswordHasher.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | protected array $_defaultConfig = [ 38 | 'hashType' => PASSWORD_DEFAULT, 39 | 'hashOptions' => [], 40 | ]; 41 | 42 | /** 43 | * Generates password hash. 44 | * 45 | * @psalm-suppress InvalidNullableReturnType 46 | * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#hashing-passwords 47 | * @param string $password Plain text password to hash. 48 | * @return string Password hash or false on failure 49 | */ 50 | public function hash(string $password): string { 51 | return (string)password_hash( 52 | $password, 53 | $this->_config['hashType'], 54 | $this->_config['hashOptions'], 55 | ); 56 | } 57 | 58 | /** 59 | * Check hash. Generate hash for user provided password and check against existing hash. 60 | * 61 | * @param string $password Plain text password to hash. 62 | * @param string $hashedPassword Existing hashed password. 63 | * @return bool True if hashes match else false. 64 | */ 65 | public function check(string $password, string $hashedPassword): bool { 66 | return password_verify($password, $hashedPassword); 67 | } 68 | 69 | /** 70 | * Returns true if the password need to be rehashed, due to the password being 71 | * created with anything else than the passwords generated by this class. 72 | * 73 | * @param string $password The password to verify 74 | * @return bool 75 | */ 76 | public function needsRehash(string $password): bool { 77 | return password_needs_rehash($password, $this->_config['hashType'], $this->_config['hashOptions']); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Auth/DigestAuthenticate.php: -------------------------------------------------------------------------------- 1 | loadComponent('Auth', [ 35 | * 'authenticate' => ['Digest'], 36 | * 'storage' => 'Memory', 37 | * 'unauthorizedRedirect' => false, 38 | * ]); 39 | * ``` 40 | * 41 | * You should set `storage` to `Memory` to prevent CakePHP from sending a 42 | * session cookie to the client. 43 | * 44 | * You should set `unauthorizedRedirect` to `false`. This causes `AuthComponent` to 45 | * throw a `ForbiddenException` exception instead of redirecting to another page. 46 | * 47 | * Since HTTP Digest Authentication is stateless you don't need call `setUser()` 48 | * in your controller. The user credentials will be checked on each request. If 49 | * valid credentials are not provided, required authentication headers will be sent 50 | * by this authentication provider which triggers the login dialog in the browser/client. 51 | * 52 | * ### Generating passwords compatible with Digest authentication. 53 | * 54 | * DigestAuthenticate requires a special password hash that conforms to RFC2617. 55 | * You can generate this password using `DigestAuthenticate::password()` 56 | * 57 | * ``` 58 | * $digestPass = DigestAuthenticate::password($username, $password, env('SERVER_NAME')); 59 | * ``` 60 | * 61 | * If you wish to use digest authentication alongside other authentication methods, 62 | * it's recommended that you store the digest authentication separately. For 63 | * example `User.digest_pass` could be used for a digest password, while 64 | * `User.password` would store the password hash for use with other methods like 65 | * Basic or Form. 66 | * 67 | * @see https://book.cakephp.org/4/en/controllers/components/authentication.html 68 | */ 69 | class DigestAuthenticate extends BasicAuthenticate { 70 | 71 | /** 72 | * Constructor 73 | * 74 | * Besides the keys specified in BaseAuthenticate::$_defaultConfig, 75 | * DigestAuthenticate uses the following extra keys: 76 | * 77 | * - `secret` The secret to use for nonce validation. Defaults to Security::getSalt(). 78 | * - `realm` The realm authentication is for, Defaults to the servername. 79 | * - `qop` Defaults to 'auth', no other values are supported at this time. 80 | * - `opaque` A string that must be returned unchanged by clients. 81 | * Defaults to `md5($config['realm'])` 82 | * - `nonceLifetime` The number of seconds that nonces are valid for. Defaults to 300. 83 | * 84 | * @param \Cake\Controller\ComponentRegistry $registry The Component registry 85 | * used on this request. 86 | * @param array $config Array of config to use. 87 | */ 88 | public function __construct(ComponentRegistry $registry, array $config = []) { 89 | $this->setConfig([ 90 | 'nonceLifetime' => 300, 91 | 'secret' => Security::getSalt(), 92 | 'realm' => null, 93 | 'qop' => 'auth', 94 | 'opaque' => null, 95 | ]); 96 | 97 | parent::__construct($registry, $config); 98 | } 99 | 100 | /** 101 | * Get a user based on information in the request. Used by cookie-less auth for stateless clients. 102 | * 103 | * @param \Cake\Http\ServerRequest $request Request object. 104 | * @return array|false Either false or an array of user information 105 | */ 106 | public function getUser(ServerRequest $request) { 107 | $digest = $this->_getDigest($request); 108 | if (empty($digest)) { 109 | return false; 110 | } 111 | 112 | $user = $this->_findUser($digest['username']); 113 | if (empty($user)) { 114 | return false; 115 | } 116 | 117 | if (!$this->validNonce($digest['nonce'])) { 118 | return false; 119 | } 120 | 121 | $field = $this->_config['fields']['password']; 122 | $password = $user[$field]; 123 | unset($user[$field]); 124 | 125 | $requestMethod = $request->getEnv('ORIGINAL_REQUEST_METHOD') ?: $request->getMethod(); 126 | $hash = $this->generateResponseHash( 127 | $digest, 128 | $password, 129 | (string)$requestMethod, 130 | ); 131 | if (hash_equals($hash, $digest['response'])) { 132 | return $user; 133 | } 134 | 135 | return false; 136 | } 137 | 138 | /** 139 | * Gets the digest headers from the request/environment. 140 | * 141 | * @param \Cake\Http\ServerRequest $request Request object. 142 | * @return array|null Array of digest information. 143 | */ 144 | protected function _getDigest(ServerRequest $request): ?array { 145 | $digest = $request->getEnv('PHP_AUTH_DIGEST'); 146 | if (empty($digest) && function_exists('apache_request_headers')) { 147 | $headers = apache_request_headers(); 148 | if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') { 149 | $digest = substr($headers['Authorization'], 7); 150 | } 151 | } 152 | if (empty($digest)) { 153 | return null; 154 | } 155 | 156 | return $this->parseAuthData($digest); 157 | } 158 | 159 | /** 160 | * Parse the digest authentication headers and split them up. 161 | * 162 | * @param string $digest The raw digest authentication headers. 163 | * @return array|null An array of digest authentication headers 164 | */ 165 | public function parseAuthData(string $digest): ?array { 166 | if (substr($digest, 0, 7) === 'Digest ') { 167 | $digest = substr($digest, 7); 168 | } 169 | $keys = $match = []; 170 | $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; 171 | preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER); 172 | 173 | foreach ($match as $i) { 174 | $keys[$i[1]] = $i[3]; 175 | unset($req[$i[1]]); 176 | } 177 | 178 | if (empty($req)) { 179 | return $keys; 180 | } 181 | 182 | return null; 183 | } 184 | 185 | /** 186 | * Generate the response hash for a given digest array. 187 | * 188 | * @param array $digest Digest information containing data from DigestAuthenticate::parseAuthData(). 189 | * @param string $password The digest hash password generated with DigestAuthenticate::password() 190 | * @param string $method Request method 191 | * @return string Response hash 192 | */ 193 | public function generateResponseHash(array $digest, string $password, string $method): string { 194 | return md5( 195 | $password . 196 | ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . 197 | md5($method . ':' . $digest['uri']), 198 | ); 199 | } 200 | 201 | /** 202 | * Creates an auth digest password hash to store 203 | * 204 | * @param string $username The username to use in the digest hash. 205 | * @param string $password The unhashed password to make a digest hash for. 206 | * @param string $realm The realm the password is for. 207 | * @return string the hashed password that can later be used with Digest authentication. 208 | */ 209 | public static function password(string $username, string $password, string $realm): string { 210 | return md5($username . ':' . $realm . ':' . $password); 211 | } 212 | 213 | /** 214 | * Generate the login headers 215 | * 216 | * @param \Cake\Http\ServerRequest $request Request object. 217 | * @return array Headers for logging in. 218 | */ 219 | public function loginHeaders(ServerRequest $request): array { 220 | $realm = $this->_config['realm'] ?: $request->getEnv('SERVER_NAME'); 221 | 222 | $options = [ 223 | 'realm' => $realm, 224 | 'qop' => $this->_config['qop'], 225 | 'nonce' => $this->generateNonce(), 226 | 'opaque' => $this->_config['opaque'] ?: md5($realm), 227 | ]; 228 | 229 | $digest = $this->_getDigest($request); 230 | if ($digest && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) { 231 | $options['stale'] = true; 232 | } 233 | 234 | $opts = []; 235 | foreach ($options as $k => $v) { 236 | if (is_bool($v)) { 237 | $v = $v ? 'true' : 'false'; 238 | $opts[] = sprintf('%s=%s', $k, $v); 239 | } else { 240 | $opts[] = sprintf('%s="%s"', $k, $v); 241 | } 242 | } 243 | 244 | return [ 245 | 'WWW-Authenticate' => 'Digest ' . implode(',', $opts), 246 | ]; 247 | } 248 | 249 | /** 250 | * Generate a nonce value that is validated in future requests. 251 | * 252 | * @return string 253 | */ 254 | protected function generateNonce(): string { 255 | $expiryTime = microtime(true) + $this->getConfig('nonceLifetime'); 256 | $secret = $this->getConfig('secret'); 257 | $signatureValue = hash_hmac('sha256', $expiryTime . ':' . $secret, $secret); 258 | $nonceValue = $expiryTime . ':' . $signatureValue; 259 | 260 | return base64_encode($nonceValue); 261 | } 262 | 263 | /** 264 | * Check the nonce to ensure it is valid and not expired. 265 | * 266 | * @param string $nonce The nonce value to check. 267 | * @return bool 268 | */ 269 | protected function validNonce(string $nonce): bool { 270 | /** @var string|false $value */ 271 | $value = base64_decode($nonce); 272 | if ($value === false) { 273 | return false; 274 | } 275 | $parts = explode(':', $value); 276 | if (count($parts) !== 2) { 277 | return false; 278 | } 279 | [$expires, $checksum] = $parts; 280 | if ($expires < microtime(true)) { 281 | return false; 282 | } 283 | $secret = $this->getConfig('secret'); 284 | $check = hash_hmac('sha256', $expires . ':' . $secret, $secret); 285 | 286 | return hash_equals($check, $checksum); 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /src/Auth/FallbackPasswordHasher.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | protected array $_defaultConfig = [ 33 | 'hashers' => [], 34 | ]; 35 | 36 | /** 37 | * Holds the list of password hasher objects that will be used 38 | * 39 | * @var array<\TinyAuth\Auth\AbstractPasswordHasher> 40 | */ 41 | protected $_hashers = []; 42 | 43 | /** 44 | * Constructor 45 | * 46 | * @param array $config configuration options for this object. Requires the 47 | * `hashers` key to be present in the array with a list of other hashers to be 48 | * used. 49 | */ 50 | public function __construct(array $config = []) { 51 | parent::__construct($config); 52 | foreach ($this->_config['hashers'] as $key => $hasher) { 53 | if (is_array($hasher) && !isset($hasher['className'])) { 54 | $hasher['className'] = $key; 55 | } 56 | $this->_hashers[] = PasswordHasherFactory::build($hasher); 57 | } 58 | } 59 | 60 | /** 61 | * Generates password hash. 62 | * 63 | * Uses the first password hasher in the list to generate the hash 64 | * 65 | * @param string $password Plain text password to hash. 66 | * @return string Password hash or false 67 | */ 68 | public function hash(string $password): string { 69 | return (string)$this->_hashers[0]->hash($password); 70 | } 71 | 72 | /** 73 | * Verifies that the provided password corresponds to its hashed version 74 | * 75 | * This will iterate over all configured hashers until one of them returns 76 | * true. 77 | * 78 | * @param string $password Plain text password to hash. 79 | * @param string $hashedPassword Existing hashed password. 80 | * @return bool True if hashes match else false. 81 | */ 82 | public function check(string $password, string $hashedPassword): bool { 83 | foreach ($this->_hashers as $hasher) { 84 | if ($hasher->check($password, $hashedPassword)) { 85 | return true; 86 | } 87 | } 88 | 89 | return false; 90 | } 91 | 92 | /** 93 | * Returns true if the password need to be rehashed, with the first hasher present 94 | * in the list of hashers 95 | * 96 | * @param string $password The password to verify 97 | * @return bool 98 | */ 99 | public function needsRehash(string $password): bool { 100 | return $this->_hashers[0]->needsRehash($password); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/Auth/FormAuthenticate.php: -------------------------------------------------------------------------------- 1 | loadComponent('Auth', [ 35 | * 'authenticate' => [ 36 | * 'Form' => [ 37 | * 'fields' => ['username' => 'email', 'password' => 'passwd'], 38 | * 'finder' => 'auth', 39 | * ] 40 | * ] 41 | * ]); 42 | * ``` 43 | * 44 | * When configuring FormAuthenticate you can pass in config to which fields, model and finder 45 | * are used. See `BaseAuthenticate::$_defaultConfig` for more information. 46 | * 47 | * @see https://book.cakephp.org/4/en/controllers/components/authentication.html 48 | */ 49 | class FormAuthenticate extends BaseAuthenticate { 50 | 51 | /** 52 | * Checks the fields to ensure they are supplied. 53 | * 54 | * @param \Cake\Http\ServerRequest $request The request that contains login information. 55 | * @param array $fields The fields to be checked. 56 | * @return bool False if the fields have not been supplied. True if they exist. 57 | */ 58 | protected function _checkFields(ServerRequest $request, array $fields): bool { 59 | foreach ([$fields['username'], $fields['password']] as $field) { 60 | $value = $request->getData($field); 61 | if (empty($value) || !is_string($value)) { 62 | return false; 63 | } 64 | } 65 | 66 | return true; 67 | } 68 | 69 | /** 70 | * Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields` 71 | * to find POST data that is used to find a matching record in the `config.userModel`. Will return false if 72 | * there is no post data, either username or password is missing, or if the scope conditions have not been met. 73 | * 74 | * @param \Cake\Http\ServerRequest $request The request that contains login information. 75 | * @param \Cake\Http\Response $response Unused response object. 76 | * @return array|false False on login failure. An array of User data on success. 77 | */ 78 | public function authenticate(ServerRequest $request, Response $response) { 79 | $fields = $this->_config['fields']; 80 | if (!$this->_checkFields($request, $fields)) { 81 | return false; 82 | } 83 | 84 | return $this->_findUser( 85 | $request->getData($fields['username']), 86 | $request->getData($fields['password']), 87 | ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Auth/MultiColumnAuthenticate.php: -------------------------------------------------------------------------------- 1 | Auth->setConfig('authenticate', [ 17 | * 'TinyAuth.MultiColumn' => [ 18 | * 'fields' => [ 19 | * 'username' => 'login', 20 | * 'password' => 'password' 21 | * ], 22 | * 'columns' => ['username', 'email'], 23 | * ] 24 | * ]); 25 | * ``` 26 | * 27 | * Licensed under The MIT License 28 | * Copied from discontinued FriendsOfCake/Authenticate 29 | */ 30 | class MultiColumnAuthenticate extends FormAuthenticate { 31 | 32 | /** 33 | * Besides the keys specified in BaseAuthenticate::$_defaultConfig, 34 | * MultiColumnAuthenticate uses the following extra keys: 35 | * 36 | * - 'columns' Array of columns to check username form input against 37 | * 38 | * @param \Cake\Controller\ComponentRegistry $registry The Component registry 39 | * used on this request. 40 | * @param array $config Array of config to use. 41 | */ 42 | public function __construct(ComponentRegistry $registry, array $config) { 43 | $this->setConfig([ 44 | 'columns' => [], 45 | ]); 46 | 47 | parent::__construct($registry, $config); 48 | } 49 | 50 | /** 51 | * Get query object for fetching user from database. 52 | * 53 | * @param string $username The username/identifier. 54 | * @return \Cake\ORM\Query\SelectQuery 55 | */ 56 | protected function _query(string $username): SelectQuery { 57 | $table = TableRegistry::getTableLocator()->get($this->_config['userModel']); 58 | 59 | $columns = []; 60 | foreach ($this->_config['columns'] as $column) { 61 | $columns[] = [$table->aliasField($column) => $username]; 62 | } 63 | $conditions = ['OR' => $columns]; 64 | 65 | $options = [ 66 | 'conditions' => $conditions, 67 | ]; 68 | 69 | if (!empty($this->_config['scope'])) { 70 | $options['conditions'] = array_merge($options['conditions'], $this->_config['scope']); 71 | } 72 | if (!empty($this->_config['contain'])) { 73 | $options['contain'] = $this->_config['contain']; 74 | } 75 | 76 | $finder = $this->_config['finder']; 77 | if (is_array($finder)) { 78 | $options += current($finder); 79 | $finder = key($finder); 80 | } 81 | 82 | if (!isset($options['username'])) { 83 | $options['username'] = $username; 84 | } 85 | 86 | return $table->find($finder, ...$options); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Auth/PasswordHasherFactory.php: -------------------------------------------------------------------------------- 1 | |string $passwordHasher Name of the password hasher or an array with 32 | * at least the key `className` set to the name of the class to use 33 | * @throws \RuntimeException If password hasher class not found or 34 | * it does not extend {@link \TinyAuth\Auth\AbstractPasswordHasher} 35 | * @return \TinyAuth\Auth\AbstractPasswordHasher Password hasher instance 36 | */ 37 | public static function build($passwordHasher): AbstractPasswordHasher { 38 | $config = []; 39 | if (is_string($passwordHasher)) { 40 | $class = $passwordHasher; 41 | } else { 42 | $class = $passwordHasher['className']; 43 | $config = $passwordHasher; 44 | unset($config['className']); 45 | } 46 | 47 | $className = App::className($class, 'Auth', 'PasswordHasher'); 48 | if ($className === null) { 49 | throw new RuntimeException(sprintf('Password hasher class "%s" was not found.', $class)); 50 | } 51 | 52 | $hasher = new $className($config); 53 | if (!($hasher instanceof AbstractPasswordHasher)) { 54 | throw new RuntimeException('Password hasher must extend AbstractPasswordHasher class.'); 55 | } 56 | 57 | return $hasher; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Auth/Storage/MemoryStorage.php: -------------------------------------------------------------------------------- 1 | _user; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function write($user): void { 50 | $this->_user = $user; 51 | } 52 | 53 | /** 54 | * @inheritDoc 55 | */ 56 | public function delete(): void { 57 | $this->_user = null; 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | public function redirectUrl($url = null) { 64 | if ($url === null) { 65 | return $this->_redirectUrl; 66 | } 67 | 68 | if ($url === false) { 69 | $this->_redirectUrl = null; 70 | 71 | return null; 72 | } 73 | 74 | $this->_redirectUrl = $url; 75 | 76 | return null; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Auth/Storage/SessionStorage.php: -------------------------------------------------------------------------------- 1 | 57 | */ 58 | protected $_defaultConfig = [ 59 | 'key' => 'Auth.User', 60 | 'redirect' => 'Auth.redirect', 61 | ]; 62 | 63 | /** 64 | * Constructor. 65 | * 66 | * @param \Cake\Http\ServerRequest $request Request instance. 67 | * @param \Cake\Http\Response $response Response instance. 68 | * @param array $config Configuration list. 69 | */ 70 | public function __construct(ServerRequest $request, Response $response, array $config = []) { 71 | $this->_session = $request->getSession(); 72 | $this->setConfig($config); 73 | } 74 | 75 | /** 76 | * Read user record from session. 77 | * 78 | * @psalm-suppress InvalidReturnType 79 | * @return \ArrayAccess|array|null User record if available else null. 80 | */ 81 | public function read() { 82 | if ($this->_user !== null) { 83 | return $this->_user ?: null; 84 | } 85 | 86 | /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ 87 | $this->_user = $this->_session->read($this->_config['key']) ?: false; 88 | 89 | /** @psalm-suppress InvalidReturnStatement */ 90 | return $this->_user ?: null; 91 | } 92 | 93 | /** 94 | * Write user record to session. 95 | * 96 | * The session id is also renewed to help mitigate issues with session replays. 97 | * 98 | * @param \ArrayAccess|array $user User record. 99 | * @return void 100 | */ 101 | public function write($user): void { 102 | $this->_user = $user; 103 | 104 | $this->_session->renew(); 105 | $this->_session->write($this->_config['key'], $user); 106 | } 107 | 108 | /** 109 | * Delete user record from session. 110 | * 111 | * The session id is also renewed to help mitigate issues with session replays. 112 | * 113 | * @return void 114 | */ 115 | public function delete(): void { 116 | $this->_user = false; 117 | 118 | $this->_session->delete($this->_config['key']); 119 | $this->_session->renew(); 120 | } 121 | 122 | /** 123 | * @inheritDoc 124 | */ 125 | public function redirectUrl($url = null) { 126 | if ($url === null) { 127 | return $this->_session->read($this->_config['redirect']); 128 | } 129 | 130 | if ($url === false) { 131 | $this->_session->delete($this->_config['redirect']); 132 | 133 | return null; 134 | } 135 | 136 | $this->_session->write($this->_config['redirect'], $url); 137 | 138 | return null; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/Auth/Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | ['Tools.Tiny'] 20 | * 21 | * Or with admin prefix protection only 22 | * 'authorize' => ['Tools.Tiny' => ['allowLoggedIn' => true]]; 23 | * 24 | * @author Mark Scherer 25 | * @license MIT 26 | */ 27 | class TinyAuthorize extends BaseAuthorize { 28 | 29 | use AclTrait; 30 | use AllowTrait; 31 | 32 | /** 33 | * @param \Cake\Controller\ComponentRegistry $registry 34 | * @param array $config 35 | * @throws \Cake\Core\Exception\CakeException 36 | */ 37 | public function __construct(ComponentRegistry $registry, array $config = []) { 38 | $config += Config::all(); 39 | 40 | parent::__construct($registry, $config); 41 | } 42 | 43 | /** 44 | * Authorizes a user using the AclComponent. 45 | * 46 | * Allows single or multi role based authorization 47 | * 48 | * Examples: 49 | * - User HABTM Roles (Role array in User array) 50 | * - User belongsTo Roles (role_id in User array) 51 | * 52 | * @param \ArrayObject|array $user The user to authorize 53 | * @param \Cake\Http\ServerRequest $request The request needing authorization. 54 | * @return bool Success 55 | */ 56 | public function authorize($user, ServerRequest $request): bool { 57 | return $this->_checkUser((array)$user, $request->getAttribute('params')); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Auth/WeakPasswordHasher.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected array $_defaultConfig = [ 37 | 'hashType' => null, 38 | ]; 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public function __construct(array $config = []) { 44 | if (Configure::read('debug')) { 45 | Debugger::checkSecurityKeys(); 46 | } 47 | 48 | parent::__construct($config); 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function hash(string $password): string { 55 | return Security::hash($password, $this->_config['hashType'], true); 56 | } 57 | 58 | /** 59 | * Check hash. Generate hash for user provided password and check against existing hash. 60 | * 61 | * @param string $password Plain text password to hash. 62 | * @param string $hashedPassword Existing hashed password. 63 | * @return bool True if hashes match else false. 64 | */ 65 | public function check(string $password, string $hashedPassword): bool { 66 | return $hashedPassword === $this->hash($password); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Authenticator/PrimaryKeySessionAuthenticator.php: -------------------------------------------------------------------------------- 1 | $config 24 | */ 25 | public function __construct(IdentifierInterface $identifier, array $config = []) { 26 | $config += [ 27 | 'identifierKey' => 'key', 28 | 'idField' => 'id', 29 | 'cache' => false, // `true` to activate caching layer 30 | ]; 31 | 32 | parent::__construct($identifier, $config); 33 | } 34 | 35 | /** 36 | * Authenticate a user using session data. 37 | * 38 | * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with. 39 | * @return \Authentication\Authenticator\ResultInterface 40 | */ 41 | public function authenticate(ServerRequestInterface $request): ResultInterface { 42 | $sessionKey = $this->getConfig('sessionKey'); 43 | /** @var \Cake\Http\Session $session */ 44 | $session = $request->getAttribute('session'); 45 | 46 | $userId = $session->read($sessionKey); 47 | if (!$userId) { 48 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); 49 | } 50 | 51 | if (!is_scalar($userId)) { 52 | // Maybe during migration? Let's remove this old one then 53 | $session->delete($sessionKey); 54 | 55 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); 56 | } 57 | 58 | if ($this->getConfig('cache')) { 59 | $user = SessionCache::read((string)$userId); 60 | if ($user) { 61 | return new Result($user, Result::SUCCESS); 62 | } 63 | } 64 | 65 | $user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]); 66 | if (!$user) { 67 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); 68 | } 69 | 70 | if ($this->getConfig('cache')) { 71 | SessionCache::write((string)$userId, $user); 72 | } 73 | 74 | return new Result($user, Result::SUCCESS); 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array { 81 | $sessionKey = $this->getConfig('sessionKey'); 82 | /** @var \Cake\Http\Session $session */ 83 | $session = $request->getAttribute('session'); 84 | 85 | if (!$session->check($sessionKey)) { 86 | $session->renew(); 87 | $session->write($sessionKey, $identity[$this->getConfig('idField')]); 88 | } 89 | 90 | return [ 91 | 'request' => $request, 92 | 'response' => $response, 93 | ]; 94 | } 95 | 96 | /** 97 | * @inheritDoc 98 | */ 99 | public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array { 100 | if ($this->getConfig('cache')) { 101 | $sessionKey = $this->getConfig('sessionKey'); 102 | /** @var \Cake\Http\Session $session */ 103 | $session = $request->getAttribute('session'); 104 | $userId = $session->read($sessionKey); 105 | if (is_scalar($userId)) { 106 | SessionCache::delete((string)$userId); 107 | } 108 | } 109 | 110 | return parent::clearIdentity($request, $response); 111 | } 112 | 113 | /** 114 | * Impersonates a user 115 | * 116 | * @param \Psr\Http\Message\ServerRequestInterface $request The request 117 | * @param \Psr\Http\Message\ResponseInterface $response The response 118 | * @param \ArrayAccess $impersonator User who impersonates 119 | * @param \ArrayAccess $impersonated User impersonated 120 | * @return array 121 | */ 122 | public function impersonate( 123 | ServerRequestInterface $request, 124 | ResponseInterface $response, 125 | ArrayAccess $impersonator, 126 | ArrayAccess $impersonated, 127 | ): array { 128 | $sessionKey = $this->getConfig('sessionKey'); 129 | $impersonateSessionKey = $this->getConfig('impersonateSessionKey'); 130 | /** @var \Cake\Http\Session $session */ 131 | $session = $request->getAttribute('session'); 132 | if ($session->check($impersonateSessionKey)) { 133 | throw new UnauthorizedException( 134 | 'You are impersonating a user already. ' . 135 | 'Stop the current impersonation before impersonating another user.', 136 | ); 137 | } 138 | $session->write($impersonateSessionKey, $impersonator[$this->getConfig('idField')]); 139 | $session->write($sessionKey, $impersonated[$this->getConfig('idField')]); 140 | $this->setConfig('identify', true); 141 | 142 | return [ 143 | 'request' => $request, 144 | 'response' => $response, 145 | ]; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/Command/TinyAuthAddCommand.php: -------------------------------------------------------------------------------- 1 | _getAdder(); 26 | 27 | $controller = $args->getArgument('controller'); 28 | if ($controller === null) { 29 | $controllerNames = $adder->controllers($args); 30 | $io->out('Select a controller:'); 31 | foreach ($controllerNames as $controllerName) { 32 | $io->out(' - ' . $controllerName); 33 | } 34 | while (!$controller || !in_array($controller, $controllerNames, true)) { 35 | $controller = $io->ask('Controller name'); 36 | } 37 | } 38 | 39 | $action = $args->getArgument('action') ?: '*'; 40 | $roles = $args->getArgument('roles') ?: '*'; 41 | $roles = array_map('trim', explode(',', $roles)); 42 | $adder->addAcl($controller, $action, $roles, $args, $io); 43 | $io->out('Controllers and ACL synced.'); 44 | 45 | return static::CODE_SUCCESS; 46 | } 47 | 48 | /** 49 | * @return \TinyAuth\Sync\Adder 50 | */ 51 | protected function _getAdder() { 52 | return new Adder(); 53 | } 54 | 55 | /** 56 | * Gets the option parser instance and configures it. 57 | * 58 | * @param \Cake\Console\ConsoleOptionParser $parser The parser to build 59 | * @return \Cake\Console\ConsoleOptionParser 60 | */ 61 | protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { 62 | $roles = $this->_getAvailableRoles(); 63 | 64 | $parser->setDescription( 65 | 'Get the list of controllers and make sure, they are synced into the ACL file.', 66 | )->addArgument('controller', [ 67 | 'help' => 'Controller name (Plugin.Prefix/Name) without Controller suffix.', 68 | 'required' => false, 69 | ])->addArgument('action', [ 70 | 'help' => 'Action name (camelCased or under_scored), defaults to `*` (all).', 71 | 'required' => false, 72 | ])->addArgument('roles', [ 73 | 'help' => 'Role names, comma separated, e.g. `user,admin`, defaults to `*` (all).' . ($roles ? PHP_EOL . 'Available roles: ' . implode(', ', $roles) . '.' : ''), 74 | 'required' => false, 75 | ])->addOption('plugin', [ 76 | 'short' => 'p', 77 | 'help' => 'Plugin, use `all` to include all loaded plugins.', 78 | 'default' => null, 79 | ])->addOption('dry-run', [ 80 | 'short' => 'd', 81 | 'help' => 'Dry Run (only output, do not modify INI files).', 82 | 'boolean' => true, 83 | ]); 84 | 85 | return $parser; 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | protected function _getAvailableRoles() { 92 | $roles = (new TinyAuth())->getAvailableRoles(); 93 | 94 | return array_keys($roles); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/Command/TinyAuthSyncCommand.php: -------------------------------------------------------------------------------- 1 | _getSyncer(); 26 | $syncer->syncAcl($args, $io); 27 | $io->out('Controllers and ACL synced.'); 28 | 29 | return static::CODE_SUCCESS; 30 | } 31 | 32 | /** 33 | * @return \TinyAuth\Sync\Syncer 34 | */ 35 | protected function _getSyncer() { 36 | return new Syncer(); 37 | } 38 | 39 | /** 40 | * Gets the option parser instance and configures it. 41 | * 42 | * @param \Cake\Console\ConsoleOptionParser $parser The parser to build 43 | * @return \Cake\Console\ConsoleOptionParser 44 | */ 45 | protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { 46 | $roles = $this->_getAvailableRoles(); 47 | 48 | $parser->setDescription( 49 | 'Get the list of controllers and make sure, they are synced into the ACL file.', 50 | )->addArgument('roles', [ 51 | 'help' => 'Role names, comma separated, e.g. `user,admin`.' . ($roles ? PHP_EOL . 'Available roles: ' . implode(', ', $roles) . '.' : ''), 52 | 'required' => true, 53 | ])->addOption('plugin', [ 54 | 'short' => 'p', 55 | 'help' => 'Plugin, use `all` to include all loaded plugins.', 56 | 'default' => null, 57 | ])->addOption('dry-run', [ 58 | 'short' => 'd', 59 | 'help' => 'Dry Run (only output, do not modify INI files).', 60 | 'boolean' => true, 61 | ]); 62 | 63 | return $parser; 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | protected function _getAvailableRoles() { 70 | $roles = (new TinyAuth())->getAvailableRoles(); 71 | 72 | return array_keys($roles); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Controller/Component/AuthComponent.php: -------------------------------------------------------------------------------- 1 | $config 25 | * @throws \RuntimeException 26 | */ 27 | public function __construct(ComponentRegistry $registry, array $config = []) { 28 | $config += Config::all(); 29 | if ($config && empty($config['className'])) { 30 | $config['className'] = 'TinyAuth.Auth'; 31 | } 32 | 33 | parent::__construct($registry, $config); 34 | if ($registry->has('Authentication') && get_class($registry->get('Authentication')) === AuthenticationComponent::class) { 35 | throw new RuntimeException('You cannot use new TinyAuth.Authentication component and this TinyAuth.Auth component together.'); 36 | } 37 | if ($registry->has('Authorization') && get_class($registry->get('Authorization')) === AuthorizationComponent::class) { 38 | throw new RuntimeException('You cannot use new TinyAuth.Authorization component and this TinyAuth.Auth component together.'); 39 | } 40 | } 41 | 42 | /** 43 | * @param array $config The config data. 44 | * @return void 45 | */ 46 | public function initialize(array $config): void { 47 | parent::initialize($config); 48 | 49 | $params = $this->_registry->getController()->getRequest()->getAttribute('params'); 50 | $this->_prepareAuthentication($params); 51 | } 52 | 53 | /** 54 | * @param array $params 55 | * @return void 56 | */ 57 | protected function _prepareAuthentication(array $params) { 58 | $rule = $this->_getAllowRule($params); 59 | if (!$rule) { 60 | return; 61 | } 62 | 63 | if (in_array('*', $rule['allow'], true)) { 64 | $this->allow(); 65 | } elseif (!empty($rule['allow'])) { 66 | $this->allow($rule['allow']); 67 | } 68 | if (in_array('*', $rule['deny'], true)) { 69 | $this->deny(); 70 | } elseif (!empty($rule['deny'])) { 71 | $this->deny($rule['deny']); 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Controller/Component/AuthUserComponent.php: -------------------------------------------------------------------------------- 1 | $config 27 | */ 28 | public function __construct(ComponentRegistry $registry, array $config = []) { 29 | $config += Config::all(); 30 | 31 | parent::__construct($registry, $config); 32 | } 33 | 34 | /** 35 | * @param \Cake\Event\EventInterface $event 36 | * @return void 37 | */ 38 | public function beforeRender(EventInterface $event): void { 39 | /** @var \Cake\Controller\Controller $controller */ 40 | $controller = $event->getSubject(); 41 | 42 | $authUser = $this->_getUser(); 43 | $controller->set('_authUser', $authUser); 44 | } 45 | 46 | /** 47 | * @return \ArrayAccess|array|null 48 | */ 49 | public function identity(): ArrayAccess|array|null { 50 | /** @var \Authorization\Identity|\Authentication\Identity|null $identity */ 51 | $identity = $this->getController()->getRequest()->getAttribute('identity'); 52 | if (!$identity) { 53 | return null; 54 | } 55 | 56 | /** @var \ArrayAccess|array|null */ 57 | return $identity->getOriginalData(); 58 | } 59 | 60 | /** 61 | * This is only for usage with already logged in persons as this uses the ACL (not allow) data. 62 | * 63 | * @param array $url 64 | * @return bool 65 | */ 66 | public function hasAccess(array $url): bool { 67 | $params = $this->getController()->getRequest()->getAttribute('params'); 68 | $url += [ 69 | 'prefix' => !empty($params['prefix']) ? $params['prefix'] : null, 70 | 'plugin' => !empty($params['plugin']) ? $params['plugin'] : null, 71 | 'controller' => $params['controller'], 72 | 'action' => 'index', 73 | ]; 74 | 75 | return $this->_checkUser($this->_getUser(), $url); 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | protected function _getUser(): array { 82 | if (class_exists(Identity::class)) { 83 | $identity = $this->identity(); 84 | 85 | return $identity ? $this->_toArray($identity) : []; 86 | } 87 | 88 | // We skip for new plugin(s) 89 | if ($this->getController()->components()->has('Authentication')) { 90 | return []; 91 | } 92 | 93 | // Fallback to old Auth style 94 | if (!$this->getController()->components()->has('Auth')) { 95 | $this->getController()->loadComponent('TinyAuth.Auth'); 96 | } 97 | 98 | /** @phpstan-ignore property.notFound */ 99 | return (array)$this->getController()->Auth->user(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/Controller/Component/AuthenticationComponent.php: -------------------------------------------------------------------------------- 1 | $config 28 | * @throws \RuntimeException 29 | */ 30 | public function __construct(ComponentRegistry $registry, array $config = []) { 31 | $config += Config::all(); 32 | 33 | parent::__construct($registry, $config); 34 | 35 | if ($registry->has('Auth') && get_class($registry->get('Auth')) === AuthComponent::class) { 36 | throw new RuntimeException('You cannot use TinyAuth.Authentication component and former TinyAuth.Auth component together.'); 37 | } 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | * 43 | * @return void 44 | */ 45 | public function startup(): void { 46 | $this->_prepareAuthentication(); 47 | 48 | parent::startup(); 49 | } 50 | 51 | /** 52 | * Checks if a given URL is public. 53 | * 54 | * If no URL is given it will default to the current request URL. 55 | * 56 | * @param array $url 57 | * @throws \Cake\Core\Exception\CakeException 58 | * @return bool 59 | */ 60 | public function isPublic(array $url = []) { 61 | if (!$url) { 62 | $url = $this->getController()->getRequest()->getAttribute('params'); 63 | } 64 | 65 | if (isset($url['_name'])) { 66 | //throw MissingRouteException if necessary 67 | Router::url($url); 68 | $routes = Router::getRouteCollection()->named(); 69 | $defaults = $routes[$url['_name']]->defaults; 70 | if (!isset($defaults['action']) || !isset($defaults['controller'])) { 71 | throw new CakeException('Controller or action name could not be null.'); 72 | } 73 | $url = [ 74 | 'prefix' => !empty($defaults['prefix']) ? $defaults['prefix'] : null, 75 | 'plugin' => !empty($defaults['plugin']) ? $defaults['plugin'] : null, 76 | 'controller' => $defaults['controller'], 77 | 'action' => $defaults['action'], 78 | ]; 79 | } else { 80 | $params = $this->getController()->getRequest()->getAttribute('params'); 81 | $url += [ 82 | 'prefix' => !empty($params['prefix']) ? $params['prefix'] : null, 83 | 'plugin' => !empty($params['plugin']) ? $params['plugin'] : null, 84 | 'controller' => $params['controller'], 85 | 'action' => 'index', 86 | ]; 87 | } 88 | 89 | $rule = $this->_getAllowRule($url); 90 | 91 | return $this->_isActionAllowed($rule, $url['action']); 92 | } 93 | 94 | /** 95 | * @return void 96 | */ 97 | protected function _prepareAuthentication() { 98 | $params = $this->_registry->getController()->getRequest()->getAttribute('params'); 99 | if (!isset($params['plugin'])) { 100 | $params['plugin'] = null; 101 | } 102 | if (!isset($params['prefix'])) { 103 | $params['prefix'] = null; 104 | } 105 | 106 | $rule = $this->_getAllowRule($params); 107 | 108 | if (!$rule) { 109 | return; 110 | } 111 | 112 | if (in_array('*', $rule['deny'], true)) { 113 | return; 114 | } 115 | 116 | $allowed = $this->unauthenticatedActions; 117 | if (in_array('*', $rule['allow'], true)) { 118 | $allowed = $this->_getAllActions(); 119 | } elseif (!empty($rule['allow'])) { 120 | $allowed = array_merge($allowed, $rule['allow']); 121 | } 122 | if (!empty($rule['deny'])) { 123 | $allowed = array_diff($allowed, $rule['deny']); 124 | } 125 | 126 | if (!$allowed) { 127 | return; 128 | } 129 | 130 | $this->allowUnauthenticated($allowed); 131 | } 132 | 133 | /** 134 | * @return array 135 | */ 136 | protected function _getAllActions() { 137 | $controller = $this->_registry->getController(); 138 | 139 | return get_class_methods($controller); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/Controller/Component/AuthorizationComponent.php: -------------------------------------------------------------------------------- 1 | $config 33 | * @throws \RuntimeException 34 | */ 35 | public function __construct(ComponentRegistry $registry, array $config = []) { 36 | $config += Config::all(); 37 | 38 | parent::__construct($registry, $config); 39 | 40 | if ($registry->has('Auth') && get_class($registry->get('Auth')) === AuthComponent::class) { 41 | throw new RuntimeException('You cannot use TinyAuth.Authorization component and former TinyAuth.Auth component together.'); 42 | } 43 | 44 | if ($registry->getController()->components()->has('Authentication')) { 45 | /** @var \TinyAuth\Controller\Component\AuthenticationComponent $authentication */ 46 | $authentication = $registry->getController()->components()->get('Authentication'); 47 | $this->_authentication = $authentication; 48 | } 49 | } 50 | 51 | /** 52 | * Action authorization handler. 53 | * 54 | * Checks identity and model authorization. 55 | * 56 | * @return void 57 | */ 58 | public function authorizeAction(): void { 59 | if ($this->_isUnauthenticatedAction()) { 60 | $this->skipAuthorization(); 61 | 62 | return; 63 | } 64 | 65 | $request = $this->getController()->getRequest(); 66 | 67 | $action = $request->getParam('action'); 68 | $skipAuthorization = $this->checkAction($action, 'skipAuthorization'); 69 | if ($skipAuthorization) { 70 | $this->skipAuthorization(); 71 | 72 | return; 73 | } 74 | 75 | $this->authorize($request, 'access'); 76 | 77 | parent::authorizeAction(); 78 | } 79 | 80 | /** 81 | * Is public already thanks to Authentication component. 82 | * 83 | * @return bool 84 | */ 85 | protected function _isUnauthenticatedAction() { 86 | if ($this->_authentication === null) { 87 | return false; 88 | } 89 | 90 | $unauthenticatedActions = $this->_authentication->getUnauthenticatedActions(); 91 | $request = $this->getController()->getRequest(); 92 | 93 | $action = $request->getParam('action'); 94 | 95 | return in_array($action, $unauthenticatedActions, true); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Middleware/RequestAuthorizationMiddleware.php: -------------------------------------------------------------------------------- 1 | $config Configuration options 33 | */ 34 | public function __construct(array $config = []) { 35 | $config += Config::all(); 36 | 37 | parent::__construct($config); 38 | } 39 | 40 | /** 41 | * @param \Psr\Http\Message\ServerRequestInterface $request 42 | * @param \Psr\Http\Server\RequestHandlerInterface $handler 43 | * @throws \Authorization\Exception\ForbiddenException 44 | * @return \Psr\Http\Message\ResponseInterface 45 | */ 46 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { 47 | $params = $request->getAttribute('params'); 48 | $rule = $this->_getAllowRule($params); 49 | 50 | $service = $this->getServiceFromRequest($request); 51 | if ($this->_isActionAllowed($rule, $params['action'])) { 52 | $service->skipAuthorization(); 53 | 54 | return $handler->handle($request); 55 | } 56 | 57 | $identity = $request->getAttribute($this->getConfig('identityAttribute')); 58 | 59 | $can = $service->can($identity, $this->getConfig('method'), $request); 60 | try { 61 | if (!$can) { 62 | $result = new Result($can, 'Can not ' . $this->getConfig('method') . ' request'); 63 | 64 | throw new ForbiddenException($result, [$this->getConfig('method'), $request->getRequestTarget()]); 65 | } 66 | } catch (Exception $exception) { 67 | return $this->handleException($exception, $request, $this->getConfig('unauthorizedHandler')); 68 | } 69 | 70 | return $handler->handle($request); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Middleware/UnauthorizedHandler/ForbiddenCakeRedirectHandler.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | protected array $defaultOptions = [ 29 | 'exceptions' => [ 30 | ForbiddenException::class, 31 | ], 32 | 'url' => [ 33 | 'controller' => 'Users', 34 | 'action' => 'login', 35 | ], 36 | 'queryParam' => 'redirect', 37 | 'statusCode' => 302, 38 | 'unauthorizedMessage' => null, 39 | ]; 40 | 41 | /** 42 | * @param \Authorization\Exception\Exception $exception 43 | * @param \Psr\Http\Message\ServerRequestInterface $request 44 | * @param array $options 45 | * @return \Psr\Http\Message\ResponseInterface 46 | */ 47 | public function handle(Exception $exception, ServerRequestInterface $request, array $options = []): ResponseInterface { 48 | $params = (array)$request->getAttribute('params'); 49 | if (!empty($params['_ext']) && $params['_ext'] !== 'html') { 50 | throw $exception; 51 | } 52 | 53 | $response = parent::handle($exception, $request, $options); 54 | 55 | $message = $options['unauthorizedMessage'] ?? __('You are not authorized to access that location.'); 56 | if ($message) { 57 | /** @var \Cake\Http\ServerRequest $request */ 58 | $request->getFlash()->error($message); 59 | } 60 | 61 | return $response; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Middleware/UnauthorizedHandler/ForbiddenRedirectHandler.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | protected array $defaultOptions = [ 29 | 'exceptions' => [ 30 | ForbiddenException::class, 31 | ], 32 | 'url' => '/', 33 | 'queryParam' => 'redirect', 34 | 'statusCode' => 302, 35 | 'unauthorizedMessage' => null, 36 | ]; 37 | 38 | /** 39 | * @param \Authorization\Exception\Exception $exception 40 | * @param \Psr\Http\Message\ServerRequestInterface $request 41 | * @param array $options 42 | * @return \Psr\Http\Message\ResponseInterface 43 | */ 44 | public function handle(Exception $exception, ServerRequestInterface $request, array $options = []): ResponseInterface { 45 | $params = (array)$request->getAttribute('params'); 46 | if (!empty($params['_ext']) && $params['_ext'] !== 'html') { 47 | throw $exception; 48 | } 49 | 50 | $response = parent::handle($exception, $request, $options); 51 | 52 | $message = $options['unauthorizedMessage'] ?? __('You are not authorized to access that location.'); 53 | if ($message) { 54 | /** @var \Cake\Http\ServerRequest $request */ 55 | $request->getFlash()->error($message); 56 | } 57 | 58 | return $response; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/Panel/AuthPanel.php: -------------------------------------------------------------------------------- 1 | setConfig(Config::all()); 53 | } 54 | 55 | /** 56 | * Data collection callback. 57 | * 58 | * @param \Cake\Event\EventInterface $event The shutdown event. 59 | * 60 | * @return void 61 | */ 62 | public function shutdown(EventInterface $event): void { 63 | /** @var \Cake\Controller\Controller $controller */ 64 | $controller = $event->getSubject(); 65 | $request = $controller->getRequest(); 66 | 67 | $params = $this->_getParams($request->getAttribute('params')); 68 | $availableRoles = (new TinyAuth())->getAvailableRoles(); 69 | $data = [ 70 | 'params' => $params, 71 | 'path' => $this->_getPath($params), 72 | 'availableRoles' => $availableRoles, 73 | ]; 74 | 75 | $rule = $this->_getAllowRule($params); 76 | $this->isPublic = $this->_isActionAllowed($rule, $params['action']); 77 | 78 | if (!$controller->components()->has('AuthUser')) { 79 | $controller->loadComponent('TinyAuth.AuthUser'); 80 | } 81 | 82 | /** @var \TinyAuth\Controller\Component\AuthUserComponent $authUserComponent */ 83 | $authUserComponent = $controller->components()->get('AuthUser'); 84 | $user = $authUserComponent->user(); 85 | $data['user'] = $user; 86 | 87 | try { 88 | $roles = $authUserComponent->roles(); 89 | } catch (Exception) { 90 | $roles = []; 91 | } 92 | $data['roles'] = $roles; 93 | 94 | $access = []; 95 | foreach ($availableRoles as $role => $id) { 96 | if ($user) { 97 | $tmpUser = $this->_injectRole($user, $role, $id); 98 | } else { 99 | $tmpUser = $this->_generateUser($role, $id); 100 | } 101 | $access[$role] = $this->_checkUser($tmpUser, $params); 102 | } 103 | 104 | $data['config'] = $authUserComponent->getConfig(); 105 | $data['access'] = $access; 106 | 107 | $data['identity'] = $request->getAttribute('identity'); 108 | 109 | /** @var \Authentication\AuthenticationService|null $auth */ 110 | $auth = $request->getAttribute('authentication'); 111 | $data['authenticationProvider'] = $auth ? $auth->getAuthenticationProvider() : null; 112 | $data['identificationProvider'] = $auth ? $auth->getIdentificationProvider() : null; 113 | 114 | $this->_data = $data; 115 | } 116 | 117 | /** 118 | * Get the data for this panel 119 | * 120 | * @return array 121 | */ 122 | public function data(): array { 123 | $data = [ 124 | 'isPublic' => $this->isPublic, 125 | ]; 126 | 127 | return $this->_data + $data; 128 | } 129 | 130 | /** 131 | * Get the summary data for a panel. 132 | * 133 | * This data is displayed in the toolbar even when the panel is collapsed. 134 | * 135 | * @return string 136 | */ 137 | public function summary(): string { 138 | if ($this->isPublic === null) { 139 | return ''; 140 | } 141 | 142 | return $this->isPublic ? static::ICON_PUBLIC : static::ICON_RESTRICTED; // For now no HTML possible. 143 | } 144 | 145 | /** 146 | * @param array $user 147 | * @param string $role 148 | * @param string|int $id 149 | * 150 | * @return array 151 | */ 152 | protected function _injectRole(array $user, $role, $id) { 153 | if (!$this->getConfig('multiRole')) { 154 | $user[$this->getConfig('roleColumn')] = $id; 155 | 156 | return $user; 157 | } 158 | 159 | if (isset($user[$this->getConfig('rolesTable')])) { 160 | $user[$this->getConfig('rolesTable')] = [$role => $id]; 161 | 162 | return $user; 163 | } 164 | 165 | $pivotTableName = $this->_pivotTableName(); 166 | if (isset($user[$pivotTableName])) { 167 | $user[$pivotTableName] = [$role => $id]; 168 | 169 | return $user; 170 | } 171 | 172 | //TODO: other edge cases? 173 | 174 | return $user; 175 | } 176 | 177 | /** 178 | * @param string $role 179 | * @param string|int $id 180 | * 181 | * @return array 182 | */ 183 | protected function _generateUser($role, $id): array { 184 | $user = [ 185 | 'id' => 0, 186 | ]; 187 | if (!$this->getConfig('multiRole')) { 188 | $user[$this->getConfig('roleColumn')] = $id; 189 | 190 | return $user; 191 | } 192 | 193 | $user[$this->getConfig('rolesTable')] = [$role => $id]; 194 | 195 | return $user; 196 | } 197 | 198 | /** 199 | * @param array $params 200 | * 201 | * @return array 202 | */ 203 | protected function _getParams(array $params) { 204 | $params += [ 205 | 'prefix' => null, 206 | 'plugin' => null, 207 | ]; 208 | unset($params['isAjax']); 209 | unset($params['_csrfToken']); 210 | unset($params['_Token']); 211 | 212 | return $params; 213 | } 214 | 215 | /** 216 | * @param array $params 217 | * 218 | * @return string 219 | */ 220 | protected function _getPath(array $params) { 221 | $path = $params['controller']; 222 | if ($params['prefix']) { 223 | $path = $params['prefix'] . '/' . $path; 224 | } 225 | if ($params['plugin']) { 226 | $path = $params['plugin'] . '.' . $path; 227 | } 228 | 229 | return $path; 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/Policy/RequestPolicy.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | protected array $_defaultConfig = [ 27 | ]; 28 | 29 | /** 30 | * @param array $config 31 | */ 32 | public function __construct(array $config = []) { 33 | $config += Config::all(); 34 | 35 | $this->setConfig($config); 36 | } 37 | 38 | /** 39 | * Method to check if the request can be accessed 40 | * 41 | * @param \Authorization\IdentityInterface|null $identity Identity 42 | * @param \Cake\Http\ServerRequest $request Request 43 | * @return bool 44 | */ 45 | public function canAccess(?IdentityInterface $identity, ServerRequest $request): bool { 46 | $params = $request->getAttribute('params'); 47 | $user = []; 48 | if ($identity) { 49 | $data = $identity->getOriginalData(); 50 | $user = ($data instanceof ArrayAccess && method_exists($data, 'toArray')) ? $data->toArray() : (array)$data; 51 | } 52 | 53 | return $this->_checkUser($user, $params); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Sync/Adder.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | protected array $config; 19 | 20 | public function __construct() { 21 | $defaults = [ 22 | 'aclFile' => 'auth_acl.ini', 23 | 'aclFilePath' => null, 24 | ]; 25 | $this->config = (array)Configure::read('TinyAuth') + $defaults; 26 | } 27 | 28 | /** 29 | * @var array|null 30 | */ 31 | protected $authAllow; 32 | 33 | /** 34 | * @param string $controller 35 | * @param string $action 36 | * @param array $roles 37 | * @param \Cake\Console\Arguments $args 38 | * @param \Cake\Console\ConsoleIo $io 39 | * 40 | * @return void 41 | */ 42 | public function addAcl(string $controller, string $action, array $roles, Arguments $args, ConsoleIo $io) { 43 | $path = $this->config['aclFilePath'] ?: ROOT . DS . 'config' . DS; 44 | $file = $path . $this->config['aclFile']; 45 | $content = Utility::parseFile($file); 46 | 47 | if (isset($content[$controller][$action]) || isset($content[$controller]['*'])) { 48 | $mappedRoles = $content[$controller][$action] ?? $content[$controller]['*']; 49 | if (strpos($mappedRoles, ',') !== false) { 50 | $mappedRoles = array_map('trim', explode(',', $mappedRoles)); 51 | } 52 | $this->checkRoles($roles, (array)$mappedRoles, $io); 53 | } 54 | 55 | $io->info('Add [' . $controller . '] ' . $action . ' = ' . implode(', ', $roles)); 56 | $content[$controller][$action] = implode(', ', $roles); 57 | 58 | if ($args->getOption('dry-run')) { 59 | $string = Utility::buildIniString($content); 60 | 61 | if ($args->getOption('verbose')) { 62 | $io->info('=== ' . $this->config['aclFile'] . ' ==='); 63 | $io->info($string); 64 | $io->info('=== ' . $this->config['aclFile'] . ' end ==='); 65 | } 66 | 67 | return; 68 | } 69 | 70 | Utility::generateFile($file, $content); 71 | } 72 | 73 | /** 74 | * @param string|null $plugin 75 | * @return array 76 | */ 77 | protected function _getControllers($plugin) { 78 | if ($plugin === 'all') { 79 | $plugins = Plugin::loaded(); 80 | 81 | $controllers = []; 82 | foreach ($plugins as $plugin) { 83 | $controllers = array_merge($controllers, $this->_getControllers($plugin)); 84 | } 85 | 86 | return $controllers; 87 | } 88 | 89 | $folders = App::classPath('Controller', $plugin); 90 | 91 | $controllers = []; 92 | foreach ($folders as $folder) { 93 | $controllers = array_merge($controllers, $this->_parseControllers($folder, $plugin)); 94 | } 95 | 96 | return $controllers; 97 | } 98 | 99 | /** 100 | * @param string $folder Path 101 | * @param string|null $plugin 102 | * @param string|null $prefix 103 | * 104 | * @return array 105 | */ 106 | protected function _parseControllers($folder, $plugin, $prefix = null) { 107 | $folderContent = (new Folder($folder))->read(Folder::SORT_NAME, true); 108 | 109 | $controllers = []; 110 | foreach ($folderContent[1] as $file) { 111 | $className = pathinfo($file, PATHINFO_FILENAME); 112 | 113 | if (!preg_match('#^(.+)Controller$#', $className, $matches)) { 114 | continue; 115 | } 116 | $name = $matches[1]; 117 | if ($matches[1] === 'App') { 118 | continue; 119 | } 120 | 121 | if ($this->_noAuthenticationNeeded($name, $plugin, $prefix)) { 122 | continue; 123 | } 124 | 125 | $controllers[] = ($plugin ? $plugin . '.' : '') . ($prefix ? $prefix . '/' : '') . $name; 126 | } 127 | 128 | foreach ($folderContent[0] as $subFolder) { 129 | $prefixes = (array)Configure::read('TinyAuth.prefixes') ?: null; 130 | 131 | if ($prefixes !== null && !in_array($subFolder, $prefixes, true)) { 132 | continue; 133 | } 134 | 135 | $controllers = array_merge($controllers, $this->_parseControllers($folder . $subFolder . DS, $plugin, $subFolder)); 136 | } 137 | 138 | return $controllers; 139 | } 140 | 141 | /** 142 | * @param string $name 143 | * @param string|null $plugin 144 | * @param string|null $prefix 145 | * @return bool 146 | */ 147 | protected function _noAuthenticationNeeded($name, $plugin, $prefix) { 148 | if (!isset($this->authAllow)) { 149 | $this->authAllow = $this->_parseAuthAllow(); 150 | } 151 | 152 | $key = $name; 153 | if (!isset($this->authAllow[$key])) { 154 | return false; 155 | } 156 | 157 | if ($this->authAllow[$key] === '*') { 158 | return true; 159 | } 160 | 161 | //TODO: specific actions? 162 | return false; 163 | } 164 | 165 | /** 166 | * @return array 167 | */ 168 | protected function _parseAuthAllow() { 169 | $defaults = [ 170 | 'allowFilePath' => null, 171 | 'allowFile' => 'auth_allow.ini', 172 | ]; 173 | $config = (array)Configure::read('TinyAuth') + $defaults; 174 | 175 | $path = $config['allowFilePath'] ?: ROOT . DS . 'config' . DS; 176 | $file = $path . $config['allowFile']; 177 | 178 | return Utility::parseFile($file); 179 | } 180 | 181 | /** 182 | * @param \Cake\Console\Arguments $args 183 | * 184 | * @return array 185 | */ 186 | public function controllers(Arguments $args): array { 187 | //$path = $this->config['aclFilePath'] ?: ROOT . DS . 'config' . DS; 188 | //$file = $path . $this->config['aclFile']; 189 | //$content = Utility::parseFile($file); 190 | 191 | $controllers = $this->_getControllers((string)$args->getOption('plugin') ?: null); 192 | 193 | return $controllers; 194 | } 195 | 196 | /** 197 | * @param array $roles 198 | * @param array $mappedRoles 199 | * @param \Cake\Console\ConsoleIo $io 200 | * 201 | * @return void 202 | */ 203 | protected function checkRoles(array $roles, array $mappedRoles, ConsoleIo $io): void { 204 | foreach ($roles as $role) { 205 | if (!in_array($role, $mappedRoles, true) && !in_array('*', $mappedRoles, true)) { 206 | return; 207 | } 208 | } 209 | 210 | $io->abort('Already present. Aborting'); 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /src/Sync/Syncer.php: -------------------------------------------------------------------------------- 1 | 'auth_acl.ini', 28 | 'aclFilePath' => null, 29 | ]; 30 | $config = (array)Configure::read('TinyAuth') + $defaults; 31 | 32 | $path = $config['aclFilePath'] ?: ROOT . DS . 'config' . DS; 33 | $file = $path . $config['aclFile']; 34 | $content = Utility::parseFile($file); 35 | 36 | $controllers = $this->_getControllers((string)$args->getOption('plugin') ?: null); 37 | foreach ($controllers as $controller) { 38 | if (isset($content[$controller])) { 39 | continue; 40 | } 41 | 42 | $io->info('Add ' . $controller); 43 | $roles = (string)$args->getArgument('roles'); 44 | $roles = array_map('trim', explode(',', $roles)); 45 | $map = [ 46 | '*' => implode(', ', $roles), 47 | ]; 48 | $content[$controller] = $map; 49 | } 50 | 51 | if ($args->getOption('dry-run')) { 52 | $string = Utility::buildIniString($content); 53 | 54 | if ($args->getOption('verbose')) { 55 | $io->info('=== ' . $config['aclFile'] . ' ==='); 56 | $io->info($string); 57 | $io->info('=== ' . $config['aclFile'] . ' end ==='); 58 | } 59 | 60 | return; 61 | } 62 | 63 | Utility::generateFile($file, $content); 64 | } 65 | 66 | /** 67 | * @param string|null $plugin 68 | * @return array 69 | */ 70 | protected function _getControllers($plugin) { 71 | if ($plugin === 'all') { 72 | $plugins = Plugin::loaded(); 73 | 74 | $controllers = []; 75 | foreach ($plugins as $plugin) { 76 | $controllers = array_merge($controllers, $this->_getControllers($plugin)); 77 | } 78 | 79 | return $controllers; 80 | } 81 | 82 | $folders = App::classPath('Controller', $plugin); 83 | 84 | $controllers = []; 85 | foreach ($folders as $folder) { 86 | $controllers = array_merge($controllers, $this->_parseControllers($folder, $plugin)); 87 | } 88 | 89 | return $controllers; 90 | } 91 | 92 | /** 93 | * @param string $folder Path 94 | * @param string|null $plugin 95 | * @param string|null $prefix 96 | * 97 | * @return array 98 | */ 99 | protected function _parseControllers($folder, $plugin, $prefix = null) { 100 | $folderContent = (new Folder($folder))->read(Folder::SORT_NAME, true); 101 | 102 | $controllers = []; 103 | foreach ($folderContent[1] as $file) { 104 | $className = pathinfo($file, PATHINFO_FILENAME); 105 | 106 | if (!preg_match('#^(.+)Controller$#', $className, $matches)) { 107 | continue; 108 | } 109 | $name = $matches[1]; 110 | if ($matches[1] === 'App') { 111 | continue; 112 | } 113 | 114 | if ($this->_noAuthenticationNeeded($name, $plugin, $prefix)) { 115 | continue; 116 | } 117 | 118 | $controllers[] = ($plugin ? $plugin . '.' : '') . ($prefix ? $prefix . '/' : '') . $name; 119 | } 120 | 121 | foreach ($folderContent[0] as $subFolder) { 122 | $prefixes = (array)Configure::read('TinyAuth.prefixes') ?: null; 123 | 124 | if ($prefixes !== null && !in_array($subFolder, $prefixes, true)) { 125 | continue; 126 | } 127 | 128 | $controllers = array_merge($controllers, $this->_parseControllers($folder . $subFolder . DS, $plugin, $subFolder)); 129 | } 130 | 131 | return $controllers; 132 | } 133 | 134 | /** 135 | * @param string $name 136 | * @param string|null $plugin 137 | * @param string|null $prefix 138 | * @return bool 139 | */ 140 | protected function _noAuthenticationNeeded($name, $plugin, $prefix) { 141 | if (!isset($this->authAllow)) { 142 | $this->authAllow = $this->_parseAuthAllow(); 143 | } 144 | 145 | $key = $name; 146 | if (!isset($this->authAllow[$key])) { 147 | return false; 148 | } 149 | 150 | if ($this->authAllow[$key] === '*') { 151 | return true; 152 | } 153 | 154 | //TODO: specific actions? 155 | return false; 156 | } 157 | 158 | /** 159 | * @return array 160 | */ 161 | protected function _parseAuthAllow() { 162 | $defaults = [ 163 | 'allowFilePath' => null, 164 | 'allowFile' => 'auth_allow.ini', 165 | ]; 166 | $config = (array)Configure::read('TinyAuth') + $defaults; 167 | 168 | $path = $config['allowFilePath'] ?: ROOT . DS . 'config' . DS; 169 | $file = $path . $config['allowFile']; 170 | 171 | return Utility::parseFile($file); 172 | } 173 | 174 | } 175 | -------------------------------------------------------------------------------- /src/TinyAuthPlugin.php: -------------------------------------------------------------------------------- 1 | '_cake_model_', 32 | 'cachePrefix' => 'tiny_auth_', 33 | 'allowCacheKey' => self::KEY_ALLOW, 34 | 'aclCacheKey' => self::KEY_ACL, 35 | ]; 36 | 37 | /** 38 | * Clears specific cache or all caches. 39 | * 40 | * @param string|null $type 41 | * 42 | * @return void 43 | */ 44 | public static function clear(?string $type = null): void { 45 | $config = static::prepareConfig(); 46 | static::assertValidCacheSetup($config); 47 | 48 | if ($type) { 49 | $key = static::key($type); 50 | CoreCache::delete($key, $config['cache']); 51 | 52 | return; 53 | } 54 | 55 | CoreCache::delete(static::key(static::KEY_ALLOW), $config['cache']); 56 | CoreCache::delete(static::key(static::KEY_ACL), $config['cache']); 57 | } 58 | 59 | /** 60 | * @param string $type 61 | * @param array $data 62 | * 63 | * @return void 64 | */ 65 | public static function write(string $type, array $data): void { 66 | $config = static::prepareConfig(); 67 | 68 | CoreCache::write(static::key($type), $data, $config['cache']); 69 | } 70 | 71 | /** 72 | * @param string $type 73 | * 74 | * @return array|null 75 | */ 76 | public static function read(string $type): ?array { 77 | $config = static::prepareConfig(); 78 | 79 | return CoreCache::read(static::key($type), $config['cache']) ?: null; 80 | } 81 | 82 | /** 83 | * @param string $type 84 | *@throws \Cake\Core\Exception\CakeException 85 | * @return string 86 | */ 87 | public static function key(string $type): string { 88 | $config = static::prepareConfig(); 89 | 90 | static::assertValidCacheSetup($config); 91 | 92 | $key = $type . 'CacheKey'; 93 | if (empty($config[$key])) { 94 | throw new CakeException(sprintf('Invalid TinyAuth cache key `%s`', $key)); 95 | } 96 | 97 | return $config[$key]; 98 | } 99 | 100 | /** 101 | * @param array $config 102 | * @throws \Cake\Core\Exception\CakeException 103 | * @return void 104 | */ 105 | protected static function assertValidCacheSetup(array $config): void { 106 | if (!in_array($config['cache'], CoreCache::configured(), true)) { 107 | throw new CakeException(sprintf('Invalid or not configured TinyAuth cache `%s`', $config['cache'])); 108 | } 109 | } 110 | 111 | /** 112 | * @return array 113 | */ 114 | protected static function prepareConfig(): array { 115 | $defaultConfig = static::$_defaultConfig; 116 | 117 | //BC with 5.0.x 118 | $configured = CoreCache::getRegistry(); 119 | if ($configured->has('_cake_core_')) { 120 | $defaultConfig['cache'] = '_cake_core_'; 121 | } 122 | 123 | return (array)Configure::read('TinyAuth') + $defaultConfig; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Utility/Config.php: -------------------------------------------------------------------------------- 1 | IniAllowAdapter::class, 25 | 'allowFilePath' => null, // Possible to locate INI file at given path e.g. Plugin::configPath('Admin'), filePath is also available for shared config 26 | 'allowFile' => 'auth_allow.ini', 27 | 'allowNonPrefixed' => false, // Set to true to allow all non-prefixed controller actions automatically as public access. 28 | 'allowPrefixes' => [], // Set prefixes that are whitelisted as public access 29 | // acl 30 | 'aclAdapter' => IniAclAdapter::class, 31 | 'idColumn' => 'id', // ID Column in users table 32 | 'roleColumn' => 'role_id', // Foreign key for the Role ID in users table or in pivot table 33 | 'userColumn' => 'user_id', // Foreign key for the User id in pivot table. Only for multi-roles setup 34 | 'aliasColumn' => 'alias', // Name of column in roles table holding role alias/slug 35 | 'rolesTable' => 'Roles', // name of Configure key holding available roles OR class name of roles table 36 | 'usersTable' => 'Users', // name of the Users table 37 | 'pivotTable' => null, // Should be used in multi-roles setups 38 | 'multiRole' => false, // true to enables multirole/HABTM authorization (requires a valid pivot table) 39 | 'superAdminRole' => null, // id of super admin role, which grants access to ALL resources 40 | 'superAdmin' => null, // super admin, which grants access to ALL resources 41 | 'superAdminColumn' => null, // Column of super admin 42 | 'authorizeByPrefix' => false, // true for all available 1:1 matching or list of [prefix => role(s)] 43 | 'allowLoggedIn' => false, // enable to allow logged in user access to all actions except prefixed with 'protectedPrefix' 44 | 'protectedPrefix' => 'Admin', // name or array of names as prefix route blacklist - only used when 'allowLoggedIn' is enabled 45 | 'autoClearCache' => null, // Set to true to delete cache automatically in debug mode, keep null for auto-detect 46 | 'aclFilePath' => null, // Possible to locate INI file at given path e.g. Plugin::configPath('Admin'), filePath is also available for shared config 47 | 'aclFile' => 'auth_acl.ini', 48 | 'includeAuthentication' => false, // Set to true to include public auth access into hasAccess() checks. Note, that this requires Configure configuration. 49 | ]; 50 | 51 | /** 52 | * @return array 53 | */ 54 | public static function all() { 55 | if (!static::$_config) { 56 | $config = (array)Configure::read('TinyAuth') + static::$_defaultConfig; 57 | 58 | if ($config['autoClearCache'] === null) { 59 | $config['autoClearCache'] = Configure::read('debug'); 60 | } 61 | 62 | static::$_config = $config; 63 | } 64 | 65 | return static::$_config; 66 | } 67 | 68 | /** 69 | * @param string $key 70 | * @throws \Cake\Core\Exception\CakeException 71 | * @return mixed 72 | */ 73 | public static function get($key) { 74 | $config = static::all(); 75 | if (!isset($config[$key])) { 76 | throw new CakeException('Key ' . $key . ' not found in config.'); 77 | } 78 | 79 | return $config[$key]; 80 | } 81 | 82 | /** 83 | * @return void 84 | */ 85 | public static function drop() { 86 | static::$_config = []; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Utility/SessionCache.php: -------------------------------------------------------------------------------- 1 | 'default', 20 | 'prefix' => 'auth_user_', 21 | ]; 22 | 23 | /** 24 | * Clears all session user info based on prefix 25 | * 26 | * @return void 27 | */ 28 | public static function clear(): void { 29 | $config = static::prepareConfig(); 30 | static::assertValidCacheSetup($config); 31 | 32 | if (!empty($config['groups'])) { 33 | foreach ((array)$config['groups'] as $group) { 34 | Cache::clearGroup($group, $config['cache']); 35 | } 36 | 37 | return; 38 | } 39 | 40 | Cache::clear($config['cache']); 41 | } 42 | 43 | /** 44 | * @param string|int $userId 45 | * @param \ArrayAccess|array $data 46 | * 47 | * @return void 48 | */ 49 | public static function write(int|string $userId, ArrayAccess|array $data): void { 50 | $config = static::prepareConfig(); 51 | 52 | Cache::write(static::key($userId), $data, $config['cache']); 53 | } 54 | 55 | /** 56 | * @param string|int $userId 57 | * 58 | * @return \ArrayAccess|array|null 59 | */ 60 | public static function read(int|string $userId): ArrayAccess|array|null { 61 | $config = static::prepareConfig(); 62 | 63 | return Cache::read(static::key($userId), $config['cache']) ?: null; 64 | } 65 | 66 | /** 67 | * @param string|int $userId 68 | * 69 | * @return bool 70 | */ 71 | public static function delete(int|string $userId): bool { 72 | $config = static::prepareConfig(); 73 | 74 | return Cache::delete(static::key($userId), $config['cache']); 75 | } 76 | 77 | /** 78 | * @param string|int $userId 79 | * @return string 80 | */ 81 | public static function key(int|string $userId): string { 82 | $config = static::prepareConfig(); 83 | 84 | static::assertValidCacheSetup($config); 85 | 86 | return $config['prefix'] . $userId; 87 | } 88 | 89 | /** 90 | * @param array $config 91 | * @throws \Cake\Core\Exception\CakeException 92 | * @return void 93 | */ 94 | protected static function assertValidCacheSetup(array $config): void { 95 | if (!in_array($config['cache'], Cache::configured(), true)) { 96 | throw new CakeException(sprintf('Invalid or not configured TinyAuth cache `%s`', $config['cache'])); 97 | } 98 | } 99 | 100 | /** 101 | * @return array 102 | */ 103 | protected static function prepareConfig(): array { 104 | $defaultConfig = static::$_defaultConfig; 105 | 106 | return (array)Configure::read('TinyAuth') + $defaultConfig; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/Utility/TinyAuth.php: -------------------------------------------------------------------------------- 1 | $config 23 | */ 24 | public function __construct(array $config = []) { 25 | $config += Config::all(); 26 | 27 | $this->setConfig($config); 28 | } 29 | 30 | /** 31 | * @return array 32 | */ 33 | public function getAvailableRoles() { 34 | $roles = $this->_getAvailableRoles(); 35 | 36 | return $roles; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/Utility/Utility.php: -------------------------------------------------------------------------------- 1 | Array with named keys for controller, plugin and prefix 15 | */ 16 | public static function deconstructIniKey($key) { 17 | $res = [ 18 | 'plugin' => null, 19 | 'prefix' => null, 20 | ]; 21 | 22 | if (str_contains($key, '.')) { 23 | [$res['plugin'], $key] = explode('.', $key); 24 | } 25 | $lastSlashPos = strrpos($key, '/'); 26 | if ($lastSlashPos !== false) { 27 | $prefix = substr($key, 0, $lastSlashPos); 28 | $res['prefix'] = Inflector::camelize($prefix); 29 | $key = substr($key, $lastSlashPos + 1); 30 | } 31 | $res['controller'] = $key; 32 | 33 | return $res; 34 | } 35 | 36 | /** 37 | * Returns the found INI file(s) as an array. 38 | * 39 | * @param array|string|null $paths Paths to look for INI files. 40 | * @param string $file INI file name. 41 | * @return array List with all found files. 42 | */ 43 | public static function parseFiles($paths, $file) { 44 | if ($paths === null) { 45 | $paths = ROOT . DS . 'config' . DS; 46 | } 47 | 48 | $list = []; 49 | foreach ((array)$paths as $path) { 50 | $list += static::parseFile($path . $file); 51 | } 52 | 53 | return $list; 54 | } 55 | 56 | /** 57 | * Returns the ini file as an array. 58 | * 59 | * @param string $ini Full path to the ini file 60 | * @throws \Cake\Core\Exception\CakeException 61 | * @return array List 62 | */ 63 | public static function parseFile($ini) { 64 | if (!file_exists($ini)) { 65 | throw new CakeException(sprintf('Missing TinyAuth config file (%s)', $ini)); 66 | } 67 | 68 | if (function_exists('parse_ini_file')) { 69 | $iniArray = parse_ini_file($ini, true); 70 | } else { 71 | $content = file_get_contents($ini); 72 | if ($content === false) { 73 | throw new CakeException('Cannot load INI file.'); 74 | } 75 | $iniArray = parse_ini_string($content, true); 76 | } 77 | 78 | if (!is_array($iniArray)) { 79 | throw new CakeException(sprintf('Invalid TinyAuth config file (%s)', $ini)); 80 | } 81 | 82 | return $iniArray; 83 | } 84 | 85 | /** 86 | * @param string $file 87 | * @param array $content 88 | * 89 | * @return bool 90 | */ 91 | public static function generateFile($file, array $content) { 92 | $string = static::buildIniString($content); 93 | 94 | return (bool)file_put_contents($file, $string); 95 | } 96 | 97 | /** 98 | * @param array $a 99 | * 100 | * @return string 101 | */ 102 | public static function buildIniString(array $a) { 103 | $out = []; 104 | foreach ($a as $rootkey => $rootvalue) { 105 | $out[] = "[$rootkey]"; 106 | 107 | // loop through items under a section heading 108 | foreach ($rootvalue as $key => $value) { 109 | $out[] = "$key = $value"; 110 | } 111 | 112 | $out[] = ''; 113 | } 114 | 115 | return implode(PHP_EOL, $out); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/View/Helper/AuthUserHelper.php: -------------------------------------------------------------------------------- 1 | $config Configuration settings for the helper. 34 | */ 35 | public function __construct(View $View, array $config = []) { 36 | $config += Config::all(); 37 | 38 | parent::__construct($View, $config); 39 | } 40 | 41 | /** 42 | * @return \ArrayAccess|array|null 43 | */ 44 | public function identity(): ArrayAccess|array|null { 45 | /** @var \Authorization\Identity|\Authentication\Identity|null $identity */ 46 | $identity = $this->_View->getRequest()->getAttribute('identity'); 47 | if (!$identity) { 48 | return null; 49 | } 50 | 51 | /** @var \ArrayAccess|array|null */ 52 | return $identity->getOriginalData(); 53 | } 54 | 55 | /** 56 | * This is only for usage with already logged in persons as this uses the ACL (not allow) data. 57 | * 58 | * If you need to support also public methods (via Controller or allow INI etc), you need to enable 59 | * `includeAuthentication` config and make sure all actions are whitelisted in auth allow INI file. 60 | * 61 | * @param array $url 62 | * @throws \Cake\Core\Exception\CakeException 63 | * @return bool 64 | */ 65 | public function hasAccess(array $url) { 66 | if (isset($url['_name'])) { 67 | //throw MissingRouteException if necessary 68 | Router::url($url); 69 | $routes = Router::getRouteCollection()->named(); 70 | $defaults = $routes[$url['_name']]->defaults; 71 | if (!isset($defaults['action']) || !isset($defaults['controller'])) { 72 | throw new CakeException('Controller or action name could not be null.'); 73 | } 74 | $url = [ 75 | 'prefix' => !empty($defaults['prefix']) ? $defaults['prefix'] : null, 76 | 'plugin' => !empty($defaults['plugin']) ? $defaults['plugin'] : null, 77 | 'controller' => $defaults['controller'], 78 | 'action' => $defaults['action'], 79 | ]; 80 | } else { 81 | $params = $this->_View->getRequest()->getAttribute('params'); 82 | $url += [ 83 | 'prefix' => !empty($params['prefix']) ? $params['prefix'] : null, 84 | 'plugin' => !empty($params['plugin']) ? $params['plugin'] : null, 85 | 'controller' => $params['controller'], 86 | 'action' => 'index', 87 | ]; 88 | } 89 | 90 | $authUser = $this->_View->get('_authUser'); 91 | if ($authUser === null && !$this->getConfig('includeAuthentication')) { 92 | throw new CakeException('Variable _authUser containing the user data needs to be passed down. The TinyAuth.Auth component does it automatically, if loaded.'); 93 | } 94 | $userArray = ($authUser instanceof ArrayAccess && method_exists($authUser, 'toArray')) ? $authUser->toArray() : (array)$authUser; 95 | 96 | return $this->_checkUser($userArray, $url); 97 | } 98 | 99 | /** 100 | * Options: 101 | * - default: Default to show instead, defaults to empty string. 102 | * Set to true to show just title text when not allowed. 103 | * and all other link() options 104 | * 105 | * @param string $title 106 | * @param array $url 107 | * @param array $options 108 | * @return string 109 | */ 110 | public function link($title, array $url, array $options = []) { 111 | if (!$this->hasAccess($url)) { 112 | return $this->_default($title, $options); 113 | } 114 | unset($options['default']); 115 | 116 | return $this->Html->link($title, $url, $options); 117 | } 118 | 119 | /** 120 | * Options: 121 | * - default: Default to show instead, defaults to empty string. 122 | * Set to true to show just title text when not allowed. 123 | * and all other link() options 124 | * 125 | * @param string $title 126 | * @param array $url 127 | * @param array $options 128 | * @return string 129 | */ 130 | public function postLink($title, array $url, array $options = []) { 131 | if (!$this->hasAccess($url)) { 132 | return $this->_default($title, $options); 133 | } 134 | unset($options['default']); 135 | 136 | return $this->Form->postLink($title, $url, $options); 137 | } 138 | 139 | /** 140 | * @param string $title 141 | * @param array $options 142 | * @return string 143 | */ 144 | protected function _default($title, array $options) { 145 | $options += [ 146 | 'default' => '', 147 | 'escape' => true, 148 | ]; 149 | 150 | if ($options['default'] === true) { 151 | return ($options['escape'] === false) ? $title : h($title); 152 | } 153 | 154 | return $options['default']; 155 | } 156 | 157 | /** 158 | * @throws \Cake\Core\Exception\CakeException 159 | * @return array 160 | */ 161 | protected function _getUser(): array { 162 | if (class_exists(Identity::class)) { 163 | $identity = $this->identity(); 164 | 165 | return $identity ? $this->_toArray($identity) : []; 166 | } 167 | 168 | $authUser = $this->_View->get('_authUser'); 169 | if ($authUser === null) { 170 | throw new CakeException('TinyAuth.AuthUser helper needs TinyAuth.AuthUser component to function. Please make sure it is loaded in your controller.'); 171 | } 172 | 173 | return (array)$authUser; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/View/Helper/AuthenticationHelper.php: -------------------------------------------------------------------------------- 1 | $config Configuration settings for the helper. 26 | */ 27 | public function __construct(View $View, array $config = []) { 28 | $config += Config::all(); 29 | 30 | parent::__construct($View, $config); 31 | } 32 | 33 | /** 34 | * Checks if a given URL is public. 35 | * 36 | * If no URL is given it will default to the current request URL. 37 | * 38 | * @param array $url 39 | * @return bool 40 | */ 41 | public function isPublic(array $url = []) { 42 | if (!$url) { 43 | $url = $this->_View->getRequest()->getAttribute('params'); 44 | } 45 | 46 | if (isset($url['_name'])) { 47 | //throw MissingRouteException if necessary 48 | Router::url($url); 49 | $routes = Router::getRouteCollection()->named(); 50 | $defaults = $routes[$url['_name']]->defaults; 51 | if (!isset($defaults['action']) || !isset($defaults['controller'])) { 52 | throw new CakeException('Controller or action name could not be null.'); 53 | } 54 | $url = [ 55 | 'prefix' => !empty($defaults['prefix']) ? $defaults['prefix'] : null, 56 | 'plugin' => !empty($defaults['plugin']) ? $defaults['plugin'] : null, 57 | 'controller' => $defaults['controller'], 58 | 'action' => $defaults['action'], 59 | ]; 60 | } else { 61 | $params = $this->_View->getRequest()->getAttribute('params'); 62 | $url += [ 63 | 'prefix' => !empty($params['prefix']) ? $params['prefix'] : null, 64 | 'plugin' => !empty($params['plugin']) ? $params['plugin'] : null, 65 | 'controller' => $params['controller'], 66 | 'action' => 'index', 67 | ]; 68 | } 69 | 70 | $rule = $this->_getAllowRule($url); 71 | 72 | return $this->_isActionAllowed($rule, $url['action']); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /templates/element/auth_panel.php: -------------------------------------------------------------------------------- 1 | $config 12 | * @var \Authorization\Identity|\Authentication\Identity|null $identity 13 | * @var \Authentication\Authenticator\AuthenticatorInterface|null $authenticationProvider 14 | * @var \Authentication\Identifier\IdentifierInterface|null $identificationProvider 15 | */ 16 | 17 | use Cake\Error\Debugger; 18 | use TinyAuth\Panel\AuthPanel; 19 | use TinyAuth\Utility\Config; 20 | 21 | if (!isset($params)) { 22 | $params = []; 23 | } 24 | if (!isset($path)) { 25 | $path = ''; 26 | } 27 | if (!isset($isPublic)) { 28 | $isPublic = null; 29 | } 30 | 31 | ?> 32 | 33 |
34 |

TinyAuth

35 | 36 | 37 | 38 | 80 | 125 | 126 |
39 | 40 |

Current URL

41 | 44 |
45 |

TinyAuth URL path:

46 | 47 |

Authentication (allow)

48 |

49 | No information available'; 53 | } else { 54 | $icon = $isPublic ? AuthPanel::ICON_PUBLIC : AuthPanel::ICON_RESTRICTED; 55 | echo '

' . $icon . ' ' . ($isPublic ? 'public' : 'secured') . ' action

'; 56 | if ($isPublic) { 57 | echo '
Any guest can visit this page
'; 58 | } else { 59 | echo '
Login required to visit this page
'; 60 | } 61 | } 62 | } else { 63 | echo 'disabled'; 64 | } 65 | ?> 66 |

67 | 68 | '; 70 | echo 'Authentication provider used: ' . get_class($authenticationProvider); 71 | echo '

'; 72 | } ?> 73 | '; 75 | echo 'Identification provider used: ' . get_class($identificationProvider); 76 | echo '

'; 77 | } ?> 78 | 79 |
81 | 82 |

Authorization (ACL)

83 | Logged in with ID ' . h($user[$primaryKey]) . '

'; 89 | 90 | echo '

Roles

'; 91 | Debugger::dump($roles); 92 | 93 | } else { 94 | echo 'not logged in
'; 95 | } 96 | 97 | } else { 98 | echo 'disabled
'; 99 | } 100 | ?> 101 | 102 |
103 | 104 | 105 |

106 | 107 | The following roles have access to this action: 108 | 109 | The following roles would have access to this action once you revoke public access: 110 | 111 |

112 |
    113 | $id) { 115 | echo '
  • '; 116 | echo ($access[$role] ? '' : '🚫') . ' '; 117 | echo h($role) . ' (id ' . $id . ')'; 118 | echo '
  • '; 119 | } 120 | ?> 121 |
122 | 123 | 124 |
127 | 128 | 129 |

Identity

130 | ' . get_class($identity) . ''; 132 | ?> 133 | 134 | getOriginalData(); 136 | echo Debugger::exportVar($user); 137 | ?> 138 | 139 | 140 |
141 | -------------------------------------------------------------------------------- /tests/Fixture/DatabaseRolesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'name' => ['type' => 'string', 'null' => false, 'length' => 64, 'comment' => '', 'charset' => 'utf8'], 20 | 'description' => ['type' => 'string', 'null' => false, 'default' => null, 'comment' => '', 'charset' => 'utf8'], 21 | 'alias' => ['type' => 'string', 'null' => false, 'default' => null, 'length' => 20, 'comment' => '', 'charset' => 'utf8'], 22 | 'created' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''], 23 | 'modified' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''], 24 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 25 | ]; 26 | 27 | /** 28 | * Records 29 | * 30 | * @var array 31 | */ 32 | public array $records = [ 33 | [ 34 | 'id' => '11', 35 | 'name' => 'User', 36 | 'description' => 'Basic authenticated user', 37 | 'alias' => 'user', 38 | 'created' => '2010-01-07 03:36:33', 39 | 'modified' => '2010-01-07 03:36:33', 40 | ], 41 | [ 42 | 'id' => '12', 43 | 'name' => 'Moderator', 44 | 'description' => 'Authenticated user with moderator role', 45 | 'alias' => 'moderator', 46 | 'created' => '2010-01-07 03:36:33', 47 | 'modified' => '2010-01-07 03:36:33', 48 | ], 49 | [ 50 | 'id' => '13', 51 | 'name' => 'Admin', 52 | 'description' => 'Authenticated user with admin role', 53 | 'alias' => 'admin', 54 | 'created' => '2010-01-07 03:36:33', 55 | 'modified' => '2010-01-07 03:36:33', 56 | ], 57 | ]; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/Fixture/DatabaseRolesUsersFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 16 | 'user_id' => ['type' => 'integer'], 17 | 'database_role_id' => ['type' => 'integer'], 18 | '_constraints' => [ 19 | 'primary' => ['type' => 'primary', 'columns' => ['id']], 20 | ], 21 | ]; 22 | 23 | /** 24 | * Records 25 | * 26 | * @var array 27 | */ 28 | public array $records = [ 29 | [ 30 | 'id' => 1, 31 | 'user_id' => 1, 32 | 'database_role_id' => 11, // user 33 | ], 34 | [ 35 | 'id' => 2, 36 | 'user_id' => 1, 37 | 'database_role_id' => 12, // moderator 38 | ], 39 | [ 40 | 'id' => 3, 41 | 'user_id' => 2, 42 | 'database_role_id' => 11, // user 43 | ], 44 | [ 45 | 'id' => 4, 46 | 'user_id' => 2, 47 | 'database_role_id' => 13, // admin 48 | ], 49 | ]; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/Fixture/DatabaseUserRolesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 16 | 'user_id' => ['type' => 'integer'], 17 | 'role_id' => ['type' => 'integer'], 18 | '_constraints' => [ 19 | 'primary' => ['type' => 'primary', 'columns' => ['id']], 20 | ], 21 | ]; 22 | 23 | /** 24 | * Records 25 | * 26 | * @var array 27 | */ 28 | public array $records = [ 29 | [ 30 | 'id' => 1, 31 | 'user_id' => 1, 32 | 'role_id' => 11, // user 33 | ], 34 | [ 35 | 'id' => 2, 36 | 'user_id' => 1, 37 | 'role_id' => 12, // moderator 38 | ], 39 | [ 40 | 'id' => 3, 41 | 'user_id' => 2, 42 | 'role_id' => 11, // user 43 | ], 44 | [ 45 | 'id' => 4, 46 | 'user_id' => 2, 47 | 'role_id' => 13, // admin 48 | ], 49 | ]; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/Fixture/EmptyRolesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'alias' => ['type' => 'string', 'null' => false, 'default' => null, 'length' => 20, 'comment' => '', 'charset' => 'utf8'], 20 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 21 | ]; 22 | 23 | /** 24 | * Records 25 | * 26 | * @var array 27 | */ 28 | public array $records = []; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fixture/RolesUsersFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 16 | 'user_id' => ['type' => 'integer'], 17 | 'role_id' => ['type' => 'integer'], 18 | '_constraints' => [ 19 | 'primary' => ['type' => 'primary', 'columns' => ['id']], 20 | ], 21 | ]; 22 | 23 | /** 24 | * Records 25 | * 26 | * @var array 27 | */ 28 | public array $records = [ 29 | [ 30 | 'id' => 1, 31 | 'user_id' => 1, 32 | 'role_id' => ROLE_USER, // user 33 | ], 34 | [ 35 | 'id' => 2, 36 | 'user_id' => 1, 37 | 'role_id' => ROLE_MODERATOR, // moderator 38 | ], 39 | [ 40 | 'id' => 3, 41 | 'user_id' => 2, 42 | 'role_id' => ROLE_USER, // user 43 | ], 44 | [ 45 | 'id' => 4, 46 | 'user_id' => 2, 47 | 'role_id' => ROLE_ADMIN, // admin 48 | ], 49 | ]; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/Fixture/UsersFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer'], 19 | 'username' => ['type' => 'string', 'null' => false, 'length' => 64, 'comment' => '', 'charset' => 'utf8'], 20 | 'email' => ['type' => 'string', 'null' => false, 'length' => 64, 'comment' => '', 'charset' => 'utf8'], 21 | 'password' => ['type' => 'string', 'null' => true, 'length' => 64, 'comment' => '', 'charset' => 'utf8'], 22 | 'role_id' => ['type' => 'integer', 'null' => false, 'default' => '0', 'length' => 11, 'collate' => null, 'comment' => ''], 23 | '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]], 24 | ]; 25 | 26 | /** 27 | * Records 28 | * 29 | * @var array 30 | */ 31 | public array $records = [ 32 | [ 33 | 'id' => '1', 34 | 'username' => 'dereuromark', 35 | 'email' => 'dereuromark@test.de', 36 | 'password' => '$2y$10$syCszS4cf9SJrTbf0p6myukCl812046xqM.JPZItfuySnrmm6LH1y', // 123, 37 | 'role_id' => ROLE_USER, 38 | ], 39 | [ 40 | 'id' => '2', 41 | 'username' => 'bravo-kernel', 42 | 'email' => 'bravo-kernel@test.de', 43 | 'role_id' => ROLE_ADMIN, 44 | ], 45 | [ 46 | 'id' => '3', 47 | 'username' => 'adriana', 48 | 'email' => 'adriana@test.de', 49 | 'role_id' => ROLE_MODERATOR, 50 | ], 51 | ]; 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/config/bootstrap.php: -------------------------------------------------------------------------------- 1 | fallbacks(); 10 | }); 11 | */ 12 | -------------------------------------------------------------------------------- /tests/phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 1 3 | paths: 4 | - TestCase/ 5 | bootstrapFiles: 6 | - %rootDir%/../../../tests/bootstrap.php 7 | ignoreErrors: 8 | - identifier: missingType.iterableValue 9 | - identifier: missingType.generics 10 | -------------------------------------------------------------------------------- /tests/schema.php: -------------------------------------------------------------------------------- 1 | $ierator 9 | */ 10 | $ierator = new DirectoryIterator(__DIR__ . DS . 'Fixture'); 11 | foreach ($ierator as $file) { 12 | if (!preg_match('/(\w+)Fixture.php$/', (string)$file, $matches)) { 13 | continue; 14 | } 15 | 16 | $name = $matches[1]; 17 | $tableName = Inflector::underscore($name); 18 | $class = 'TinyAuth\\Test\\Fixture\\' . $name . 'Fixture'; 19 | try { 20 | $object = (new ReflectionClass($class))->getProperty('fields'); 21 | } catch (ReflectionException $e) { 22 | continue; 23 | } 24 | 25 | $array = $object->getDefaultValue(); 26 | $constraints = $array['_constraints'] ?? []; 27 | $indexes = $array['_indexes'] ?? []; 28 | unset($array['_constraints'], $array['_indexes'], $array['_options']); 29 | $table = [ 30 | 'table' => $tableName, 31 | 'columns' => $array, 32 | 'constraints' => $constraints, 33 | 'indexes' => $indexes, 34 | ]; 35 | $tables[$tableName] = $table; 36 | } 37 | 38 | return $tables; 39 | --------------------------------------------------------------------------------