├── LICENSE.txt ├── composer.json ├── config ├── ci-setup.sh ├── composer_phive.php ├── composer_post_install.php └── composer_post_update.php ├── docker-compose.yml ├── docker └── php │ └── Dockerfile ├── docs ├── Architecture-Overview.md ├── Authenticators.md ├── Identifiers.md ├── Identity-Object.md ├── Identity-Resolvers.md ├── Installation.md ├── JWT-Example.md ├── PSR15-Middleware.md ├── PSR7-Middleware.md ├── Quick-start-and-introduction.md └── URL-Checkers.md ├── phpcs.xml ├── phpmd.baseline.xml ├── phpmd.xml.dist ├── phpstan.neon ├── readme.md └── src ├── AuthenticationException.php ├── AuthenticationService.php ├── AuthenticationServiceInterface.php ├── AuthenticationServiceProviderInterface.php ├── Authenticator ├── AbstractAuthenticator.php ├── AuthenticatorCollection.php ├── AuthenticatorCollectionInterface.php ├── AuthenticatorInterface.php ├── CookieAuthenticator.php ├── CredentialFieldsTrait.php ├── Exception │ ├── AuthenticationExceptionInterface.php │ ├── UnauthenticatedException.php │ └── UnauthorizedException.php ├── Failure.php ├── FailureInterface.php ├── FormAuthenticator.php ├── HttpBasicAuthenticator.php ├── HttpDigestAuthenticator.php ├── JwtAuthenticator.php ├── PersistenceInterface.php ├── Result.php ├── ResultInterface.php ├── SessionAuthenticator.php ├── StatelessInterface.php ├── Storage │ ├── NativePhpSessionStorage.php │ └── StorageInterface.php ├── TokenAuthenticator.php └── UrlAwareTrait.php ├── Identifier ├── AbstractIdentifier.php ├── CallbackIdentifier.php ├── CollectionIdentifier.php ├── IdentifierCollection.php ├── IdentifierCollectionInterface.php ├── IdentifierInterface.php ├── JwtSubjectIdentifier.php ├── Ldap │ ├── AdapterInterface.php │ └── ExtensionAdapter.php ├── LdapIdentifier.php ├── PasswordIdentifier.php ├── Resolver │ ├── CallbackResolver.php │ ├── PdoResolver.php │ ├── PdoStatementResolver.php │ └── ResolverInterface.php └── TokenIdentifier.php ├── Identity ├── DefaultIdentityFactory.php ├── Identity.php ├── IdentityFactoryInterface.php └── IdentityInterface.php ├── Middleware ├── AuthenticationErrorHandlerMiddleware.php └── AuthenticationMiddleware.php ├── PersistenceResult.php ├── PersistenceResultInterface.php └── UrlChecker ├── DefaultUrlChecker.php ├── RegexUrlChecker.php └── UrlCheckerInterface.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) Cake Software Foundation, Inc. (http://cakefoundation.org) 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 furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phauthentic/authentication", 3 | "description": "A Framework agnostic Authentication library for PHP", 4 | "keywords": [ 5 | "auth", 6 | "authentication", 7 | "middleware", 8 | "library" 9 | ], 10 | "require": { 11 | "php": "^8.0", 12 | "ext-json": "*", 13 | "phauthentic/password-hashers": "^2.0", 14 | "psr/http-factory": "^1.0", 15 | "psr/http-message": "~1.0", 16 | "psr/http-server-handler": "~1.0", 17 | "psr/http-server-middleware": "^1.0" 18 | }, 19 | "require-dev": { 20 | "ext-pdo": "*", 21 | "dms/phpunit-arraysubset-asserts": "^0.3.0", 22 | "firebase/php-jwt": "6.*", 23 | "misantron/dbunit": "dev-master", 24 | "nyholm/psr7": "^1.8", 25 | "phpmd/phpmd": "^2.15", 26 | "phpstan/phpstan": "^1.10", 27 | "phpunit/phpunit": "^9.5", 28 | "squizlabs/php_codesniffer": "^3.9" 29 | }, 30 | "suggest": { 31 | "firebase/php-jwt": "If you want to use the JWT adapter add this dependency", 32 | "ext-ldap": "Make sure this php extension is installed and enabled on your system if you want to use the built-in LDAP adapter for \"LdapIdentifier\"." 33 | }, 34 | "license": "MIT", 35 | "minimum-stability": "dev", 36 | "prefer-stable": true, 37 | "autoload": { 38 | "psr-4": { 39 | "Phauthentic\\Authentication\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Phauthentic\\Authentication\\Test\\": "tests" 45 | } 46 | }, 47 | "authors": [ 48 | { 49 | "name": "Florian Krämer" 50 | } 51 | ], 52 | "scripts": { 53 | "check": [ 54 | "@cs-check", 55 | "@test" 56 | ], 57 | "cs-check": "phpcs --colors -p src/ tests/", 58 | "cs-fix": "phpcbf --colors src/ tests/", 59 | "test": "phpunit", 60 | "test-coverage": "phpunit --coverage-clover=clover.xml", 61 | "phpmd": "bin/phpmd src/ text phpmd.xml.dist", 62 | "phpmd-baseline": "bin/phpmd --generate-baseline --baseline-file phpmd.baseline.xml src/ text phpmd.xml.dist" 63 | }, 64 | "config": { 65 | "sort-packages": true, 66 | "bin-dir": "bin" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /config/ci-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | wget -O phive.phar https://phar.io/releases/phive.phar 4 | wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc 5 | gpg --keyserver pool.sks-keyservers.net --recv-keys 0x9D8A98B29B2D5D79 6 | gpg --verify phive.phar.asc phive.phar 7 | chmod +x phive.phar 8 | sudo mv phive.phar /usr/local/bin/phive 9 | phive --no-progress install --target ./bin --trust-gpg-keys 0x4AA394086372C20A,0x8E730BA25823D8B5,0x31C7E470E2138192,0x4AA394086372C20A,0x0F9684B8B16B7AB0,0xBB5F005D6FFDD89E 10 | composer self-update 11 | composer --version 12 | composer global require hirak/prestissimo --no-plugins 13 | composer install --prefer-dist --no-interaction 14 | npm install yarn -g 15 | chmod -R +x ./bin 16 | -------------------------------------------------------------------------------- /config/composer_phive.php: -------------------------------------------------------------------------------- 1 | import($publicData); 28 | $gpg->addencryptkey($publicKey['fingerprint']); 29 | echo $gpg->encrypt('Data to encrypt'); 30 | } 31 | 32 | $keys = [ 33 | '0x4AA394086372C20A', 34 | '0x8E730BA25823D8B5', 35 | '0x31C7E470E2138192', 36 | '0x4AA394086372C20A', 37 | '0x0F9684B8B16B7AB0', 38 | '0xBB5F005D6FFDD89E', 39 | '0xCF1A108D0E7AE720' 40 | ]; 41 | $keys = implode(',', $keys); 42 | 43 | $output = ''; 44 | exec('php .' . $ds . 'phive.phar install --target ./bin --trust-gpg-keys ' . $keys . ' --force-accept-unsigned', $output); 45 | echo implode(PHP_EOL, $output); 46 | -------------------------------------------------------------------------------- /config/composer_post_install.php: -------------------------------------------------------------------------------- 1 | setReturnPayload(false); 63 | ``` 64 | 65 | ## HttpBasic 66 | 67 | See https://en.wikipedia.org/wiki/Basic_access_authentication 68 | 69 | Configuration setters: 70 | 71 | * **setRealm()**: Default is `$_SERVER['SERVER_NAME']` override it as needed. 72 | 73 | ## HttpDigest 74 | 75 | See https://en.wikipedia.org/wiki/Digest_access_authentication 76 | 77 | Configuration setters: 78 | 79 | * **setRealm()**: Sets the realm. 80 | * **setQop()**: Sets Qop. 81 | * **setNonce()**: Sets the nounce. 82 | * **setOpaque()**: Sets Opaque. 83 | * **setNonceLifetime()**: Sets the nonce lifetime. 84 | * **setPasswordField()**: Sets the password field name. 85 | 86 | ## Cookie Authenticator aka "Remember Me" 87 | 88 | The Cookie Authenticator allows you to implement the "remember me" feature for your login forms. 89 | 90 | Just make sure your login form has a field that matches the field name that is configured in this authenticator. 91 | 92 | To encrypt and decrypt your cookie make sure you added the EncryptedCookieMiddleware to your app *before* the AuthenticationMiddleware. 93 | 94 | Configuration setters: 95 | 96 | * **setRememberMeField()**: Default is `remember_me` 97 | * **setCookie()**: Array of cookie options: 98 | * **setname()**: Cookie name, default is `CookieAuth` 99 | * **setexpire()**: Expiration, default is `null` 100 | * **setpath()**: Path, default is `/` 101 | * **setdomain()**: Domain, default is an empty string `` 102 | * **setsecure()**: Bool, default is `false` 103 | * **sethttpOnly()**: Bool, default is `false` 104 | * **setvalue()**: Value, default is an empty string `` 105 | * **setFields()**: Array that maps `username` and `password` to the specified identity fields. 106 | * **setUrlChecker()**: The URL checker class or object. Default is `DefaultUrlChecker`. 107 | * **setLoginUrl()**: The login URL, string or array of URLs. Default is `null` and all pages will be checked. 108 | * **setPasswordHasher()**: Password hasher to use for token hashing. Default is `DefaultPasswordHasher::class`. 109 | 110 | ## OAuth 111 | 112 | There are currently no plans to implement an OAuth authenticator. 113 | The main reason for this is that OAuth 2.0 is not an authentication protocol. 114 | 115 | Read more about this topic [here](https://oauth.net/articles/authentication/). 116 | 117 | We will maybe add an OpenID Connect authenticator in the future. 118 | 119 | ## Url Checkers 120 | 121 | Some authenticators like `Form` or `Cookie` should be executed only on certain pages like `/login` page. This can be achieved using Url Checkers. 122 | 123 | By default a `DefaultUrlChecker` is used, which uses string URLs for comparison with support for regex check. 124 | 125 | Configuration setters: 126 | 127 | * **setUseRegex()**: Whether or not to use regular expressions for URL matching. Default is `false`. 128 | * **setCheckFullUrl()**: Whether or not to check full URL. Useful when a login form is on a different subdomain. Default is `false`. 129 | 130 | A custom url checker can be implemented for example if a support for framework specific URLs is needed. 131 | In this case the `Authentication\UrlChecker\UrlCheckerInterface should be implemented. 132 | 133 | For more details about URL Checkers [see this documentation page](URL-Checkers.md). 134 | -------------------------------------------------------------------------------- /docs/Identifiers.md: -------------------------------------------------------------------------------- 1 | # Identifiers 2 | 3 | Identifiers will identify an user or service based on the information that was extracted from the request by the authenticators. A holistic example of using the Password Identifier looks like: 4 | 5 | ```php 6 | use Phauthentic\Identifier\PasswordIdenfier; 7 | use Phauthentic\Identifier\Resolver\OrmResolver; 8 | use Phauthentic\PasswordHasher\DefaultPasswordHasher; 9 | 10 | $identifier = new PasswordIdenfier( 11 | new OrmResolver(), 12 | new DefaultPasswordHasher() 13 | ); 14 | ``` 15 | 16 | Some identifiers might use other constructor arguments. Construct them manually or set them up in your DI config as needed. 17 | 18 | ## Identifier options 19 | 20 | Almost each identifier takes a few different configuration options. The options can be set through setter methods. The following list of identifiers describes their setter options: 21 | 22 | 23 | ## Password Identifier 24 | 25 | The password identifier checks the passed credentials against a datasource. 26 | 27 | Configuration option setters: 28 | 29 | * **setFields()**: The fields for the lookup. Default is `['username' => 'username', 'password' => 'password']`. 30 | You can also set the `username` to an array. For e.g. using 31 | `['username' => ['username', 'email'], 'password' => 'password']` will allow 32 | you to match value of either username or email columns. 33 | 34 | ## Token Identifier 35 | 36 | Checks the passed token against a datasource. 37 | 38 | Configuration option setters: 39 | 40 | * **setTokenField()**: The field in the database to check against. Default is `token`. 41 | * **setDataField()**: The field in the passed data from the authenticator. Default is `token`. 42 | 43 | ## JWT Subject Identifier 44 | 45 | Checks the passed JWT token against a datasource. 46 | 47 | Configuration option setters: 48 | 49 | * **setTokenField()**: The field in the database to check against. Default is `id`. 50 | * **setDataField()**: The payload key to get user identifier from. Default is `sub`. 51 | 52 | ## LDAP Identifier 53 | 54 | Checks the passed credentials against a LDAP server. 55 | 56 | The constructor takes three required argument, the fourth, the port, is optional. 57 | 58 | The first argument is an adapter instance, the library comes with an LDAP adapter that requires [the LDAP extension](http://php.net/manual/en/book.ldap.php). 59 | 60 | The second argument is the host. The third argument is the distinguished name of the user to authenticate. Must be a callable. Anonymous binds are not supported. You can pass a custom object/classname here if it implements the `AdapterInterface`. 61 | 62 | ```php 63 | use Phauthentic\Identifier\LdapIdentifier; 64 | use Phauthentic\Identifier\Ldap\ExtensionAdapter; 65 | 66 | $identifier = new LdapIdentifier( 67 | new ExtensionAdapter(), // 68 | '127.0.0.1' // Host 69 | function() { /*...*/ } // BindDN Callable 70 | 389 // Port, optional, defaults to 389 71 | ); 72 | ``` 73 | 74 | Configuration option setters: 75 | 76 | * **setCredentialFields()**: The fields for the lookup. Default is `['username' => 'username', 'password' => 'password']`. 77 | * **setLdapOptions()**: Additional LDAP options, like `LDAP_OPT_PROTOCOL_VERSION` or `LDAP_OPT_NETWORK_TIMEOUT`. 78 | See [php.net](http://php.net/manual/en/function.ldap-set-option.php) for more valid options. 79 | 80 | ## Callback Identifier 81 | 82 | Allows you to use a callback for identification. This is useful for simple identifiers or quick prototyping. 83 | 84 | ```php 85 | use Phauthentic\Identifier\CallableIdentifier; 86 | 87 | $identifier = new CallableIdentifier(function($data) { 88 | // Whatever you need here 89 | }); 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/Identity-Object.md: -------------------------------------------------------------------------------- 1 | # The Identity Object 2 | 3 | The identity object is returned by the service and made available in the 4 | request. The object provides a method `getIdentifier()` that can be called to 5 | get the id of the current log in identity. 6 | 7 | The reason this object exists is to provide an interface that makes it easy to 8 | get access to the identity's id across various implementations/sources. 9 | 10 | It is named "identity" because a login can be made also by another system which 11 | is not necessary what is considered an "user". Identity provides a neutral but 12 | ubiquitous naming. 13 | 14 | ```php 15 | // Service implementing \Phauthentic\AuthenticationServiceInterface 16 | $authenticationService 17 | ->getIdentity() 18 | ->getIdentifier() 19 | 20 | // Instance of a request implementing \Psr\Http\Message\ServerRequestInterface 21 | $this->request 22 | ->getAttribute('identity') 23 | ->getIdentifier(); 24 | ``` 25 | 26 | The identity object provides ArrayAccess but as well a `get()` method to access 27 | data. It is strongly recommended to use the `get()` method over array access 28 | because the get method is aware of the field mapping. 29 | 30 | ```php 31 | $identity->get('email'); 32 | $identity->get('username'); 33 | ``` 34 | 35 | The default Identity object class can be configured to map fields. This is 36 | pretty useful if the identifier of the identity is a non-conventional `id` field 37 | or if you want to map other fields to more generic and common names. 38 | 39 | ```php 40 | $identity = new Identity($data, [ 41 | 'fieldMap' => [ 42 | 'id' => 'uid', 43 | 'username' => 'first_name' 44 | ] 45 | ]); 46 | }; 47 | ``` 48 | 49 | ## Creating your own Identity Object 50 | 51 | If you want to create your own identity object, your object must implement the 52 | `IdentityInterface`. 53 | 54 | ## Implementing the IdentityInterface on your User class 55 | 56 | If you'd like to continue using your existing User class with this plugin you 57 | can implement the `Authentication\IdentityInterface`: 58 | 59 | ```php 60 | namespace App\Model\User; 61 | 62 | use Phauthentic\Authentication\IdentityInterface; 63 | use SomeFramework\ORM\Entity; 64 | 65 | class User extends Entity implements IdentityInterface 66 | { 67 | 68 | /** 69 | * Authentication\IdentityInterface method 70 | */ 71 | public function getIdentifier() 72 | { 73 | return $this->id; 74 | } 75 | 76 | /** 77 | * Authentication\IdentityInterface method 78 | */ 79 | public function getOriginalData() 80 | { 81 | return $this; 82 | } 83 | 84 | // Other methods 85 | } 86 | ``` 87 | 88 | ## Using a Custom Identity Decorator 89 | 90 | If your identifiers cannot have their resulting objects modified to implement 91 | the `IdentityInterface` you can implement a custom decorator that implements the 92 | required interface: 93 | 94 | ```php 95 | // You can use a callable... 96 | $identityResolver = function ($data) { 97 | return new MyCustomIdentity($data); 98 | }; 99 | 100 | //...or a class name to set the identity wrapper. 101 | $identityResolver = MyCustomIdentity::class; 102 | 103 | // Then pass it to the service configuration 104 | $service = new AuthenticationService([ 105 | 'identityClass' => $identityResolver, 106 | 'identifiers' => [ 107 | 'Authentication.Password' 108 | ], 109 | 'authenticators' => [ 110 | 'Authentication.Form' 111 | ] 112 | ]); 113 | ``` 114 | -------------------------------------------------------------------------------- /docs/Identity-Resolvers.md: -------------------------------------------------------------------------------- 1 | # Identity resolvers 2 | 3 | Identity resolvers are adapters for different datasources. They allow 4 | you to control which source identities are searched in. They are separate from 5 | the identifiers so that they can be swapped out independently from the 6 | identifier method (form, jwt, basic auth). 7 | 8 | Also the resolver is in charge of implementing the query logic. 9 | 10 | So for example if you want to lookup an user based on 11 | `username OR email AND password` you'll have to set up these conditions in your 12 | resolver. The reason for that is, that it is simply not possible to cover all 13 | possibilities of every implementation. 14 | 15 | ## Callback Resolver 16 | 17 | The callback resolver is good starting point for prototyping or might already be 18 | enough for your concrete implementation. Just define a callable and pass it as 19 | constructor to the resolver. 20 | 21 | Here is a *very* simple *example* that just checks if a given username is in a 22 | list of provided usernames and compares the passwords. 23 | 24 | ```php 25 | use Phauthentic\Identifier\Resolver\CallbackResolver; 26 | 27 | $userList = [ 28 | 'florian' => 'password', 29 | 'robert' => 'password' 30 | ]; 31 | 32 | // You could wrap this in a class implementing __invoke() as well 33 | $callback = function($conditions) use ($userList) { 34 | if (isset($conditions['username']) 35 | && isset($userList[$conditions['username']]) 36 | && $userList[$conditions['username']] === $conditions['password']) 37 | ) { 38 | return ['username' => $conditions['username']]; 39 | } 40 | 41 | return null; 42 | ); 43 | 44 | $resolver = new CallbackResolver($callback); 45 | ``` 46 | 47 | Instead of using an array for the user data here, you could pass an instance of 48 | your favorite ORM object into the callable as well and implement your logic in 49 | it to resolve the user. 50 | 51 | ## PDO Statement Resolver 52 | 53 | The PDO Statement resolver will, as the name implies, take an instance of 54 | `\PDOStatement`. Prepare your query and you'll have to use the `:placeholder` 55 | notation, so that the resolver can insert the correct values. 56 | 57 | Check the [official documentation for prepared statements](https://www.php.net/manual/en/pdo.prepared-statements.php) 58 | for more information. 59 | 60 | The names might be different depending on your configuration of the identifiers 61 | that pass the data as an array of `['name' => 'value']` to your resolver. 62 | 63 | Remember, it's up to your resolver to implement the query. So write it according 64 | your needs. Sometimes you might want to use the username to compare it against 65 | an `username` and `email` field. So write your statement as you need it. 66 | 67 | If you need to know more about PDO please [read the PDO documentation on php.net](https://www.php.net/manual/en/book.pdo.php) 68 | 69 | ```php 70 | use PDO; 71 | use Phauthentic\Identifier\Resolver\PdoStatementResolver; 72 | 73 | // Get your PDO instance from your library / framework or create it 74 | $pdo = new PDO(getenv('sqlite::memory:')); 75 | $statement = $statement = $pdo->query('SELECT * FROM users WHERE username = :username AND password = :password'); 76 | 77 | // You could also query username and email: 78 | // SELECT * FROM users WHERE username = :username OR email = :username AND password = :password 79 | 80 | $resolver = new PdoStatementResolver($statement); 81 | ``` 82 | 83 | ## Writing your own resolver 84 | 85 | Any ORM or datasource can be adapted to work with authentication by creating a 86 | resolver. Resolvers must implement `\Phauthentic\Authentication\Identifier\Resolver\ResolverInterface`. 87 | 88 | Resolver can be configured using setter methods, same as the identifiers. 89 | -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The recommended way of installing and upgrading the library is [Composer](https://getcomposer.org/). Of course you can install the library manually but we **don't** provide official help or support for any other way of installing the library. 4 | 5 | ## Via Composer 6 | 7 | Install the library with [Composer](https://getcomposer.org/). 8 | 9 | Global Composer setup: 10 | ```sh 11 | composer require phauthentic/authentication 12 | ``` 13 | 14 | With the phar version of Composer: 15 | ```sh 16 | php composer.phar require cakephp/authentication 17 | ``` 18 | 19 | ## Manual installation 20 | 21 | Download a release [from Github](https://github.com/Phauthentic/authentication/releases) and unpack it in a location of your choice inside your application. Then set up whatever autoloader system you're using. See [the official php documentation for registering autoloaders](http://php.net/manual/en/language.oop5.autoload.php). 22 | 23 | You can install the library this way, there are no technical reason you can't but we don't provide official help or support for any other way of installing the library other than Composer. 24 | -------------------------------------------------------------------------------- /docs/JWT-Example.md: -------------------------------------------------------------------------------- 1 | # JWT Example 2 | 3 | Requirements for this document: 4 | 5 | * You know how [JWT](https://jwt.io/) works 6 | * You've read [the quick start and introduction](Quick-start-and-introduction.md) guide 7 | 8 | Assuming you've read the quick start guide, you'll remember the `getAuthenticationService()` method. Inside this method you already added the `FormAuthenticator`, now you're going to add the `JwtAuthenticator` to the `$authenticatorCollection`: 9 | 10 | ```php 11 | $authenticatorCollection->(new JwtAuthenticator( 12 | new JwtSubjectIdentifier( 13 | $resolver 14 | ), 15 | 'your-jwt-secret-goes-here' 16 | )); 17 | ``` 18 | 19 | This is enough for a very simple JWT authentication. For additional settings and ways to configure JWT take a look at the [authentictor](Authenticators.md) and [identifier](Identifiers.md) documentation. 20 | 21 | This is a *very* simple request handler or controller action. The middleware has set the identity attribute to your request object if your form login was successful. You can now use the data it contains with your JWT token. Make sure you're not getting a ton of data, the user id should be enough. 22 | 23 | ```php 24 | declare(strict_types = 1); 25 | 26 | namespace App\Application\Http\Login\Action; 27 | 28 | use Firebase\JWT\JWT; 29 | use Psr\Http\Message\ServerRequestInterface; 30 | 31 | class LoginAction 32 | { 33 | public function handle(ServerRequestInterface $request) 34 | { 35 | $identity = $request->getAttribute('identity'); 36 | 37 | if ($identity) { 38 | return [ 39 | 'token' => JWT::encode($identity, 'secret key') 40 | ]; 41 | } 42 | 43 | return [ 44 | 'error' => 'Invalid credentials' 45 | ]; 46 | } 47 | } 48 | ``` 49 | 50 | However your framework or your implementation is resolving the result of an action, you might need to return something else. Keep in mind, that this here is just a very simple example to give you an idea of how to use the library and how to get the token. 51 | -------------------------------------------------------------------------------- /docs/PSR15-Middleware.md: -------------------------------------------------------------------------------- 1 | # PSR15 Middleware 2 | 3 | The library comes with a PSR 15 middleware implementation. The middleware expects a service provider object. The object must implement the `Phauthentic\Authentication\AuthenticationServiceProviderInterface`. 4 | 5 | ```php 6 | namespace Application\ServiceProvider; 7 | 8 | use Phauthentic\Authentication\AuthenticationService; 9 | use Phauthentic\Authentication\AuthenticationServiceInterface; 10 | use Phauthentic\Authentication\AuthenticationServiceProviderInterface; 11 | use Phauthentic\Authentication\Identifier\PasswordIdentifier; 12 | use Phauthentic\Authentication\Identifier\Resolver\CallbackResolver; 13 | use Phauthentic\Authentication\Identity\DefaultIdentityFactory; 14 | use Phauthentic\Authentication\Middleware\AuthenticationMiddleware; 15 | use Phauthentic\PasswordHasher\DefaultPasswordHasher; 16 | 17 | /** 18 | * Authentication Service Provider 19 | */ 20 | class AuthenticationServiceProvider implements AuthenticationServiceProviderInterface 21 | { 22 | /** 23 | * Gets an instance of the authentication service 24 | */ 25 | public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface 26 | { 27 | // Configure the service here or use your favorite DI container 28 | // there are plenty of possibilities! 29 | $authenticatorCollection = new AuthenticatorCollection(); 30 | 31 | $authenticatorCollection->add(new FormAuthenticator( 32 | new PasswordIdenfier( 33 | new CallbackResolver(function() { 34 | return true; 35 | }), 36 | new DefaultPasswordHasher() 37 | ), 38 | new DefaultUrlChecekr() 39 | )); 40 | 41 | return new AuthenticationService($authenticatorCollection, new DefaultIdentityFactory()); 42 | } 43 | } 44 | 45 | ``` 46 | 47 | Then use it in your middleware: 48 | 49 | ```php 50 | use \Application\ServiceProvider\AuthenticationServiceProvider; 51 | use \Phauthentic\Authentication\Middleware\AuthenticationMiddleware; 52 | 53 | $middleware = new AuthenticationMiddleware(new AuthenticationServiceProvider()); 54 | 55 | // Now just add the middleware to your middleware queue 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/PSR7-Middleware.md: -------------------------------------------------------------------------------- 1 | # PSR7 Middleware 2 | 3 | Because PSR7 doesn't define an interface for the middleware, we assume the 4 | presence of the PSR ServerRequestInterface **and** ResponseInterface in your 5 | iddleware queue. 6 | 7 | The *example* implementation here is assuming that your queue handler is a 8 | callable. 9 | 10 | It should be very easy to implement the authentication library in a PSR7 11 | middleware by using the `AuthenticationService`. 12 | 13 | ```php 14 | use Psr\Http\Message\ResponseInterface; 15 | use Psr\Http\Message\ServerRequestInterface; 16 | 17 | /** 18 | * PSR 7 Authenticator Middleware 19 | */ 20 | class Psr7AuthenticationMiddleware extends AuthenticationMiddleware 21 | { 22 | 23 | public function handle(ServerRequestInterface $request, ResponseInterface $response, callable $next) 24 | { 25 | $service = $this->provider->getAuthenticationService($request); 26 | $request = $this->addAttribute($request, $this->serviceAttribute, $service); 27 | 28 | $service->authenticate($request); 29 | 30 | $identity = $service->getIdentity(); 31 | $request = $this->addAttribute($request, $this->identityAttribute, $identity); 32 | 33 | $response = $next($request, $response); 34 | if ($response instanceof ResponseInterface) { 35 | $result = $service->persistIdentity($request, $response); 36 | 37 | return $result->getResponse(); 38 | } 39 | 40 | return $response; 41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/Quick-start-and-introduction.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ## Installation 4 | 5 | Install the library with [composer](https://getcomposer.org/). 6 | 7 | ```sh 8 | composer require phauthentic/authentication 9 | ``` 10 | 11 | ## Configuration 12 | 13 | Example of configuring the authentication middleware using `authentication` application hook. 14 | 15 | **This is an example**! 16 | 17 | Please note that it is recommended to implement the service provider in its own 18 | class and pass it to the middleware as an instance. This makes it easy to use 19 | composition via a DI container to get your objects tied together. 20 | 21 | ```php 22 | namespace MyApp; 23 | 24 | use Application\Http\BaseApplication; 25 | use Phauthentic\Authentication\AuthenticationService; 26 | use Phauthentic\Authentication\AuthenticationServiceProviderInterface; 27 | use Phauthentic\Authentication\Authenticator\AuthenticatorCollection; 28 | use Phauthentic\Authentication\Authenticator\FormAuthenticator; 29 | use Phauthentic\Authentication\Identifier\PasswordIdentifier; 30 | use Phauthentic\Authentication\Identifier\Resolver\PdoStatementResolver; 31 | use Phauthentic\Authentication\Identity\DefaultIdentityFactory; 32 | use Phauthentic\Authentication\Middleware\AuthenticationMiddleware; 33 | use Phauthentic\Authentication\UrlChecker\DefaultUrlChecker; 34 | use Phauthentic\PasswordHasher\DefaultPasswordHasher; 35 | use Psr\Http\Message\ResponseInterface; 36 | use Psr\Http\Message\ServerRequestInterface; 37 | 38 | class Application extends BaseApplication implements AuthenticationServiceProviderInterface 39 | { 40 | /** 41 | * Returns a service provider instance. 42 | * 43 | * @param \Psr\Http\Message\ServerRequestInterface $request Request 44 | * @param \Psr\Http\Message\ResponseInterface $response Response 45 | * @return \Phauthentic\Authentication\AuthenticationServiceInterface 46 | */ 47 | public function getAuthenticationService(ServerRequestInterface $request): \Phauthentic\Authentication\AuthenticationServiceInterface 48 | { 49 | $authenticatorCollection = new AuthenticatorCollection(); 50 | 51 | // The PdoStatementResolver needs a PDO Statement object 52 | // Put your real connection in here if you want to use this code 53 | // But it is suggested to inject this 54 | $pdo = new PDO(getenv('sqlite::memory:')); 55 | $statement = $statement = $pdo->query('SELECT * FROM users WHERE username = :username AND password = :password'); 56 | 57 | $authenticatorCollection->add(new FormAuthenticator( 58 | new PasswordIdentifier( 59 | new PdoStatementResolver($statement), 60 | new DefaultPasswordHasher() 61 | ), 62 | new DefaultUrlChecker() 63 | )); 64 | 65 | return new AuthenticationService( 66 | $authenticatorCollection, 67 | new DefaultIdentityFactory() 68 | ); 69 | } 70 | 71 | /** 72 | * @param mixed $middlewareQueue Whatever your middleware queue implementation is 73 | * @return mixed 74 | */ 75 | public function middleware($middlewareQueue) 76 | { 77 | // Various other middlewares for error handling, routing etc. added here. 78 | 79 | // Add the authentication middleware 80 | $authentication = new AuthenticationMiddleware($this); 81 | 82 | // Add the middleware to the middleware queue 83 | $middlewareQueue->add($authentication); 84 | 85 | return $middlewareQueue; 86 | } 87 | } 88 | 89 | ``` 90 | 91 | If one of the configured authenticators was able to validate the credentials, 92 | the middleware will add the authentication service to the request object as an 93 | attribute. If you're not yet familiar with request attributes [check the PSR7 94 | documentation](http://www.php-fig.org/psr/psr-7/). 95 | 96 | ## Using Stateless Authenticators with other Authenticators 97 | 98 | When using `HttpBasic` or `HttpDigest` with other authenticators, you should 99 | remember that these authenticators will halt the request when authentication 100 | credentials are missing or invalid. This is necessary as these authenticators 101 | must send specific challenge headers in the response. If you want to combine 102 | `HttpBasic` or `HttpDigest` with other authenticators, you may want to configure 103 | these authenticators as the *last* authenticators: 104 | 105 | ```php 106 | use Phauthentic\Authentication\AuthenticationService; 107 | use Phauthentic\Authentication\Authenticator\SessionAuthenticator; 108 | use Phauthentic\Authentication\Authenticator\HttpBasicAuthentcator; 109 | use Phauthentic\Authentication\Authenticator\SessionAuthenticator; 110 | use Phauthentic\Authentication\Authenticator\AuthenticatorCollection; 111 | 112 | $authenticatorCollection = new AuthenticatorCollection(); 113 | 114 | $authenticatorCollection->add(new SessionAuthenticator(/*...*/); 115 | $authenticatorCollection->add(new FormAuthenticator(/*...*/); 116 | // Load the authenticators leaving Basic as the last one! 117 | $authenticatorCollection->add(new HttpBasicAuthentcator(/*...*/); 118 | 119 | $service = new AuthenticationService($authenticatorCollection); 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/URL-Checkers.md: -------------------------------------------------------------------------------- 1 | # URL Checkers 2 | 3 | To provide an abstract and framework agnostic solution there are URL checkers 4 | implemented that allow you to customize the comparision of the current URL if 5 | needed. For example to another frameworks routing. 6 | 7 | URL checkers use setter methods and sometimes constructor args to set 8 | configuration options. 9 | 10 | ## Included Checkers 11 | 12 | ### DefaultUrlChecker 13 | 14 | The default checker allows you to compare an URL by regex or string URLs. 15 | 16 | Option setters: 17 | 18 | * **setCheckFullUrl(bool $check)**: To compare the full URL, including protocol, host and port or not. Default is `false` 19 | * **setUseRegex(bool $useRegex)**: Compares the URL by a regular expression provided in the `$loginUrls` argument of the checker. 20 | 21 | ### CakeRouterUrlChecker 22 | 23 | Option setters: 24 | 25 | Use this checker if you want to use the array notation of CakePHPs routing system. The checker also works with named routes. 26 | 27 | * **setCheckFullUrl(bool $check)**: To compare the full URL, including protocol, host and port or not. Default is `false` 28 | 29 | ## Implementing your own Checker 30 | 31 | An URL checker **must** implement the ``\Phauthentication\Authentication\UrlChecker\UrlCheckterInterface``. 32 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./src 7 | ./tests 8 | 9 | -------------------------------------------------------------------------------- /phpmd.baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /phpmd.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | PHPMD Rules 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | checkGenericClassInNonGenericObjectType: false 3 | level: 8 4 | paths: 5 | - src 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](LICENSE) 4 | [![Codecov](https://img.shields.io/codecov/c/github/phauthentic/authentication/master?style=for-the-badge)](https://app.codecov.io/gh/burzum/authentication) 5 | [![Code Quality](https://img.shields.io/scrutinizer/g/Phauthentic/authentication/master.svg?style=for-the-badge)](https://scrutinizer-ci.com/g/Phauthentic/authentication/?branch=master) 6 | ![phpstan Level 8](https://img.shields.io/badge/phpstan-Level%208-brightgreen?style=for-the-badge) 7 | ![php 8.0](https://img.shields.io/badge/php-8.0-blue?style=for-the-badge) 8 | 9 | 10 | This library intends to provide a framework around authentication and user identification. Authorization is a [separate concern](https://en.wikipedia.org/wiki/Separation_of_concerns). 11 | 12 | ## Installation 13 | 14 | You can install this library using [composer](http://getcomposer.org): 15 | 16 | ``` 17 | composer require phauthentic/authentication 18 | ``` 19 | 20 | ## Requirements 21 | 22 | Your application **must** use the [PSR 7 HTTP Message interfaces](https://github.com/php-fig/http-message) for your request and response objects. The whole library is build to be framework agnostic but uses these interfaces as the common API. Every modern and well written framework and application should fulfill this requirement. 23 | 24 | * php >= 8.0 25 | * [psr/http-message](https://github.com/php-fig/http-message) 26 | 27 | Only if you plan to use the PSR-15 middleware: 28 | 29 | * [psr/http-server-handler](https://github.com/php-fig/http-server-handler) 30 | * [psr/http-factory](https://github.com/php-fig/http-factory) 31 | * [psr/http-server-middleware](https://github.com/php-fig/http-server-middleware) 32 | 33 | ## Documentation 34 | 35 | * [Architectural Overview](docs/Architecture-Overview.md) 36 | * [Quick Start and Introduction](docs/Quick-start-and-introduction.md) 37 | * [JWT Example](docs/JWT-Example.md) 38 | * [Authenticators](docs/Authenticators.md) 39 | * [Session](docs/Authenticators.md#session) 40 | * [Token](docs/Authenticators.md#token) 41 | * [JWT](docs/Authenticators.md#jwt) 42 | * [HTTP Basic](docs/Authenticators.md#httpbasic) 43 | * [HTTP Digest](docs/Authenticators.md#httpdigest) 44 | * [Cookie](docs/Authenticators.md#cookie-authenticator-aka-remember-me) 45 | * [OAuth](docs/Authenticators.md#oauth) 46 | * [Identifiers](docs/Identifiers.md) 47 | * [Identity Resolvers](docs/Identity-Resolvers.md) 48 | * [Callback Resolver](docs/Identity-Resolvers.md#callback-resolver) 49 | * [PDO Statement Resolver](docs/Identity-Resolvers.md#pdo-statement-resolver) 50 | * [Writing your own Resolver](docs/Identity-Resolvers.md#writing-your-own-resolver) 51 | * [Identity Objects](docs/Identity-Object.md) 52 | * [URL Checkers](docs/URL-Checkers.md) 53 | * [PSR15 Middleware](docs/PSR15-Middleware.md) 54 | * [PSR7 Middleware](docs/PSR7-Middleware.md) 55 | 56 | ## Copyright & License 57 | 58 | Licensed under the [MIT license](LICENSE.txt). 59 | 60 | * Copyright (c) [Phauthentic](https://github.com/Phauthentic) 61 | * Copyright (c) [Cake Software Foundation, Inc.](https://cakefoundation.org) 62 | -------------------------------------------------------------------------------- /src/AuthenticationException.php: -------------------------------------------------------------------------------- 1 | 43 | */ 44 | protected AuthenticatorCollectionInterface $authenticators; 45 | 46 | /** 47 | * Authenticator that successfully authenticated the identity. 48 | * 49 | * @var \Phauthentic\Authentication\Authenticator\AuthenticatorInterface|null 50 | */ 51 | protected ?AuthenticatorInterface $successfulAuthenticator; 52 | 53 | /** 54 | * A list of failed authenticators after an authentication attempt 55 | * 56 | * @var \Phauthentic\Authentication\Authenticator\FailureInterface[] 57 | */ 58 | protected array $failures = []; 59 | 60 | /** 61 | * Identity object. 62 | * 63 | * @var \Phauthentic\Authentication\Identity\IdentityInterface|null 64 | */ 65 | protected ?IdentityInterface $identity; 66 | 67 | /** 68 | * Result of the last authenticate() call. 69 | * 70 | * @var \Phauthentic\Authentication\Authenticator\ResultInterface|null 71 | */ 72 | protected ?ResultInterface $result; 73 | 74 | /** 75 | * Identity factory used to instantiate an identity object 76 | * 77 | * @var \Phauthentic\Authentication\Identity\IdentityFactoryInterface 78 | */ 79 | protected IdentityFactoryInterface $identityFactory; 80 | 81 | /** 82 | * Constructor 83 | * 84 | * @param \Phauthentic\Authentication\Authenticator\AuthenticatorCollectionInterface<\Phauthentic\Authentication\Authenticator\AuthenticatorInterface> $authenticators Authenticator collection. 85 | * @param \Phauthentic\Authentication\Identity\IdentityFactoryInterface $factory Identity factory. 86 | */ 87 | public function __construct( 88 | AuthenticatorCollectionInterface $authenticators, 89 | IdentityFactoryInterface $factory 90 | ) { 91 | $this->authenticators = $authenticators; 92 | $this->identityFactory = $factory; 93 | } 94 | 95 | /** 96 | * Access the authenticator collection 97 | * 98 | * @return \Phauthentic\Authentication\Authenticator\AuthenticatorCollectionInterface<\Phauthentic\Authentication\Authenticator\AuthenticatorInterface> 99 | */ 100 | public function authenticators(): AuthenticatorCollectionInterface 101 | { 102 | return $this->authenticators; 103 | } 104 | 105 | /** 106 | * Checks if at least one authenticator is in the collection 107 | * 108 | * @throws \RuntimeException 109 | * @return void 110 | */ 111 | protected function checkAuthenticators(): void 112 | { 113 | if ($this->authenticators()->isEmpty()) { 114 | throw new RuntimeException( 115 | 'No authenticators loaded. You need to load at least one authenticator.' 116 | ); 117 | } 118 | } 119 | 120 | /** 121 | * {@inheritDoc} 122 | * 123 | * @throws \RuntimeException Throws a runtime exception when no authenticators are loaded. 124 | */ 125 | public function authenticate(ServerRequestInterface $request): bool 126 | { 127 | $this->checkAuthenticators(); 128 | $this->identity = null; 129 | $this->successfulAuthenticator = null; 130 | $this->failures = []; 131 | 132 | $result = null; 133 | foreach ($this->authenticators() as $authenticator) { 134 | /* @var $authenticator \Phauthentic\Authentication\Authenticator\AuthenticatorInterface */ 135 | $result = $authenticator->authenticate($request); 136 | if ($result->isValid()) { 137 | $this->successfulAuthenticator = $authenticator; 138 | $this->result = $result; 139 | 140 | return true; 141 | } 142 | 143 | if ($result->isValid() === false) { 144 | if ($authenticator instanceof StatelessInterface) { 145 | $authenticator->unauthorizedChallenge($request); 146 | } 147 | 148 | $this->failures[] = new Failure($authenticator, $result); 149 | } 150 | } 151 | 152 | $this->result = $result; 153 | 154 | return false; 155 | } 156 | 157 | /** 158 | * {@inheritDoc} 159 | */ 160 | public function getFailures(): array 161 | { 162 | return $this->failures; 163 | } 164 | 165 | /** 166 | * Clears the identity from authenticators that store them and the request 167 | * 168 | * @param \Psr\Http\Message\ServerRequestInterface $request The request. 169 | * @param \Psr\Http\Message\ResponseInterface $response The response. 170 | * @return \Phauthentic\Authentication\PersistenceResultInterface Return an array containing the request and response objects. 171 | */ 172 | public function clearIdentity( 173 | ServerRequestInterface $request, 174 | ResponseInterface $response 175 | ): PersistenceResultInterface { 176 | foreach ($this->authenticators() as $authenticator) { 177 | if ($authenticator instanceof PersistenceInterface) { 178 | $response = $authenticator->clearIdentity($request, $response); 179 | } 180 | } 181 | 182 | 183 | $this->resetInternalState(); 184 | 185 | return new PersistenceResult($request, $response); 186 | } 187 | 188 | /** 189 | * Resets the internal state of the service 190 | * 191 | * @return void 192 | */ 193 | protected function resetInternalState(): void 194 | { 195 | $this->identity = null; 196 | $this->result = null; 197 | $this->successfulAuthenticator = null; 198 | $this->failures = []; 199 | } 200 | 201 | /** 202 | * Sets identity data and persists it in the authenticators that support it. 203 | * 204 | * @param \Psr\Http\Message\ServerRequestInterface $request The request. 205 | * @param \Psr\Http\Message\ResponseInterface $response The response. 206 | * @param \Phauthentic\Authentication\Identity\IdentityInterface|null $identity Identity. 207 | * @return \Phauthentic\Authentication\PersistenceResultInterface 208 | */ 209 | public function persistIdentity( 210 | ServerRequestInterface $request, 211 | ResponseInterface $response, 212 | IdentityInterface $identity = null 213 | ): PersistenceResultInterface { 214 | if ($identity === null) { 215 | $identity = $this->getIdentity(); 216 | } 217 | 218 | if ($identity !== null) { 219 | foreach ($this->authenticators() as $authenticator) { 220 | if ($authenticator instanceof PersistenceInterface) { 221 | $response = $authenticator->persistIdentity($request, $response, $identity->getOriginalData()); 222 | } 223 | } 224 | } 225 | 226 | return new PersistenceResult($request, $response); 227 | } 228 | 229 | /** 230 | * Gets the successful authenticator instance if one was successful after calling authenticate 231 | * 232 | * @return \Phauthentic\Authentication\Authenticator\AuthenticatorInterface|null 233 | */ 234 | public function getSuccessfulAuthenticator(): ?AuthenticatorInterface 235 | { 236 | return $this->successfulAuthenticator; 237 | } 238 | 239 | /** 240 | * Gets the result of the last authenticate() call. 241 | * 242 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface|null Authentication result interface 243 | */ 244 | public function getResult(): ?ResultInterface 245 | { 246 | return $this->result; 247 | } 248 | 249 | /** 250 | * Gets an identity object 251 | * 252 | * @return null|\Phauthentic\Authentication\Identity\IdentityInterface 253 | */ 254 | public function getIdentity(): ?IdentityInterface 255 | { 256 | if ($this->result === null || !$this->result->isValid()) { 257 | return null; 258 | } 259 | 260 | $data = $this->result->getData(); 261 | if ($data instanceof IdentityInterface || $data === null) { 262 | return $data; 263 | } 264 | 265 | if ($this->identity === null) { 266 | $this->identity = $this->buildIdentity($data); 267 | } 268 | 269 | return $this->identity; 270 | } 271 | 272 | /** 273 | * Builds the identity object 274 | * 275 | * @param \ArrayAccess $data Identity data 276 | * @return \Phauthentic\Authentication\Identity\IdentityInterface 277 | */ 278 | public function buildIdentity(ArrayAccess $data): IdentityInterface 279 | { 280 | return $this->identityFactory->create($data); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/AuthenticationServiceInterface.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 47 | } 48 | 49 | /** 50 | * Gets the identifier. 51 | * 52 | * @return \Phauthentic\Authentication\Identifier\IdentifierInterface 53 | */ 54 | public function getIdentifier(): IdentifierInterface 55 | { 56 | return $this->identifier; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Authenticator/AuthenticatorCollection.php: -------------------------------------------------------------------------------- 1 | $autheticators Authenticators 40 | */ 41 | public function __construct(iterable $autheticators = []) 42 | { 43 | foreach ($autheticators as $authenticator) { 44 | $this->add($authenticator); 45 | } 46 | } 47 | 48 | /** 49 | * Returns true if a collection is empty. 50 | * 51 | * @return bool 52 | */ 53 | public function isEmpty(): bool 54 | { 55 | return empty($this->authenticators); 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | public function add(AuthenticatorInterface $authenticator): void 62 | { 63 | $this->authenticators[] = $authenticator; 64 | } 65 | 66 | /** 67 | * Retrieve an external iterator 68 | * 69 | * @link http://php.net/manual/en/iteratoraggregate.getiterator.php 70 | * @return Traversable<\Phauthentic\Authentication\Authenticator\AuthenticatorInterface> 71 | */ 72 | public function getIterator(): Traversable 73 | { 74 | return new ArrayIterator($this->authenticators); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Authenticator/AuthenticatorCollectionInterface.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 72 | $this->passwordHasher = $passwordHasher; 73 | $this->urlChecker = $urlChecker; 74 | } 75 | 76 | /** 77 | * Sets "remember me" form field name. 78 | * 79 | * @param string $field Field name. 80 | * @return $this 81 | */ 82 | public function setRememberMeField(string $field): self 83 | { 84 | $this->rememberMeField = $field; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | */ 92 | public function authenticate(ServerRequestInterface $request): ResultInterface 93 | { 94 | $token = $this->storage->read($request); 95 | 96 | if ($token === null) { 97 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [ 98 | 'Login credentials not found' 99 | ]); 100 | } 101 | 102 | if (!is_array($token) || count($token) !== 2) { 103 | return new Result(null, Result::FAILURE_CREDENTIALS_INVALID, [ 104 | 'Cookie token is invalid.' 105 | ]); 106 | } 107 | 108 | [$username, $tokenHash] = $token; 109 | 110 | $data = $this->identifier->identify([ 111 | IdentifierInterface::CREDENTIAL_USERNAME => $username, 112 | ]); 113 | 114 | if (empty($data)) { 115 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->identifier->getErrors()); 116 | } 117 | 118 | if (!$this->checkToken($data, $tokenHash)) { 119 | return new Result(null, Result::FAILURE_CREDENTIALS_INVALID, [ 120 | 'Cookie token does not match' 121 | ]); 122 | } 123 | 124 | return new Result($data, Result::SUCCESS); 125 | } 126 | 127 | /** 128 | * {@inheritDoc} 129 | * @throws \JsonException 130 | */ 131 | public function persistIdentity( 132 | ServerRequestInterface $request, 133 | ResponseInterface $response, 134 | ArrayAccess $data 135 | ): ResponseInterface { 136 | $field = $this->rememberMeField; 137 | $bodyData = $request->getParsedBody(); 138 | 139 | if (!$this->checkUrl($request) || !is_array($bodyData) || empty($bodyData[$field])) { 140 | return $response; 141 | } 142 | 143 | $token = $this->createToken($data); 144 | 145 | return $this->storage->write($request, $response, $token); 146 | } 147 | 148 | /** 149 | * {@inheritDoc} 150 | */ 151 | public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 152 | { 153 | return $this->storage->clear($request, $response); 154 | } 155 | 156 | /** 157 | * Creates a plain part of a cookie token. 158 | * 159 | * Returns concatenated username and password hash. 160 | * 161 | * @param \ArrayAccess $data Identity data. 162 | * @return string 163 | */ 164 | protected function createPlainToken(ArrayAccess $data): string 165 | { 166 | $usernameField = $this->credentialFields[IdentifierInterface::CREDENTIAL_USERNAME]; 167 | $passwordField = $this->credentialFields[IdentifierInterface::CREDENTIAL_PASSWORD]; 168 | 169 | return $data[$usernameField] . $data[$passwordField]; 170 | } 171 | 172 | /** 173 | * Creates a full cookie token serialized as a JSON sting. 174 | * Cookie token consists of a username and hashed username + password hash. 175 | * 176 | * @param \ArrayAccess $data Identity data. 177 | * @return string 178 | * @throws \JsonException 179 | */ 180 | protected function createToken(ArrayAccess $data): string 181 | { 182 | $plain = $this->createPlainToken($data); 183 | $hash = $this->passwordHasher->hash($plain); 184 | 185 | $usernameField = $this->credentialFields[IdentifierInterface::CREDENTIAL_USERNAME]; 186 | 187 | return (string)json_encode([$data[$usernameField], $hash], JSON_THROW_ON_ERROR); 188 | } 189 | 190 | /** 191 | * Checks whether a token hash matches the identity data. 192 | * 193 | * @param \ArrayAccess $data Identity data. 194 | * @param string $tokenHash Hashed part of a cookie token. 195 | * @return bool 196 | */ 197 | protected function checkToken(ArrayAccess $data, string $tokenHash): bool 198 | { 199 | $plain = $this->createPlainToken($data); 200 | 201 | return $this->passwordHasher->check($plain, $tokenHash); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Authenticator/CredentialFieldsTrait.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | protected array $credentialFields = [ 34 | IdentifierInterface::CREDENTIAL_USERNAME => 'username', 35 | IdentifierInterface::CREDENTIAL_PASSWORD => 'password' 36 | ]; 37 | 38 | /** 39 | * Set the fields used to to get the credentials from 40 | * 41 | * @param string $username Username field 42 | * @param string $password Password field 43 | * @return $this 44 | */ 45 | public function setCredentialFields(string $username, string $password): self 46 | { 47 | $this->credentialFields[IdentifierInterface::CREDENTIAL_USERNAME] = $username; 48 | $this->credentialFields[IdentifierInterface::CREDENTIAL_PASSWORD] = $password; 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Authenticator/Exception/AuthenticationExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | protected array $headers = []; 33 | 34 | /** 35 | * @var string 36 | */ 37 | protected string $body = ''; 38 | 39 | /** 40 | * Constructor 41 | * 42 | * @param array $headers The headers that should be sent in the unauthorized challenge response. 43 | * @param string $body The response body that should be sent in the challenge response. 44 | * @param int $code The exception code that will be used as a HTTP status code 45 | * @param \Throwable|null $previous Previous exception 46 | */ 47 | public function __construct(array $headers, string $body = '', $code = 401, Throwable $previous = null) 48 | { 49 | parent::__construct('Authentication is required to continue', $code, $previous); 50 | $this->headers = $headers; 51 | $this->body = $body; 52 | } 53 | 54 | /** 55 | * Get the headers. 56 | * 57 | * @return array 58 | */ 59 | public function getHeaders(): array 60 | { 61 | return $this->headers; 62 | } 63 | 64 | /** 65 | * Get the body. 66 | * 67 | * @return string 68 | */ 69 | public function getBody(): string 70 | { 71 | return $this->body; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Authenticator/Failure.php: -------------------------------------------------------------------------------- 1 | authenticator = $authenticator; 40 | $this->result = $result; 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function getAuthenticator(): AuthenticatorInterface 47 | { 48 | return $this->authenticator; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function getResult(): ResultInterface 55 | { 56 | return $this->result; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Authenticator/FailureInterface.php: -------------------------------------------------------------------------------- 1 | urlChecker = $urlChecker; 44 | } 45 | 46 | /** 47 | * Checks the fields to ensure they are supplied. 48 | * 49 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 50 | * @return array|null Username and password retrieved from a request body. 51 | */ 52 | protected function getData(ServerRequestInterface $request): ?array 53 | { 54 | $body = (array)$request->getParsedBody(); 55 | 56 | $data = []; 57 | foreach ($this->credentialFields as $key => $field) { 58 | if (!isset($body[$field])) { 59 | return null; 60 | } 61 | 62 | $value = $body[$field]; 63 | if (!is_string($value) || $value === '') { 64 | return null; 65 | } 66 | 67 | $data[$key] = $value; 68 | } 69 | 70 | return $data; 71 | } 72 | 73 | /** 74 | * Prepares the error object for a login URL error 75 | * 76 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 77 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface 78 | */ 79 | protected function buildLoginUrlErrorResult($request): ResultInterface 80 | { 81 | $errors = [ 82 | sprintf( 83 | 'Login URL `%s` did not match `%s`.', 84 | (string)$request->getUri(), 85 | implode('` or `', $this->loginUrls) 86 | ) 87 | ]; 88 | 89 | return new Result(null, Result::FAILURE_OTHER, $errors); 90 | } 91 | 92 | /** 93 | * Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields` 94 | * to find POST data that is used to find a matching record in the `config.userModel`. Will return false if 95 | * there is no post data, either username or password is missing, or if the scope conditions have not been met. 96 | * 97 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 98 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface 99 | */ 100 | public function authenticate(ServerRequestInterface $request): ResultInterface 101 | { 102 | if (!$this->checkUrl($request)) { 103 | return $this->buildLoginUrlErrorResult($request); 104 | } 105 | 106 | $data = $this->getData($request); 107 | if ($data === null) { 108 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [ 109 | 'Login credentials not found' 110 | ]); 111 | } 112 | 113 | $user = $this->identifier->identify($data); 114 | 115 | if (empty($user)) { 116 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->identifier->getErrors()); 117 | } 118 | 119 | return new Result($user, Result::SUCCESS); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Authenticator/HttpBasicAuthenticator.php: -------------------------------------------------------------------------------- 1 | realm = $realm; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Authenticate a user using HTTP auth. Will use the configured User model and attempt a 56 | * login using HTTP auth. 57 | * 58 | * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with. 59 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface 60 | */ 61 | public function authenticate(ServerRequestInterface $request): ResultInterface 62 | { 63 | $user = $this->getUser($request); 64 | 65 | if (empty($user)) { 66 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); 67 | } 68 | 69 | return new Result($user, Result::SUCCESS); 70 | } 71 | 72 | /** 73 | * Checks for the user and password in the server request params 74 | * 75 | * @param array $serverParams Server params from \Psr\Http\Message\ServerRequestInterface::getServerParams() 76 | * @return bool 77 | */ 78 | protected function checkServerParams(array $serverParams): bool 79 | { 80 | return !isset($serverParams['PHP_AUTH_USER'], $serverParams['PHP_AUTH_PW']) 81 | || !is_string($serverParams['PHP_AUTH_USER']) 82 | || $serverParams['PHP_AUTH_USER'] === '' 83 | || !is_string($serverParams['PHP_AUTH_PW']) 84 | || $serverParams['PHP_AUTH_PW'] === ''; 85 | } 86 | 87 | /** 88 | * Get a user based on information in the request. Used by cookie-less auth for stateless clients. 89 | * 90 | * @param \Psr\Http\Message\ServerRequestInterface $request Request object. 91 | * @return \ArrayAccess|null User entity or null on failure. 92 | */ 93 | public function getUser(ServerRequestInterface $request): ?ArrayAccess 94 | { 95 | $serverParams = $request->getServerParams(); 96 | if ($this->checkServerParams($serverParams)) { 97 | return null; 98 | } 99 | 100 | return $this->identifier->identify([ 101 | IdentifierInterface::CREDENTIAL_USERNAME => $serverParams['PHP_AUTH_USER'], 102 | IdentifierInterface::CREDENTIAL_PASSWORD => $serverParams['PHP_AUTH_PW'], 103 | ]); 104 | } 105 | 106 | /** 107 | * Create a challenge exception for basic auth challenge. 108 | * 109 | * @param \Psr\Http\Message\ServerRequestInterface $request A request object. 110 | * @return void 111 | * @throws \Phauthentic\Authentication\Authenticator\Exception\UnauthorizedException 112 | */ 113 | public function unauthorizedChallenge(ServerRequestInterface $request): void 114 | { 115 | throw new UnauthorizedException($this->loginHeaders($request), ''); 116 | } 117 | 118 | /** 119 | * Generate the login headers 120 | * 121 | * @param \Psr\Http\Message\ServerRequestInterface $request Request object. 122 | * @return array Headers for logging in. 123 | */ 124 | protected function loginHeaders(ServerRequestInterface $request): array 125 | { 126 | $server = $request->getServerParams(); 127 | $realm = $this->realm ?: $server['SERVER_NAME']; 128 | 129 | return ['WWW-Authenticate' => sprintf('Basic realm="%s"', $realm)]; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Authenticator/HttpDigestAuthenticator.php: -------------------------------------------------------------------------------- 1 | secret = $secret; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Sets the Qop 86 | * 87 | * @param string $qop Qop 88 | * @return $this 89 | */ 90 | public function setQop(string $qop): self 91 | { 92 | $this->qop = $qop; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Sets the Nonce Lifetime 99 | * 100 | * @param int $lifeTime Lifetime 101 | * @return $this 102 | */ 103 | public function setNonceLifetime(int $lifeTime): self 104 | { 105 | $this->nonceLifetime = $lifeTime; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Sets the Opaque 112 | * 113 | * @param string|null $opaque Opaque 114 | * @return $this 115 | */ 116 | public function setOpaque(?string $opaque): self 117 | { 118 | $this->opaque = $opaque; 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * Sets the password field name 125 | * 126 | * @param string $field Field name 127 | * @return $this 128 | */ 129 | public function setPasswordField(string $field) 130 | { 131 | $this->credentialFields[IdentifierInterface::CREDENTIAL_PASSWORD] = $field; 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * Get a user based on information in the request. Used by cookie-less auth for stateless clients. 138 | * 139 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 140 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface 141 | */ 142 | public function authenticate(ServerRequestInterface $request): ResultInterface 143 | { 144 | $digest = $this->getDigest($request); 145 | if ($digest === null) { 146 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); 147 | } 148 | 149 | $user = $this->identifier->identify([ 150 | IdentifierInterface::CREDENTIAL_USERNAME => $digest['username'] 151 | ]); 152 | 153 | if (empty($user)) { 154 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); 155 | } 156 | 157 | if (!$this->isNonceValid($digest['nonce'])) { 158 | return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); 159 | } 160 | 161 | $field = $this->credentialFields[IdentifierInterface::CREDENTIAL_PASSWORD]; 162 | $password = $user[$field]; 163 | 164 | $server = $request->getServerParams(); 165 | if (!isset($server['ORIGINAL_REQUEST_METHOD'])) { 166 | $server['ORIGINAL_REQUEST_METHOD'] = $server['REQUEST_METHOD']; 167 | } 168 | 169 | $hash = $this->generateResponseHash($digest, $password, $server['ORIGINAL_REQUEST_METHOD']); 170 | if (hash_equals($hash, $digest['response'])) { 171 | return new Result($user, Result::SUCCESS); 172 | } 173 | 174 | return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); 175 | } 176 | 177 | /** 178 | * Gets the digest headers from the request/environment. 179 | * 180 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 181 | * @return array|null Array of digest information. 182 | */ 183 | protected function getDigest(ServerRequestInterface $request): ?array 184 | { 185 | $server = $request->getServerParams(); 186 | $digest = empty($server['PHP_AUTH_DIGEST']) ? null : $server['PHP_AUTH_DIGEST']; 187 | $digest = $this->getDigestFromApacheHeaders($digest); 188 | 189 | if (empty($digest)) { 190 | return null; 191 | } 192 | 193 | return $this->parseAuthData($digest); 194 | } 195 | 196 | /** 197 | * Fallback to apache_request_headers() 198 | * 199 | * @param null|string $digest Digest 200 | * @return null|string 201 | */ 202 | protected function getDigestFromApacheHeaders(?string $digest) 203 | { 204 | if (empty($digest) && function_exists('apache_request_headers')) { 205 | $headers = (array)apache_request_headers(); 206 | if (!empty($headers['Authorization']) && strpos($headers['Authorization'], 'Digest ') === 0) { 207 | $digest = substr($headers['Authorization'], 7); 208 | } 209 | } 210 | 211 | return $digest; 212 | } 213 | 214 | /** 215 | * Parse the digest authentication headers and split them up. 216 | * 217 | * @param string $digest The raw digest authentication headers. 218 | * @return array|null An array of digest authentication headers 219 | */ 220 | public function parseAuthData(string $digest): ?array 221 | { 222 | if (strpos($digest, 'Digest ') === 0) { 223 | $digest = substr($digest, 7); 224 | } 225 | $keys = $match = []; 226 | $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; 227 | preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER); 228 | 229 | foreach ($match as $i) { 230 | $keys[$i[1]] = $i[3]; 231 | unset($req[$i[1]]); 232 | } 233 | 234 | if (empty($req)) { 235 | return $keys; 236 | } 237 | 238 | return null; 239 | } 240 | 241 | /** 242 | * Generate the response hash for a given digest array. 243 | * 244 | * @param array $digest Digest information containing data from HttpDigestAuthenticate::parseAuthData(). 245 | * @param string $password The digest hash password generated with HttpDigestAuthenticate::password() 246 | * @param string $method Request method 247 | * @return string Response hash 248 | */ 249 | public function generateResponseHash(array $digest, string $password, string $method): string 250 | { 251 | return md5( 252 | $password . 253 | ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . 254 | md5($method . ':' . $digest['uri']) 255 | ); 256 | } 257 | 258 | /** 259 | * Creates an auth digest password hash to store 260 | * 261 | * @param string $username The username to use in the digest hash. 262 | * @param string $password The unhashed password to make a digest hash for. 263 | * @param string $realm The realm the password is for. 264 | * @return string the hashed password that can later be used with Digest authentication. 265 | */ 266 | public static function generatePasswordHash(string $username, string $password, string $realm): string 267 | { 268 | return md5($username . ':' . $realm . ':' . $password); 269 | } 270 | 271 | /** 272 | * @param \Psr\Http\Message\ServerRequestInterface $request 273 | * @return array 274 | */ 275 | protected function getDigestOptions(ServerRequestInterface $request): array 276 | { 277 | $server = $request->getServerParams(); 278 | $realm = $this->realm ?: $server['SERVER_NAME']; 279 | 280 | return [ 281 | 'realm' => $realm, 282 | 'qop' => $this->qop, 283 | 'nonce' => $this->generateNonce(), 284 | 'opaque' => $this->opaque ?: md5($realm) 285 | ]; 286 | } 287 | 288 | protected function formatOptions(string $key, mixed $value): string 289 | { 290 | if (is_bool($value)) { 291 | $value = $value ? 'true' : 'false'; 292 | return sprintf('%s=%s', $key, $value); 293 | } 294 | 295 | return sprintf('%s="%s"', $key, $value); 296 | } 297 | 298 | /** 299 | * Generate the login headers 300 | * 301 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 302 | * @return array Headers for logging in. 303 | */ 304 | protected function loginHeaders(ServerRequestInterface $request): array 305 | { 306 | $options = $this->getDigestOptions($request); 307 | $digest = $this->getDigest($request); 308 | 309 | if ($digest !== null && isset($digest['nonce']) && !$this->isNonceValid($digest['nonce'])) { 310 | $options['stale'] = true; 311 | } 312 | 313 | $formattedOptions = []; 314 | foreach ($options as $key => $value) { 315 | $formattedOptions[] = $this->formatOptions($key, $value); 316 | } 317 | 318 | return ['WWW-Authenticate' => 'Digest ' . implode(',', $formattedOptions)]; 319 | } 320 | 321 | /** 322 | * Generate a nonce value that is validated in future requests. 323 | * 324 | * @return string 325 | */ 326 | protected function generateNonce(): string 327 | { 328 | $expiryTime = microtime(true) + $this->nonceLifetime; 329 | $signatureValue = hash_hmac('sha1', $expiryTime . ':' . $this->secret, $this->secret); 330 | $nonceValue = $expiryTime . ':' . $signatureValue; 331 | 332 | return base64_encode($nonceValue); 333 | } 334 | 335 | /** 336 | * Check the nonce to ensure it is valid and not expired. 337 | * 338 | * @param string $nonce The nonce value to check. 339 | * @return bool 340 | */ 341 | protected function isNonceValid(string $nonce): bool 342 | { 343 | $value = base64_decode($nonce); 344 | if (!is_string($value)) { 345 | return false; 346 | } 347 | $parts = explode(':', $value); 348 | if (count($parts) !== 2) { 349 | return false; 350 | } 351 | [$expires, $checksum] = $parts; 352 | if ($expires < microtime(true)) { 353 | return false; 354 | } 355 | $secret = $this->secret; 356 | $check = hash_hmac('sha1', $expires . ':' . $secret, $secret); 357 | 358 | return hash_equals($check, $checksum); 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/Authenticator/JwtAuthenticator.php: -------------------------------------------------------------------------------- 1 | 57 | */ 58 | protected array $algorithms = [ 59 | 'HS256' 60 | ]; 61 | 62 | /** 63 | * Return payload 64 | * 65 | * @var bool 66 | */ 67 | protected bool $returnPayload = true; 68 | 69 | /** 70 | * Secret key 71 | * 72 | * @var null|string 73 | */ 74 | protected ?string $secretKey; 75 | 76 | /** 77 | * Payload data. 78 | * 79 | * @var object|null 80 | */ 81 | protected ?object $payload = null; 82 | 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function __construct(IdentifierInterface $identifier, string $secretKey) 87 | { 88 | parent::__construct($identifier); 89 | 90 | $this->secretKey = $secretKey; 91 | } 92 | 93 | /** 94 | * Sets algorithms to use 95 | * 96 | * @param array $algorithms List of algorithms 97 | * @return $this 98 | */ 99 | public function setAlgorithms(array $algorithms): self 100 | { 101 | $this->algorithms = $algorithms; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Sets return payload. 108 | * 109 | * @param bool $return Return payload. 110 | * @return $this 111 | */ 112 | public function setReturnPayload(bool $return): self 113 | { 114 | $this->returnPayload = $return; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * Sets secret key. 121 | * 122 | * @param string $key Secret key. 123 | * @return $this 124 | */ 125 | public function setSecretKey(string $key): self 126 | { 127 | $this->secretKey = $key; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Authenticates the identity based on a JWT token contained in a request. 134 | * 135 | * @link https://jwt.io/ 136 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 137 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface 138 | */ 139 | public function authenticate(ServerRequestInterface $request): ResultInterface 140 | { 141 | try { 142 | $result = $this->getPayload($request); 143 | } catch (Exception $e) { 144 | return new Result( 145 | null, 146 | Result::FAILURE_CREDENTIALS_INVALID, 147 | [ 148 | 'message' => $e->getMessage(), 149 | 'exception' => $e 150 | ] 151 | ); 152 | } 153 | 154 | if (!($result instanceof stdClass)) { 155 | return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); 156 | } 157 | 158 | $result = json_decode((string)json_encode($result), true); 159 | 160 | $key = IdentifierInterface::CREDENTIAL_JWT_SUBJECT; 161 | if (empty($result[$key])) { 162 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); 163 | } 164 | 165 | if ($this->returnPayload) { 166 | $user = new ArrayObject($result); 167 | 168 | return new Result($user, Result::SUCCESS); 169 | } 170 | 171 | $user = $this->identifier->identify([ 172 | $key => $result[$key] 173 | ]); 174 | 175 | if (empty($user)) { 176 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->identifier->getErrors()); 177 | } 178 | 179 | return new Result($user, Result::SUCCESS); 180 | } 181 | 182 | /** 183 | * Get payload data. 184 | * 185 | * @param \Psr\Http\Message\ServerRequestInterface|null $request Request to get authentication information from. 186 | * @return object|null Payload object on success, null on failure 187 | */ 188 | public function getPayload(ServerRequestInterface $request = null) 189 | { 190 | if (!$request) { 191 | return $this->payload; 192 | } 193 | 194 | $payload = null; 195 | $token = $this->getToken($request); 196 | 197 | if ($token !== null) { 198 | $payload = $this->decodeToken($token); 199 | } 200 | 201 | $this->payload = $payload; 202 | 203 | return $this->payload; 204 | } 205 | 206 | /** 207 | * Decode JWT token. 208 | * 209 | * @param string $token JWT token to decode. 210 | * @return object|null The JWT's payload as a PHP object, null on failure. 211 | */ 212 | protected function decodeToken($token) 213 | { 214 | return JWT::decode( 215 | $token, 216 | new Key((string)$this->secretKey, $this->algorithms[0]) 217 | ); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Authenticator/PersistenceInterface.php: -------------------------------------------------------------------------------- 1 | 49 | */ 50 | protected array $errors = []; 51 | 52 | /** 53 | * Sets the result status, identity, and failure messages 54 | * 55 | * @param null|\ArrayAccess $data The identity data 56 | * @param string $status Status constant equivalent. 57 | * @param mixed[] $messages Messages. 58 | * @throws \InvalidArgumentException When invalid identity data is passed. 59 | */ 60 | public function __construct(?ArrayAccess $data, string $status, array $messages = []) 61 | { 62 | if ($status === self::SUCCESS && $data === null) { 63 | throw new InvalidArgumentException('Identity data can not be empty with status success.'); 64 | } 65 | 66 | $this->status = $status; 67 | $this->data = $data; 68 | $this->errors = $messages; 69 | } 70 | 71 | /** 72 | * Returns whether the result represents a successful authentication attempt. 73 | * 74 | * @return bool 75 | */ 76 | public function isValid(): bool 77 | { 78 | return $this->status === ResultInterface::SUCCESS; 79 | } 80 | 81 | /** 82 | * Get the result status for this authentication attempt. 83 | * 84 | * @return string 85 | */ 86 | public function getStatus(): string 87 | { 88 | return $this->status; 89 | } 90 | 91 | /** 92 | * Returns the identity data used in the authentication attempt. 93 | * 94 | * @return \ArrayAccess|null 95 | */ 96 | public function getData(): ?ArrayAccess 97 | { 98 | return $this->data; 99 | } 100 | 101 | /** 102 | * Returns an array of string reasons why the authentication attempt was unsuccessful. 103 | * 104 | * If authentication was successful, this method returns an empty array. 105 | * 106 | * @return mixed[] 107 | */ 108 | public function getErrors(): array 109 | { 110 | return $this->errors; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Authenticator/ResultInterface.php: -------------------------------------------------------------------------------- 1 | 77 | */ 78 | public function getErrors(): array; 79 | } 80 | -------------------------------------------------------------------------------- /src/Authenticator/SessionAuthenticator.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | protected array $credentialFields = [ 36 | IdentifierInterface::CREDENTIAL_USERNAME => 'username', 37 | ]; 38 | 39 | /** 40 | * @var bool 41 | */ 42 | protected bool $verify = false; 43 | 44 | /** 45 | * @var \Phauthentic\Authentication\Authenticator\Storage\StorageInterface 46 | */ 47 | protected StorageInterface $storage; 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function __construct( 53 | IdentifierInterface $identifiers, 54 | StorageInterface $storage 55 | ) { 56 | parent::__construct($identifiers); 57 | 58 | $this->storage = $storage; 59 | } 60 | 61 | /** 62 | * Set the fields to use to verify a user by. 63 | * 64 | * @param array $fields Credential fields. 65 | * @return $this 66 | */ 67 | public function setCredentialFields(array $fields): self 68 | { 69 | $this->credentialFields = $fields; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Enable identity verification after it is retrieved from the session storage. 76 | * 77 | * @return $this 78 | */ 79 | public function enableVerification(): self 80 | { 81 | $this->verify = true; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Disable identity verification after it is retrieved from the session storage. 88 | * 89 | * @return $this 90 | */ 91 | public function disableVerification(): self 92 | { 93 | $this->verify = false; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Authenticate a user using session data. 100 | * 101 | * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with. 102 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface 103 | */ 104 | public function authenticate(ServerRequestInterface $request): ResultInterface 105 | { 106 | $user = $this->storage->read($request); 107 | 108 | if (empty($user)) { 109 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND); 110 | } 111 | 112 | if ($this->verify) { 113 | $credentials = []; 114 | foreach ($this->credentialFields as $key => $field) { 115 | $credentials[$key] = $user[$field]; 116 | } 117 | $user = $this->identifier->identify($credentials); 118 | 119 | if (empty($user)) { 120 | return new Result(null, Result::FAILURE_CREDENTIALS_INVALID); 121 | } 122 | } 123 | 124 | if (!($user instanceof ArrayAccess)) { 125 | $user = new ArrayObject($user); 126 | } 127 | 128 | return new Result($user, Result::SUCCESS); 129 | } 130 | 131 | /** 132 | * {@inheritDoc} 133 | */ 134 | public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 135 | { 136 | return $this->storage->clear($request, $response); 137 | } 138 | 139 | /** 140 | * {@inheritDoc} 141 | */ 142 | public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, ArrayAccess $data): ResponseInterface 143 | { 144 | return $this->storage->write($request, $response, $data); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Authenticator/StatelessInterface.php: -------------------------------------------------------------------------------- 1 | key = $sessionKey; 40 | } 41 | 42 | /** 43 | * Set session key for stored identity. 44 | * 45 | * @param string $key Session key. 46 | * @return $this 47 | */ 48 | public function setKey(string $key): self 49 | { 50 | $this->key = $key; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function clear(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 59 | { 60 | unset($_SESSION[$this->key]); 61 | 62 | return $response; 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public function read(ServerRequestInterface $request) 69 | { 70 | if (!isset($_SESSION[$this->key])) { 71 | return null; 72 | } 73 | 74 | return $_SESSION[$this->key]; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function write(ServerRequestInterface $request, ResponseInterface $response, $data): ResponseInterface 81 | { 82 | $_SESSION[$this->key] = (array)$data; 83 | 84 | return $response; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Authenticator/Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | header = $name; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Sets the query param to get the token from 67 | * 68 | * @param null|string $queryParam Query param 69 | * @return $this 70 | */ 71 | public function setQueryParam(?string $queryParam): self 72 | { 73 | $this->queryParam = $queryParam; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Sets the token prefix 80 | * 81 | * @param null|string $prefix Token prefix 82 | * @return $this 83 | */ 84 | public function setTokenPrefix(?string $prefix): self 85 | { 86 | $this->tokenPrefix = $prefix; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Checks if the token is in the headers or a request parameter 93 | * 94 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 95 | * @return string|null 96 | */ 97 | protected function getToken(ServerRequestInterface $request): ?string 98 | { 99 | $token = $this->getTokenFromHeader($request, $this->header); 100 | if ($token === null) { 101 | $token = $this->getTokenFromQuery($request, $this->queryParam); 102 | } 103 | 104 | $prefix = $this->tokenPrefix; 105 | if ($prefix !== null && is_string($token)) { 106 | return $this->stripTokenPrefix($token, $prefix); 107 | } 108 | 109 | return $token; 110 | } 111 | 112 | /** 113 | * Strips a prefix from a token 114 | * 115 | * @param string $token Token string 116 | * @param string $prefix Prefix to strip 117 | * @return string 118 | */ 119 | protected function stripTokenPrefix(string $token, string $prefix): string 120 | { 121 | return str_ireplace($prefix . ' ', '', $token); 122 | } 123 | 124 | /** 125 | * Gets the token from the request headers 126 | * 127 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 128 | * @param string|null $headerLine Header name 129 | * @return string|null 130 | */ 131 | protected function getTokenFromHeader(ServerRequestInterface $request, ?string $headerLine): ?string 132 | { 133 | if (!empty($headerLine)) { 134 | $header = $request->getHeaderLine($headerLine); 135 | if (!empty($header)) { 136 | return $header; 137 | } 138 | } 139 | 140 | return null; 141 | } 142 | 143 | /** 144 | * Gets the token from the request headers 145 | * 146 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 147 | * @param string|null $queryParam Request query parameter name 148 | * @return string|null 149 | */ 150 | protected function getTokenFromQuery(ServerRequestInterface $request, ?string $queryParam): ?string 151 | { 152 | $queryParams = $request->getQueryParams(); 153 | 154 | if (empty($queryParams[$queryParam])) { 155 | return null; 156 | } 157 | 158 | return $queryParams[$queryParam]; 159 | } 160 | 161 | /** 162 | * Authenticates the identity contained in a request. Will use the `config.userModel`, and `config.fields` 163 | * to find POST data that is used to find a matching record in the `config.userModel`. Will return false if 164 | * there is no post data, either username or password is missing, or if the scope conditions have not been met. 165 | * 166 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information. 167 | * @return \Phauthentic\Authentication\Authenticator\ResultInterface 168 | */ 169 | public function authenticate(ServerRequestInterface $request): ResultInterface 170 | { 171 | $token = $this->getToken($request); 172 | if ($token === null) { 173 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING); 174 | } 175 | 176 | $user = $this->identifier->identify([ 177 | IdentifierInterface::CREDENTIAL_TOKEN => $token 178 | ]); 179 | 180 | if (empty($user)) { 181 | return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->identifier->getErrors()); 182 | } 183 | 184 | return new Result($user, Result::SUCCESS); 185 | } 186 | 187 | /** 188 | * No-op method. 189 | * 190 | * @param \Psr\Http\Message\ServerRequestInterface $request A request object. 191 | * @return void 192 | */ 193 | public function unauthorizedChallenge(ServerRequestInterface $request): void 194 | { 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Authenticator/UrlAwareTrait.php: -------------------------------------------------------------------------------- 1 | $urls An array of URLs. 44 | * @return $this 45 | */ 46 | public function setLoginUrls(array $urls): self 47 | { 48 | $this->loginUrls = $urls; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Adds a login URL. 55 | * 56 | * @param string $url Login URL. 57 | * @return $this 58 | */ 59 | public function addLoginUrl(string $url): self 60 | { 61 | $this->loginUrls[] = $url; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * URL checker wrapper for multiple URLs. 68 | * Returns true if there are no URLs configured. 69 | * 70 | * @param \Psr\Http\Message\ServerRequestInterface $request Request. 71 | * @return bool 72 | */ 73 | protected function checkUrl(ServerRequestInterface $request): bool 74 | { 75 | if (!$this->loginUrls) { 76 | return true; 77 | } 78 | 79 | foreach ($this->loginUrls as $url) { 80 | if ($this->urlChecker->check($request, $url)) { 81 | return true; 82 | } 83 | } 84 | 85 | return false; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Identifier/AbstractIdentifier.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | protected array $errors = []; 29 | 30 | /** 31 | * Returns errors 32 | * 33 | * @return array 34 | */ 35 | public function getErrors(): array 36 | { 37 | return $this->errors; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Identifier/CallbackIdentifier.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function identify(array $data): ?ArrayAccess 45 | { 46 | $callback = $this->callable; 47 | 48 | return $callback($data); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Identifier/CollectionIdentifier.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | protected array $errors = []; 34 | 35 | /** 36 | * Identifier Collection 37 | * 38 | * @var iterable<\Phauthentic\Authentication\Identifier\IdentifierInterface> 39 | */ 40 | protected iterable $collection; 41 | 42 | /** 43 | * Constructor 44 | * 45 | * @param iterable<\Phauthentic\Authentication\Identifier\IdentifierInterface> $collection Identifier collection. 46 | */ 47 | public function __construct(iterable $collection) 48 | { 49 | $this->collection = $collection; 50 | } 51 | 52 | /** 53 | * Get errors 54 | * 55 | * @return array 56 | */ 57 | public function getErrors(): array 58 | { 59 | return $this->errors; 60 | } 61 | 62 | /** 63 | * Identifies an user or service by the passed credentials 64 | * 65 | * @param array $credentials Authentication credentials 66 | * @return \ArrayAccess|null 67 | */ 68 | public function identify(array $credentials): ?ArrayAccess 69 | { 70 | /** @var \Phauthentic\Authentication\Identifier\IdentifierInterface $identifier */ 71 | foreach ($this->collection as $identifier) { 72 | $result = $identifier->identify($credentials); 73 | if ($result) { 74 | return $result; 75 | } 76 | $this->errors[get_class($identifier)] = $identifier->getErrors(); 77 | } 78 | 79 | return null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Identifier/IdentifierCollection.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | protected array $identifiers; 35 | 36 | /** 37 | * Constructor 38 | * 39 | * @param iterable<\Phauthentic\Authentication\Identifier\IdentifierInterface> $identifiers Identifier objects. 40 | */ 41 | public function __construct(iterable $identifiers = []) 42 | { 43 | foreach ($identifiers as $identifier) { 44 | $this->add($identifier); 45 | } 46 | } 47 | 48 | /** 49 | * Adds an identifier to the collection 50 | * 51 | * @param IdentifierInterface $identifier Identifier 52 | * @return void 53 | */ 54 | public function add(IdentifierInterface $identifier): void 55 | { 56 | $this->identifiers[] = $identifier; 57 | } 58 | 59 | /** 60 | * Returns true if a collection is empty. 61 | * 62 | * @return bool 63 | */ 64 | public function isEmpty(): bool 65 | { 66 | return empty($this->identifiers); 67 | } 68 | 69 | /** 70 | * Returns iterator. 71 | * 72 | * @return \ArrayIterator 73 | */ 74 | public function getIterator(): Traversable 75 | { 76 | return new ArrayIterator($this->identifiers); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Identifier/IdentifierCollectionInterface.php: -------------------------------------------------------------------------------- 1 | $credentials Authentication credentials 37 | * @return \ArrayAccess|null 38 | */ 39 | public function identify(array $credentials): ?ArrayAccess; 40 | 41 | /** 42 | * Gets a list of errors happened in the identification process 43 | * 44 | * @return array 45 | */ 46 | public function getErrors(): array; 47 | } 48 | -------------------------------------------------------------------------------- /src/Identifier/JwtSubjectIdentifier.php: -------------------------------------------------------------------------------- 1 | $options Additional options 38 | * @return void 39 | */ 40 | public function connect(string $host, int $port, array $options): void; 41 | 42 | /** 43 | * Unbind from LDAP directory 44 | * 45 | * @return void 46 | */ 47 | public function unbind(); 48 | 49 | /** 50 | * Get the diagnostic message 51 | * 52 | * @return string|null 53 | */ 54 | public function getDiagnosticMessage(); 55 | } 56 | -------------------------------------------------------------------------------- /src/Identifier/Ldap/ExtensionAdapter.php: -------------------------------------------------------------------------------- 1 | setErrorHandler(); 68 | $result = ldap_bind($this->getConnection(), $bind, $password); 69 | $this->unsetErrorHandler(); 70 | 71 | return $result; 72 | } 73 | 74 | /** 75 | * Get the LDAP connection 76 | * 77 | * @return mixed 78 | * @throws \RuntimeException If the connection is empty 79 | */ 80 | public function getConnection() 81 | { 82 | if (empty($this->connection)) { 83 | throw new RuntimeException('You are not connected to a LDAP server.'); 84 | } 85 | 86 | return $this->connection; 87 | } 88 | 89 | /** 90 | * Connect to an LDAP server 91 | * 92 | * @param string $host Hostname 93 | * @param int $port Port 94 | * @param array $options Additional LDAP options 95 | * @return void 96 | * @throws \ErrorException 97 | */ 98 | public function connect(string $host, int $port, array $options): void 99 | { 100 | $this->setErrorHandler(); 101 | $this->connection = ldap_connect($host, $port) ?: null; 102 | $this->unsetErrorHandler(); 103 | 104 | if (is_array($options)) { 105 | foreach ($options as $option => $value) { 106 | $this->setOption($option, $value); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Set the value of the given option 113 | * 114 | * @param int $option Option to set 115 | * @param int|bool|string $value The new value for the specified option 116 | * @return void 117 | */ 118 | public function setOption(int $option, $value) 119 | { 120 | $this->setErrorHandler(); 121 | ldap_set_option($this->getConnection(), $option, $value); 122 | $this->unsetErrorHandler(); 123 | } 124 | 125 | /** 126 | * Get the current value for given option 127 | * 128 | * @param int $option Option to get 129 | * @return mixed This will be set to the option value. 130 | */ 131 | public function getOption($option) 132 | { 133 | $returnValue = null; 134 | $this->setErrorHandler(); 135 | ldap_get_option($this->getConnection(), $option, $returnValue); 136 | $this->unsetErrorHandler(); 137 | 138 | return $returnValue; 139 | } 140 | 141 | /** 142 | * Get the diagnostic message 143 | * 144 | * @return string|null 145 | */ 146 | public function getDiagnosticMessage() 147 | { 148 | return $this->getOption(LDAP_OPT_DIAGNOSTIC_MESSAGE); 149 | } 150 | 151 | /** 152 | * Unbind from LDAP directory 153 | * 154 | * @return void 155 | */ 156 | public function unbind() 157 | { 158 | $this->setErrorHandler(); 159 | if ($this->connection) { 160 | ldap_unbind($this->connection); 161 | } 162 | $this->unsetErrorHandler(); 163 | 164 | $this->connection = null; 165 | } 166 | 167 | /** 168 | * Set an error handler to turn LDAP errors into exceptions 169 | * 170 | * @return void 171 | * @throws \ErrorException 172 | */ 173 | protected function setErrorHandler() 174 | { 175 | set_error_handler( 176 | function ($errorNumber, $errorText) { 177 | throw new ErrorException($errorText); 178 | }, 179 | E_ALL 180 | ); 181 | } 182 | 183 | /** 184 | * Restore the error handler 185 | * 186 | * @return void 187 | */ 188 | protected function unsetErrorHandler() 189 | { 190 | restore_error_handler(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Identifier/LdapIdentifier.php: -------------------------------------------------------------------------------- 1 | setOptions([ 36 | * LDAP_OPT_PROTOCOL_VERSION => 3 37 | * ]); 38 | * ``` 39 | * 40 | * @link https://github.com/QueenCityCodeFactory/LDAP 41 | */ 42 | class LdapIdentifier extends AbstractIdentifier 43 | { 44 | /** 45 | * Credential fields 46 | * 47 | * @var array 48 | */ 49 | protected array $credentialFields = [ 50 | self::CREDENTIAL_USERNAME => 'username', 51 | self::CREDENTIAL_PASSWORD => 'password' 52 | ]; 53 | 54 | /** 55 | * Host 56 | * 57 | * @var string 58 | */ 59 | protected string $host = ''; 60 | 61 | /** 62 | * Bind DN 63 | * 64 | * @var callable 65 | */ 66 | protected $bindDN; 67 | 68 | /** 69 | * Port 70 | * 71 | * @var int 72 | */ 73 | protected int $port = 389; 74 | 75 | /** 76 | * Adapter Options 77 | * 78 | * @var array 79 | */ 80 | protected array $ldapOptions = []; 81 | 82 | /** 83 | * List of errors 84 | * 85 | * @var array 86 | */ 87 | protected array $errors = []; 88 | 89 | /** 90 | * LDAP connection object 91 | * 92 | * @var \Phauthentic\Authentication\Identifier\Ldap\AdapterInterface 93 | */ 94 | protected AdapterInterface $ldap; 95 | 96 | /** 97 | * {} 98 | * @param \Phauthentic\Authentication\Identifier\Ldap\AdapterInterface $ldapAdapter 99 | * @param string $host 100 | * @param callable $bindDN 101 | * @param int $port 102 | */ 103 | public function __construct(AdapterInterface $ldapAdapter, string $host, callable $bindDN, int $port = 389) 104 | { 105 | $this->ldap = $ldapAdapter; 106 | $this->bindDN = $bindDN; 107 | $this->host = $host; 108 | $this->port = $port; 109 | } 110 | 111 | /** 112 | * Set the fields used to to get the credentials from 113 | * 114 | * @param string $username Username field 115 | * @param string $password Password field 116 | * @return $this 117 | */ 118 | public function setCredentialFields(string $username, string $password): self 119 | { 120 | $this->credentialFields[self::CREDENTIAL_USERNAME] = $username; 121 | $this->credentialFields[self::CREDENTIAL_PASSWORD] = $password; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Sets LDAP options 128 | * 129 | * @param array $options LDAP Options array 130 | * @return $this 131 | */ 132 | public function setLdapOptions(array $options): self 133 | { 134 | $this->ldapOptions = $options; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * {@inheritDoc} 141 | */ 142 | public function identify(array $data): ?ArrayAccess 143 | { 144 | $this->connectLdap(); 145 | $fields = $this->credentialFields; 146 | 147 | if (isset($data[$fields[self::CREDENTIAL_USERNAME]]) && isset($data[$fields[self::CREDENTIAL_PASSWORD]])) { 148 | return $this->bindUser($data[$fields[self::CREDENTIAL_USERNAME]], $data[$fields[self::CREDENTIAL_PASSWORD]]); 149 | } 150 | 151 | return null; 152 | } 153 | 154 | /** 155 | * Returns configured LDAP adapter. 156 | * 157 | * @return \Phauthentic\Authentication\Identifier\Ldap\AdapterInterface 158 | */ 159 | public function getAdapter(): AdapterInterface 160 | { 161 | return $this->ldap; 162 | } 163 | 164 | /** 165 | * Initializes the LDAP connection 166 | * 167 | * @return void 168 | */ 169 | protected function connectLdap() 170 | { 171 | $this->ldap->connect( 172 | $this->host, 173 | $this->port, 174 | $this->ldapOptions 175 | ); 176 | } 177 | 178 | /** 179 | * Try to bind the given user to the LDAP server 180 | * 181 | * @param string $username The username 182 | * @param string $password The password 183 | * @return \ArrayAccess|null 184 | */ 185 | protected function bindUser($username, $password) 186 | { 187 | try { 188 | $callable = $this->bindDN; 189 | $ldapBind = $this->ldap->bind($callable($username), $password); 190 | if ($ldapBind === true) { 191 | $this->ldap->unbind(); 192 | 193 | return new ArrayObject([ 194 | $this->credentialFields[self::CREDENTIAL_USERNAME] => $username 195 | ]); 196 | } 197 | } catch (ErrorException $e) { 198 | $this->handleLdapError($e->getMessage()); 199 | } 200 | $this->ldap->unbind(); 201 | 202 | return null; 203 | } 204 | 205 | /** 206 | * Handles an LDAP error 207 | * 208 | * @param string $message Exception message 209 | * @return void 210 | */ 211 | protected function handleLdapError($message) 212 | { 213 | $extendedError = $this->ldap->getDiagnosticMessage(); 214 | if (!is_null($extendedError)) { 215 | $this->errors[] = $extendedError; 216 | } 217 | $this->errors[] = $message; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Identifier/PasswordIdentifier.php: -------------------------------------------------------------------------------- 1 | 40 | */ 41 | protected array $credentialFields = [ 42 | IdentifierInterface::CREDENTIAL_USERNAME => 'username', 43 | IdentifierInterface::CREDENTIAL_PASSWORD => 'password' 44 | ]; 45 | 46 | /** 47 | * Resolver 48 | * 49 | * @var \Phauthentic\Authentication\Identifier\Resolver\ResolverInterface 50 | */ 51 | protected ResolverInterface $resolver; 52 | 53 | /** 54 | * Password Hasher 55 | * 56 | * @var \Phauthentic\PasswordHasher\PasswordHasherInterface 57 | */ 58 | protected PasswordHasherInterface $passwordHasher; 59 | 60 | /** 61 | * Whether or not the user authenticated by this class 62 | * requires their password to be rehashed with another algorithm. 63 | * 64 | * @var bool 65 | */ 66 | protected bool $needsPasswordRehash = false; 67 | 68 | /** 69 | * Constructor 70 | * 71 | * @param ResolverInterface $resolver Resolver instance. 72 | * @param PasswordHasherInterface $passwordHasher Password hasher. 73 | */ 74 | public function __construct( 75 | ResolverInterface $resolver, 76 | PasswordHasherInterface $passwordHasher 77 | ) { 78 | $this->resolver = $resolver; 79 | $this->passwordHasher = $passwordHasher; 80 | } 81 | 82 | /** 83 | * Set the username fields used to to get the credentials from. 84 | * 85 | * @param array $usernames An array of fields. 86 | * @return $this 87 | */ 88 | public function setUsernameFields(array $usernames): self 89 | { 90 | $this->credentialFields[self::CREDENTIAL_USERNAME] = $usernames; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Set the single username field used to to get the credentials from. 97 | * 98 | * @param string $username Username field. 99 | * @return $this 100 | */ 101 | public function setUsernameField(string $username): self 102 | { 103 | return $this->setUsernameFields([$username]); 104 | } 105 | 106 | /** 107 | * Sets the password field. 108 | * 109 | * @param string $password Password field. 110 | * @return $this 111 | */ 112 | public function setPasswordField(string $password): self 113 | { 114 | $this->credentialFields[self::CREDENTIAL_PASSWORD] = $password; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | public function identify(array $credentials): ?ArrayAccess 123 | { 124 | if (!isset($credentials[self::CREDENTIAL_USERNAME])) { 125 | return null; 126 | } 127 | 128 | $data = $this->findIdentity($credentials[self::CREDENTIAL_USERNAME]); 129 | if (array_key_exists(self::CREDENTIAL_PASSWORD, $credentials)) { 130 | $password = $credentials[self::CREDENTIAL_PASSWORD]; 131 | if (!$this->checkPassword($data, $password)) { 132 | return null; 133 | } 134 | } 135 | 136 | return $data; 137 | } 138 | 139 | /** 140 | * Find a user record using the username and password provided. 141 | * Input passwords will be hashed even when a user doesn't exist. This 142 | * helps mitigate timing attacks that are attempting to find valid usernames. 143 | * 144 | * @param \ArrayAccess|null $data The identity or null. 145 | * @param string|null $password The password. 146 | * @return bool 147 | */ 148 | protected function checkPassword(?ArrayAccess $data, $password): bool 149 | { 150 | $passwordField = $this->credentialFields[self::CREDENTIAL_PASSWORD]; 151 | 152 | if ($data === null) { 153 | $data = new ArrayObject([ 154 | $passwordField => '' 155 | ]); 156 | } 157 | 158 | $hasher = $this->passwordHasher; 159 | $hashedPassword = $data[$passwordField]; 160 | if (!$hasher->check((string)$password, $hashedPassword)) { 161 | return false; 162 | } 163 | 164 | $this->needsPasswordRehash = $hasher->needsRehash($hashedPassword); 165 | 166 | return true; 167 | } 168 | 169 | /** 170 | * Check if a password needs to be re-hashed 171 | * 172 | * @return bool 173 | */ 174 | public function needsPasswordRehash(): bool 175 | { 176 | return $this->needsPasswordRehash; 177 | } 178 | 179 | /** 180 | * Find a user record using the username/identifier provided. 181 | * 182 | * @param string $identifier The username/identifier. 183 | * @return \ArrayAccess|null 184 | */ 185 | protected function findIdentity($identifier): ?ArrayAccess 186 | { 187 | $fields = $this->credentialFields[self::CREDENTIAL_USERNAME]; 188 | 189 | $conditions = []; 190 | foreach ((array)$fields as $field) { 191 | $conditions[$field] = $identifier; 192 | } 193 | 194 | return $this->resolver->find($conditions); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Identifier/Resolver/CallbackResolver.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function find(array $conditions): ?ArrayAccess 48 | { 49 | $callable = $this->callable; 50 | 51 | return $callable($conditions); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Identifier/Resolver/PdoResolver.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 50 | $this->sql = $sql; 51 | } 52 | 53 | /** 54 | * Builds the statement 55 | * 56 | * @return \PDOStatement 57 | */ 58 | protected function buildStatement(): PDOStatement 59 | { 60 | $statement = $this->pdo->prepare($this->sql); 61 | 62 | $error = $this->pdo->errorInfo(); 63 | if ($error[0] !== '00000') { 64 | throw new PDOException($error[2], (int)$error[0]); 65 | } 66 | 67 | if (!$statement instanceof PDOStatement) { 68 | throw new RuntimeException(sprintf( 69 | 'There was an error running your PDO resolver using this query: %s', 70 | $this->sql 71 | )); 72 | } 73 | 74 | return $statement; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function find(array $conditions): ?ArrayAccess 81 | { 82 | foreach ($conditions as $key => $value) { 83 | unset($conditions[$key]); 84 | $conditions[':' . $key] = $value; 85 | } 86 | 87 | $statement = $this->buildStatement(); 88 | $statement->execute($conditions); 89 | $result = $statement->fetchAll(); 90 | 91 | if (empty($result)) { 92 | return null; 93 | } 94 | 95 | return new ArrayObject($result[0]); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Identifier/Resolver/PdoStatementResolver.php: -------------------------------------------------------------------------------- 1 | statement = $statement; 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function find(array $conditions): ?ArrayAccess 53 | { 54 | foreach ($conditions as $key => $value) { 55 | unset($conditions[$key]); 56 | $conditions[':' . $key] = $value; 57 | } 58 | 59 | $this->statement->execute($conditions); 60 | $result = $this->statement->fetchAll(); 61 | 62 | if (empty($result)) { 63 | return null; 64 | } 65 | 66 | return new ArrayObject($result[0]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Identifier/Resolver/ResolverInterface.php: -------------------------------------------------------------------------------- 1 | $conditions Find conditions. 31 | * @return \ArrayAccess|null 32 | */ 33 | public function find(array $conditions): ?ArrayAccess; 34 | } 35 | -------------------------------------------------------------------------------- /src/Identifier/TokenIdentifier.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 58 | } 59 | 60 | /** 61 | * Sets data field 62 | * 63 | * @param null|string $field Field name 64 | * @return $this 65 | */ 66 | public function setDataField(?string $field): self 67 | { 68 | $this->dataField = $field; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Sets the token field 75 | * 76 | * @param string $field Field name 77 | * @return $this 78 | */ 79 | public function setTokenField(string $field): self 80 | { 81 | $this->tokenField = $field; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public function identify(array $data): ?ArrayAccess 90 | { 91 | if (!isset($data[$this->dataField])) { 92 | return null; 93 | } 94 | 95 | $conditions = [ 96 | $this->tokenField => $data[$this->dataField] 97 | ]; 98 | 99 | return $this->resolver->find($conditions); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Identity/DefaultIdentityFactory.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | protected array $config = []; 27 | 28 | /** 29 | * Constructor. 30 | * 31 | * @param array $config Config. 32 | */ 33 | public function __construct(array $config = []) 34 | { 35 | $this->config = $config; 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | public function create(ArrayAccess $data): IdentityInterface 42 | { 43 | return new Identity($data, $this->config); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Identity/Identity.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected array $defaultConfig = [ 37 | 'fieldMap' => [ 38 | 'id' => 'id' 39 | ] 40 | ]; 41 | 42 | /** 43 | * Config 44 | * 45 | * @var array 46 | */ 47 | protected array $config = []; 48 | 49 | /** 50 | * Identity data 51 | * 52 | * @var \ArrayAccess 53 | */ 54 | protected ArrayAccess $data; 55 | 56 | /** 57 | * Constructor 58 | * 59 | * @param \ArrayAccess $data Identity data 60 | * @param array $config Config options 61 | * @throws \InvalidArgumentException When invalid identity data is passed. 62 | */ 63 | public function __construct(ArrayAccess $data, array $config = []) 64 | { 65 | $this->config = array_merge_recursive($this->defaultConfig, $config); 66 | $this->data = $data; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getIdentifier() 73 | { 74 | return $this->get('id'); 75 | } 76 | 77 | /** 78 | * Get data from the identity using object access. 79 | * 80 | * @param string $field Field in the user data. 81 | * @return mixed 82 | */ 83 | public function __get($field) 84 | { 85 | return $this->get($field); 86 | } 87 | 88 | /** 89 | * Check if the field isset() using object access. 90 | * 91 | * @param string $field Field in the user data. 92 | * @return mixed 93 | */ 94 | public function __isset($field) 95 | { 96 | return $this->get($field) !== null; 97 | } 98 | 99 | /** 100 | * Get data from the identity 101 | * 102 | * @param string $field Field in the user data. 103 | * @return mixed 104 | */ 105 | protected function get($field) 106 | { 107 | $map = $this->config['fieldMap']; 108 | if (isset($map[$field])) { 109 | $field = $map[$field]; 110 | } 111 | 112 | if (isset($this->data[$field])) { 113 | return $this->data[$field]; 114 | } 115 | 116 | return null; 117 | } 118 | 119 | /** 120 | * Whether a offset exists 121 | * 122 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php 123 | * @param mixed $offset Offset 124 | * @return bool 125 | */ 126 | public function offsetExists($offset): bool 127 | { 128 | return $this->get($offset) !== null; 129 | } 130 | 131 | /** 132 | * Offset to retrieve 133 | * 134 | * @link http://php.net/manual/en/arrayaccess.offsetget.php 135 | * @param mixed $offset Offset 136 | * @return mixed 137 | */ 138 | public function offsetGet($offset): mixed 139 | { 140 | return $this->get($offset); 141 | } 142 | 143 | /** 144 | * Offset to set 145 | * 146 | * @link http://php.net/manual/en/arrayaccess.offsetset.php 147 | * @param mixed $offset The offset to assign the value to. 148 | * @param mixed $value Value 149 | * @throws \BadMethodCallException 150 | * @return void 151 | */ 152 | public function offsetSet($offset, $value): void 153 | { 154 | throw new BadMethodCallException('Identity does not allow wrapped data to be mutated.'); 155 | } 156 | 157 | /** 158 | * Offset to unset 159 | * 160 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php 161 | * @param mixed $offset Offset 162 | * @throws \BadMethodCallException 163 | * @return void 164 | */ 165 | public function offsetUnset($offset): void 166 | { 167 | throw new BadMethodCallException('Identity does not allow wrapped data to be mutated.'); 168 | } 169 | 170 | /** 171 | * {@inheritDoc} 172 | */ 173 | public function getOriginalData(): ArrayAccess 174 | { 175 | return $this->data; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Identity/IdentityFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | public function getOriginalData(): ArrayAccess; 41 | } 42 | -------------------------------------------------------------------------------- /src/Middleware/AuthenticationErrorHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 57 | $this->streamFactory = $streamFactory; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 64 | { 65 | try { 66 | return $handler->handle($request); 67 | } catch (UnauthorizedException $e) { 68 | return $this->createUnauthorizedResponse($e); 69 | } 70 | } 71 | 72 | /** 73 | * Creates an unauthorized response. 74 | * 75 | * @param \Phauthentic\Authentication\Authenticator\Exception\UnauthorizedException $exception Exception. 76 | * @return \Psr\Http\Message\ResponseInterface 77 | */ 78 | protected function createUnauthorizedResponse(UnauthorizedException $exception): ResponseInterface 79 | { 80 | $body = $this->streamFactory->createStream(); 81 | $body->write($exception->getBody()); 82 | 83 | $response = $this 84 | ->responseFactory 85 | ->createResponse($exception->getCode()) 86 | ->withBody($body); 87 | 88 | foreach ($exception->getHeaders() as $header => $value) { 89 | $response = $response->withHeader($header, $value); 90 | } 91 | 92 | return $response; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Middleware/AuthenticationMiddleware.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 57 | } 58 | 59 | /** 60 | * Sets request attribute name for authentication service. 61 | * 62 | * @param string $attribute Attribute name. 63 | * @return $this 64 | */ 65 | public function setServiceAttribute(string $attribute): self 66 | { 67 | $this->serviceAttribute = $attribute; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Sets the identity attribute 74 | * 75 | * @param string $attribute Attribute name 76 | * @return $this 77 | */ 78 | public function setIdentityAttribute(string $attribute): self 79 | { 80 | $this->identityAttribute = $attribute; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Adds an attribute to the request and returns a modified request. 87 | * 88 | * @param ServerRequestInterface $request Request. 89 | * @param string $name Attribute name. 90 | * @param mixed $value Attribute value. 91 | * @return ServerRequestInterface 92 | * @throws RuntimeException When attribute is present. 93 | */ 94 | protected function addAttribute(ServerRequestInterface $request, string $name, $value): ServerRequestInterface 95 | { 96 | if ($request->getAttribute($name)) { 97 | $message = sprintf('Request attribute `%s` already exists.', $name); 98 | throw new RuntimeException($message); 99 | } 100 | 101 | return $request->withAttribute($name, $value); 102 | } 103 | 104 | /** 105 | * {@inheritDoc} 106 | * 107 | * @throws RuntimeException When request attribute exists. 108 | */ 109 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 110 | { 111 | $service = $this->provider->getAuthenticationService($request); 112 | $request = $this->addAttribute($request, $this->serviceAttribute, $service); 113 | 114 | $service->authenticate($request); 115 | 116 | $identity = $service->getIdentity(); 117 | $request = $this->addAttribute($request, $this->identityAttribute, $identity); 118 | 119 | $response = $handler->handle($request); 120 | 121 | $result = $service->persistIdentity($request, $response); 122 | 123 | return $result->getResponse(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/PersistenceResult.php: -------------------------------------------------------------------------------- 1 | request = $request; 49 | $this->response = $response; 50 | } 51 | 52 | /** 53 | * Returns response. 54 | * 55 | * @return \Psr\Http\Message\ResponseInterface 56 | */ 57 | public function getResponse(): ResponseInterface 58 | { 59 | return $this->response; 60 | } 61 | 62 | /** 63 | * Returns request. 64 | * 65 | * @return \Psr\Http\Message\ServerRequestInterface 66 | */ 67 | public function getRequest(): ServerRequestInterface 68 | { 69 | return $this->request; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/PersistenceResultInterface.php: -------------------------------------------------------------------------------- 1 | checkFullUrl = $fullUrl; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function check(ServerRequestInterface $request, string $loginUrl): bool 51 | { 52 | $requestUrl = $this->getUrlFromRequest($request->getUri()); 53 | 54 | return $requestUrl === $loginUrl; 55 | } 56 | 57 | /** 58 | * Returns current url. 59 | * 60 | * @param \Psr\Http\Message\UriInterface $uri Server Request 61 | * @return string 62 | */ 63 | protected function getUrlFromRequest(UriInterface $uri): string 64 | { 65 | if ($this->checkFullUrl) { 66 | return (string)$uri; 67 | } 68 | 69 | return $uri->getPath(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/UrlChecker/RegexUrlChecker.php: -------------------------------------------------------------------------------- 1 | checkFullUrl = $fullUrl; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function check(ServerRequestInterface $request, string $regex): bool 51 | { 52 | $requestUrl = $this->getUrlFromRequest($request->getUri()); 53 | 54 | return (bool)preg_match($regex, $requestUrl); 55 | } 56 | 57 | /** 58 | * Returns current url. 59 | * 60 | * @param \Psr\Http\Message\UriInterface $uri Server Request 61 | * @return string 62 | */ 63 | protected function getUrlFromRequest(UriInterface $uri): string 64 | { 65 | if ($this->checkFullUrl) { 66 | return (string)$uri; 67 | } 68 | 69 | return $uri->getPath(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/UrlChecker/UrlCheckerInterface.php: -------------------------------------------------------------------------------- 1 |