|
5 | 5 | Table of Contents:
|
6 | 6 |
|
7 | 7 | * [Who are Authenticated Users](#who-are-authenticated-users)
|
| 8 | + * [Accessing Users](#accessing-users) |
| 9 | + [Authorizers](#authorizers) |
| 10 | + * [Request Aware Auhtorizers](#request-aware-auhtorizers) |
| 11 | +* [Transports](#transports) |
| 12 | +* [Authentication Middlewares](#authentication-middlewares) |
8 | 13 | * [Working with Passwords](#working-with-passwords)
|
9 | 14 | * [Hashing and Validating Passwords](#hashing-and-validating-passwords)
|
10 | 15 | * [Password Policy](#password-policy)
|
@@ -51,6 +56,171 @@ class MyUsersRepository implements \ActiveCollab\Authentication\AuthenticatedUse
|
51 | 56 | }
|
52 | 57 | ```
|
53 | 58 |
|
| 59 | +## Authorizers |
| 60 | + |
| 61 | +Authorizers are used to authorize user credentials against data that is stored by a particular authentication service (stored users, LDAP/AD, IdP etc). |
| 62 | + |
| 63 | +Key authorizer method is `verifyCredentials`. It receives an array with credentials and it is expected to return `ActiveCollab\Authentication\AuthenticatedUser\AuthenticatedUserInterface` instance on successful authorization, or `null` when auhtorization is not successful. Some implementations may decide to throw exceptions, to make a clear distinction between various reasons why authorization failed (user not found, invalid password, user account is temporaly or permanently suspended etc). |
| 64 | + |
| 65 | +Example of Authorizer implementation that fetches user from users repository, and validates user's password: |
| 66 | + |
| 67 | +```php |
| 68 | +<?php |
| 69 | + |
| 70 | +namespace MyApp; |
| 71 | + |
| 72 | +use ActiveCollab\Authentication\Authorizer\AuthorizerInterface; |
| 73 | +use ActiveCollab\Authentication\AuthenticatedUser\RepositoryInterface; |
| 74 | +use ActiveCollab\Authentication\Exception\InvalidPasswordException; |
| 75 | +use ActiveCollab\Authentication\Exception\UserNotFoundException; |
| 76 | +use InvalidArgumentException; |
| 77 | + |
| 78 | +class MyAuthorizer implements AuthorizerInterface |
| 79 | +{ |
| 80 | + /** |
| 81 | + * @var RepositoryInterface |
| 82 | + */ |
| 83 | + private $user_repository; |
| 84 | + |
| 85 | + /** |
| 86 | + * @param RepositoryInterface $user_repository |
| 87 | + */ |
| 88 | + public function __construct(RepositoryInterface $user_repository) |
| 89 | + { |
| 90 | + $this->user_repository = $user_repository; |
| 91 | + } |
| 92 | + |
| 93 | + /** |
| 94 | + * {@inheritdoc} |
| 95 | + */ |
| 96 | + public function verifyCredentials(array $credentials) |
| 97 | + { |
| 98 | + if (empty($credentials['username'])) { |
| 99 | + throw new InvalidArgumentException('Username not found in credentials array'); |
| 100 | + } |
| 101 | + |
| 102 | + if (empty($credentials['password'])) { |
| 103 | + throw new InvalidArgumentException('Password not found in credentials array'); |
| 104 | + } |
| 105 | + |
| 106 | + $user = $this->user_repository->findByUsername($credentials['username']); |
| 107 | + |
| 108 | + if (!$user) { |
| 109 | + throw new UserNotFoundException(); |
| 110 | + } |
| 111 | + |
| 112 | + if (!$user->isValidPassword($credentials['password'])) { |
| 113 | + throw new InvalidPasswordException(); |
| 114 | + } |
| 115 | + |
| 116 | + return $user; |
| 117 | + } |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +## Request Aware Auhtorizers |
| 122 | + |
| 123 | +Request aware authorizers go a step further. They offer a mechanism to receive PSR-7 request, and extract credentials and default payload from them (or based on them). This is useful when authorizer requires request data validation and parsing. For example, SAML authorizer will need to parse SAML payload in order to extract relevant credentials from it. For authorizer to become request aware, it additionally needs to implement `ActiveCollab\Authentication\Authorizer\RequestAware\RequestAwareInterface`, and implement request processor that can take in `Psr\Http\Message\ServerRequestInterface` and return processing result: |
| 124 | + |
| 125 | +```php |
| 126 | +<?php |
| 127 | + |
| 128 | +namespace MyApp; |
| 129 | + |
| 130 | +use ActiveCollab\Authentication\Authorizer\AuthorizerInterface; |
| 131 | +use ActiveCollab\Authentication\Authorizer\RequestAware\RequestAwareInterface; |
| 132 | + |
| 133 | +class MyAuthorizer implements AuthorizerInterface, RequestAwareInterface |
| 134 | +{ |
| 135 | + /** |
| 136 | + * {@inheritdoc} |
| 137 | + */ |
| 138 | + public function verifyCredentials(array $credentials) |
| 139 | + { |
| 140 | + } |
| 141 | + |
| 142 | + /** |
| 143 | + * {@inheritdoc} |
| 144 | + */ |
| 145 | + public function getRequestProcessor() |
| 146 | + { |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +## Transports |
| 152 | + |
| 153 | +During authentication and authorization steps, this library returns transport objects that encapsulate all auth elements that are relevant for the given step in the process: |
| 154 | + |
| 155 | +1. `AuthenticationTransportInterface` is returned on initial authentication. It can be empty, when request does not bear any user ID embedded (token, or session), or it can contain information about authenticated user, way of authentication, used adapter etc, when system finds valid ID in the request. |
| 156 | +1. `AuthroizationTransportInterface` is returned when user provides their credentials to the authorizer. |
| 157 | +1. `CleanUpTransportInterface` is returned when there's ID found in the request, but it expired, and needs to be cleaned up. |
| 158 | +1. `DeauthenticationTransportInterface` - is returned when user requests to be logged out of the system. |
| 159 | + |
| 160 | +Authentication and authorization transports can be applied to responses (and requests) to sign them with proper identification data (set or extend user session cookie for example): |
| 161 | + |
| 162 | +```php |
| 163 | +if (!$transport->isApplied()) { |
| 164 | + list ($request, $response) = $transport->applyTo($request, $response); |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +## Authentication Middlewares |
| 169 | + |
| 170 | +`AuthenticationInterface` interface assumes that implementation will be such that it can be invoked as a middleware in a [PSR-7](http://www.php-fig.org/psr/psr-7/) middleware stack. That is why implementation of `__invoke` method in middleware stack fashion is part of the interface. |
| 171 | + |
| 172 | +Default implementation of the interface (`ActiveCollab\Authentication\Authentication`) is implemented in such way that it initializes authentication by looking at server request when it is invoked. Initialization process will look for an ID in the request (token, session cookie, etc, depending on the used adapters), and loading proper user account when found. User and method of authentication (token, session, etc) are set as request attributes (`authenticated_user`, and `authenticated_with` respectively) and passed down the middleware stack. You can check these attributes in inner middlewares: |
| 173 | + |
| 174 | +Here's an example of middleware that checks for authenticated user, and returns 401 Unauthorized status if user is not authenticated: |
| 175 | + |
| 176 | +```php |
| 177 | +use Psr\Http\Message\ResponseInterface; |
| 178 | +use Psr\Http\Message\ServerRequestInterface; |
| 179 | + |
| 180 | +/** |
| 181 | + * @package ActiveCollab\Authentication\Middleware |
| 182 | + */ |
| 183 | +class CheckAuthMiddleware |
| 184 | +{ |
| 185 | + /** |
| 186 | + * @param ServerRequestInterface $request |
| 187 | + * @param ResponseInterface $response |
| 188 | + * @param callable|null $next |
| 189 | + * @return ResponseInterface |
| 190 | + */ |
| 191 | + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) |
| 192 | + { |
| 193 | + if (empty($request->getAttribute('authenticated_user'))) { |
| 194 | + return $response->withStatus(401); |
| 195 | + } |
| 196 | + |
| 197 | + if ($next) { |
| 198 | + $response = $next($request, $response); |
| 199 | + } |
| 200 | + |
| 201 | + return $response; |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +During request handling, authentication can change: |
| 207 | + |
| 208 | +1. User can log in, |
| 209 | +1. User can log out, |
| 210 | +1. System may request that authentication artifacts (like cookies) are cleaned up. |
| 211 | + |
| 212 | +System can communicate these changes by making appropriate authentication transports that encapuslate information aboute these events available as request attributes, and handing them over to `ActiveCollab\Authentication\Middleware\ApplyAuthenticationMiddleware`: |
| 213 | + |
| 214 | +```php |
| 215 | +$middleware_stack->add(new ApplyAuthenticationMiddleware('authentication_transport')); |
| 216 | +``` |
| 217 | + |
| 218 | +This will tell `ApplyAuthenticationMiddleware` to check for `authentication_transport` attribute, and apply it to request and response if found. |
| 219 | + |
| 220 | +**Note**: Reason why we do this in a separate middleware, instead of exiting part of Authentication middleware is because we may need to clean up request (remove invalid cookie for example). |
| 221 | + |
| 222 | + |
| 223 | + |
54 | 224 | ## Working with Passwords
|
55 | 225 |
|
56 | 226 | ### Hashing and Validating Passwords
|
@@ -164,3 +334,4 @@ Login policy implements `\JsonSerializable` interface, and can be safely encoded
|
164 | 334 | ## To Do
|
165 | 335 |
|
166 | 336 | 1. Consider adding previously used passwords repository, so library can enforce no-repeat policy for passwords
|
| 337 | +1. Remove compat `ActiveCollab\Authentication\Adapter\BrowserSession` and `ActiveCollab\Authentication\Adapter\TokenBearer` classes once all apps that use this package are updated to use new classes |
0 commit comments