├── 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 | [](LICENSE)
4 | [](https://app.codecov.io/gh/burzum/authentication)
5 | [](https://scrutinizer-ci.com/g/Phauthentic/authentication/?branch=master)
6 | 
7 | 
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 |