From 56b903238d7042aa7e16734761ad3880360a2aac Mon Sep 17 00:00:00 2001 From: Popa Tiberiu Date: Wed, 8 Mar 2017 22:46:58 +0200 Subject: [PATCH] Develop (#2) * refactoring --- CHANGELOG.md | 15 ++ README.md | 97 ++++----- authentication-web.global.php.dist | 47 +++- composer.json | 14 +- src/Action/LoginAction.php | 192 +++++++++++++---- src/Action/LogoutAction.php | 62 +++--- src/AuthenticationEventTrait.php | 76 ------- src/ConfigProvider.php | 43 ++-- src/ErrorHandler/UnauthorizedHandler.php | 162 +++++++++++--- .../AbstractAuthenticationEventListener.php | 20 ++ src/Event/AuthenticationEvent.php | 98 +-------- .../AuthenticationEventListenerInterface.php | 37 ++++ .../AuthenticationEventListenerTrait.php | 108 ++++++++++ .../DispatchAuthenticationEventTrait.php | 48 +++++ src/Exception/ExceptionInterface.php | 2 + src/Exception/InvalidArgumentException.php | 7 +- src/Exception/RuntimeException.php | 2 + src/Factory/BaseActionFactory.php | 81 +++++++ .../DefaultAuthenticationListenerFactory.php | 48 ----- src/Factory/DefaultLogoutListenerFactory.php | 36 ---- .../DefaultUnauthorizedListenerFactory.php | 42 ---- src/Factory/LoginActionFactory.php | 37 ++-- src/Factory/LogoutActionFactory.php | 23 +- src/Factory/UnauthorizedHandlerFactory.php | 31 +-- .../WebAuthenticationOptionsFactory.php | 7 +- .../DefaultAuthenticationListener.php | 201 ------------------ src/Listener/DefaultLogoutListener.php | 88 -------- src/Listener/DefaultUnauthorizedListener.php | 137 ------------ src/Options/MessagesOptions.php | 60 ++++-- src/Options/WebAuthenticationOptions.php | 152 ++++++------- src/Utils.php | 33 +++ 31 files changed, 958 insertions(+), 1048 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 src/AuthenticationEventTrait.php create mode 100644 src/Event/AbstractAuthenticationEventListener.php create mode 100644 src/Event/AuthenticationEventListenerInterface.php create mode 100644 src/Event/AuthenticationEventListenerTrait.php create mode 100644 src/Event/DispatchAuthenticationEventTrait.php create mode 100644 src/Factory/BaseActionFactory.php delete mode 100644 src/Factory/DefaultAuthenticationListenerFactory.php delete mode 100644 src/Factory/DefaultLogoutListenerFactory.php delete mode 100644 src/Factory/DefaultUnauthorizedListenerFactory.php delete mode 100644 src/Listener/DefaultAuthenticationListener.php delete mode 100644 src/Listener/DefaultLogoutListener.php delete mode 100644 src/Listener/DefaultUnauthorizedListener.php create mode 100644 src/Utils.php diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..976e8ce --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +## 0.1.0 - 2017-03-08 + +Initial tagged release + +### Added +* Everything + +### Deprecated +* Nothing + +### Removed +* Nothing + +### Fixed +* Nothing diff --git a/README.md b/README.md index 44e7da8..bbc28ca 100644 --- a/README.md +++ b/README.md @@ -26,24 +26,57 @@ return [ //this package's specific configuration template 'web' => [ //change next two only if you changed the default login/logout routes - 'login_route' => ['name' => 'login', 'params' => [], 'query_params' => []], - 'logout_route' => ['name' => 'logout', 'params' => []], - + 'login_route' => ['route_name' => 'login', 'route_params' => [], 'query_params' => []], + 'logout_route' => ['route_name' => 'logout', 'route_params' => []], + //template name to use for the login form 'login_template' => 'app::login', - + //where to redirect after login success - 'after_login_route' => ['name' => 'my-account', 'params' => []], + 'after_login_route' => ['route_name' => 'my-account', 'route_params' => []], //where to redirect after logging out - 'after_logout_route' => ['name' => 'login', 'params' => []], - + 'after_logout_route' => ['route_name' => 'login', 'route_params' => []], + //enable the wanted url feature, to login to the previously requested uri after login - 'allow_redirect_param' => true, - 'redirect_param_name' => 'redirect', - + 'enable_wanted_url' => true, + 'wanted_url_name' => 'redirect', + + 'event_listeners' => [ + [ + 'type' => 'Some\Class\Or\Service', + 'priority' => 1 + ], + ], + //for overwriting default module messages 'messages_options' => [ - 'messages' => [], + 'messages' => [ + //MessagesOptions::AUTHENTICATION_FAILURE => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_INVALID_CREDENTIALS => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_IDENTITY_AMBIGUOUS => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_IDENTITY_NOT_FOUND => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_UNCATEGORIZED => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_MISSING_CREDENTIALS => + // 'Authentication failed. Missing or invalid credentials', + + //MessagesOptions::AUTHENTICATION_SUCCESS => + // 'Welcome! You have successfully signed in', + + //MessagesOptions::AUTHENTICATION_FAIL_UNKNOWN => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::UNAUTHORIZED => 'You must sign in first to access the requested content', + ], ], ] ] @@ -52,44 +85,4 @@ return [ ## Login flow -The authentication flow uses [zend-eventmanager](https://github.com/zendframework/zend-eventmanager). We advise you to check the official documentation before. - -Calling the login route and subsequently the LoginAction middleware, an authentication event is triggered. -The actual authentication process happens on a listener registered at priority 1 defined in the listener aggregate `DefaultAuthenticationListener` -Please note the authentication event is triggered on both GET and POST requests. You should check in your listeners the request method before taking the appropriate action. - -There is also a post authentication listener at priority -1000 that checks if there are errors and redirects back to the login page. -If authentication succeeded, it redirects to the after login route or the wanted url. - -You can come with your own listeners to further extend the functionality or even completely rewrite the authentication process. - - -## Logout flow - -Calling LogoutAction middleware, similar to login, it triggers a logout event. We provide a default logout listeners that uses an AuthenticationInterface service to clear the identity from storage. -It also redirects to the after logout route as configured. Again, you can register your own listeners for this event to do additional actions when users log out. - -## Unauthorized exception handling - -A piped error handler middleware is provided to catch UnauthorizedException or any Exception or response that has a 401 code. -In the same vein as login/logout, the unauthorized handler does not process the exception, it delegates instead responsibility to listeners by triggering an unauthorized event. - -The default unauthorized listener process the authentication error messages, setting them as session messages(flash messages) and redirects back to the login route, optionally appending the wanted url to the query. - - -## AuthenticationEvent - -Triggered on login, logout and unauthorized actions, it holds identity information, authentication result and also the authentication service, and current errors, depending on the authentication stage. -Defines 3 types of authentication events -* `AuthenticationEvent::EVENT_AUTHENTICATION_AUTHENTICATE` - triggered when authentication is needed -* `AuthenticationEvent::EVENT_AUTHENTICATION_LOGOUT` - triggered when logout is needed -* `AuthenticationEvent::EVENT_AUTHENTICATION_UNAUTHORIZED` - triggered when an UnauthorizedException or 401 codes are detected - -The package provides default listeners for all these events, in order to provide just the basic functionality of a web authentication flow. - - -## Useful observations - -* The default authentication listener skips if the AuthenticationEvent errors property is not empty. This allows you to have pre authentication listeners to make additional validations for example. -* The AuthenticationEvent->getParams() are sent to the login template, so you can inject your own variables into the template(like the login form, for example) -* If you have listeners that return a ResponseInterface, you basically interrupt the listener chain. You could use this to completely rewrite the authentication flow if needed, by registering listeners before all the default ones that are provided. +@TODO: write full documentation for new version diff --git a/authentication-web.global.php.dist b/authentication-web.global.php.dist index 2fd22cc..fef88d0 100644 --- a/authentication-web.global.php.dist +++ b/authentication-web.global.php.dist @@ -10,24 +10,57 @@ return [ //this package's specific configuration template 'web' => [ //change next two only if you changed the default login/logout routes - 'login_route' => ['name' => 'login', 'params' => [], 'query_params' => []], - 'logout_route' => ['name' => 'logout', 'params' => []], + 'login_route' => ['route_name' => 'login', 'route_params' => [], 'query_params' => []], + 'logout_route' => ['route_name' => 'logout', 'route_params' => []], //template name to use for the login form 'login_template' => 'app::login', //where to redirect after login success - 'after_login_route' => ['name' => 'my-account', 'params' => []], + 'after_login_route' => ['route_name' => 'my-account', 'route_params' => []], //where to redirect after logging out - 'after_logout_route' => ['name' => 'login', 'params' => []], + 'after_logout_route' => ['route_name' => 'login', 'route_params' => []], //enable the wanted url feature, to login to the previously requested uri after login - 'allow_redirect_param' => true, - 'redirect_param_name' => 'redirect', + 'enable_wanted_url' => true, + 'wanted_url_name' => 'redirect', + + 'event_listeners' => [ + [ + 'type' => 'Some\Class\Or\Service', + 'priority' => 1 + ], + ], //for overwriting default module messages 'messages_options' => [ - 'messages' => [], + 'messages' => [ + //MessagesOptions::AUTHENTICATION_FAILURE => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_INVALID_CREDENTIALS => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_IDENTITY_AMBIGUOUS => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_IDENTITY_NOT_FOUND => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_UNCATEGORIZED => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::AUTHENTICATION_MISSING_CREDENTIALS => + // 'Authentication failed. Missing or invalid credentials', + + //MessagesOptions::AUTHENTICATION_SUCCESS => + // 'Welcome! You have successfully signed in', + + //MessagesOptions::AUTHENTICATION_FAIL_UNKNOWN => + // 'Authentication failed. Check your credentials and try again', + + //MessagesOptions::UNAUTHORIZED => 'You must sign in first to access the requested content', + ], ], ] ] diff --git a/composer.json b/composer.json index 575752e..fd37d12 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "dotkernel/dot-authentication-web", "type": "library", - "description": "Login/logout authentication flow for web based applications", + "description": "DotKernel login/logout authentication flow for web based applications", "license": "MIT", "authors": [ { @@ -10,14 +10,14 @@ } ], "require": { - "php": "^5.6 || ^7.0", + "php": "^7.1", "psr/http-message": "^1.0", "container-interop/container-interop": "^1.1", - "dotkernel/dot-authentication": "0.6.x-dev", - "dotkernel/dot-event": "0.6.x-dev", - "dotkernel/dot-helpers": "0.6.x-dev", - "dotkernel/dot-flashmessenger": "0.6.x-dev" + "dotkernel/dot-authentication": "~0.1", + "dotkernel/dot-event": "~0.1", + "dotkernel/dot-helpers": "~0.1", + "dotkernel/dot-flashmessenger": "~0.1" }, "require-dev": { "phpunit/phpunit": "^4.8", @@ -39,7 +39,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.6-dev" + "dev-master": "0.2-dev" } } } diff --git a/src/Action/LoginAction.php b/src/Action/LoginAction.php index 273e7ee..7854631 100644 --- a/src/Action/LoginAction.php +++ b/src/Action/LoginAction.php @@ -7,26 +7,37 @@ * Time: 8:37 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Action; use Dot\Authentication\AuthenticationInterface; -use Dot\Authentication\Web\AuthenticationEventTrait; +use Dot\Authentication\AuthenticationResult; use Dot\Authentication\Web\Event\AuthenticationEvent; +use Dot\Authentication\Web\Event\AuthenticationEventListenerInterface; +use Dot\Authentication\Web\Event\AuthenticationEventListenerTrait; +use Dot\Authentication\Web\Event\DispatchAuthenticationEventTrait; +use Dot\Authentication\Web\Exception\RuntimeException; +use Dot\Authentication\Web\Options\MessagesOptions; use Dot\Authentication\Web\Options\WebAuthenticationOptions; +use Dot\Authentication\Web\Utils; +use Dot\FlashMessenger\FlashMessengerInterface; use Dot\Helpers\Route\RouteOptionHelper; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\Response\HtmlResponse; use Zend\Diactoros\Response\RedirectResponse; -use Zend\EventManager\EventManagerAwareTrait; +use Zend\Diactoros\Uri; +use Zend\Expressive\Template\TemplateRendererInterface; /** * Class LoginAction * @package Dot\Authentication\Web\Action */ -class LoginAction +class LoginAction implements AuthenticationEventListenerInterface { - use EventManagerAwareTrait; - use AuthenticationEventTrait; + use DispatchAuthenticationEventTrait; + use AuthenticationEventListenerTrait; /** @var AuthenticationInterface */ protected $authentication; @@ -37,70 +48,179 @@ class LoginAction /** @var WebAuthenticationOptions */ protected $options; + /** @var FlashMessengerInterface */ + protected $flashMessenger; + + /** @var TemplateRendererInterface */ + protected $template; + + /** @var ServerRequestInterface */ + protected $request; + + /** @var bool */ + protected $debug = false; + /** * LoginAction constructor. * @param AuthenticationInterface $authentication + * @param TemplateRendererInterface $template * @param RouteOptionHelper $routeHelper * @param WebAuthenticationOptions $options + * @param FlashMessengerInterface $flashMessenger */ public function __construct( AuthenticationInterface $authentication, + TemplateRendererInterface $template, RouteOptionHelper $routeHelper, - WebAuthenticationOptions $options + WebAuthenticationOptions $options, + FlashMessengerInterface $flashMessenger ) { $this->authentication = $authentication; $this->options = $options; $this->routeHelper = $routeHelper; + $this->flashMessenger = $flashMessenger; + $this->template = $template; } /** * @param ServerRequestInterface $request * @param ResponseInterface $response * @param callable|null $next - * @return RedirectResponse + * @return ResponseInterface */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) - { + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + callable $next = null + ): ResponseInterface { if ($this->authentication->hasIdentity()) { return new RedirectResponse($this->routeHelper->getUri($this->options->getAfterLoginRoute())); } - $data = []; + $this->request = $request; + if ($request->getMethod() === 'POST') { $data = $request->getParsedBody(); + + $event = $this->dispatchEvent(AuthenticationEvent::EVENT_BEFORE_AUTHENTICATION, [ + 'request' => $request, + 'authenticationService' => $this->authentication, + 'data' => $data + ]); + + if ($event instanceof ResponseInterface) { + return $event; + } + + $error = $event->getParam('error', null); + // get the request in case someone changed it + $request = $event->getParam('request'); + $this->request = $request; + if (empty($error)) { + $result = $this->authentication->authenticate($request); + //we get this in case authentication skipped(due to missing credentials in request) + //but for web application, we want to force implemetors to prepare their auth adapter first + //so we throw an exception to be clear developers have missed something + if ($result->getCode() === AuthenticationResult::FAILURE_MISSING_CREDENTIALS) { + throw new RuntimeException('Authentication service could not authenticate request. ' . + 'Have you forgot to prepare the request first according to authentication adapter needs?'); + } + + $params = $event->getParams(); + $params += [ + 'authenticationResult' => $result + ]; + + if ($result->isValid()) { + $params += [ + 'identity' => $result->getIdentity() + ]; + $event = $this->dispatchEvent(AuthenticationEvent::EVENT_AFTER_AUTHENTICATION, $params); + if ($event instanceof ResponseInterface) { + return $event; + } + + $error = $event->getParam('error'); + if (empty($error)) { + $this->dispatchEvent(AuthenticationEvent::EVENT_AUTHENTICATION_SUCCESS, $params); + + $uri = $this->routeHelper->getUri($this->options->getAfterLoginRoute()); + if ($this->options->isEnableWantedUrl()) { + $params = $request->getQueryParams(); + $wantedUrlName = $this->options->getWantedUrlName(); + + if (isset($params[$wantedUrlName]) && !empty($params[$wantedUrlName])) { + $uri = new Uri(urldecode($params[$wantedUrlName])); + } + } + return new RedirectResponse($uri); + } else { + $this->dispatchEvent(AuthenticationEvent::EVENT_AUTHENTICATION_ERROR, $event->getParams()); + return $this->prgRedirect($error); + } + } else { + $message = $this->options->getMessagesOptions() + ->getMessage(Utils::$authResultCodeToMessageMap[$result->getCode()]); + $params += [ + 'error' => $message + ]; + $this->dispatchEvent(AuthenticationEvent::EVENT_AUTHENTICATION_ERROR, $params); + return $this->prgRedirect($message); + } + } else { + $this->dispatchEvent(AuthenticationEvent::EVENT_AUTHENTICATION_ERROR, $event->getParams()); + return $this->prgRedirect($error); + } } - $result = $this->triggerAuthenticateEvent($request, $response, $data); - if ($result instanceof ResponseInterface) { - return $result; + $event = $this->dispatchEvent(AuthenticationEvent::EVENT_AUTHENTICATION_BEFORE_RENDER, [ + 'request' => $request, + 'authenticationService' => $this->authentication, + 'template' => $this->options->getLoginTemplate() + ]); + if ($event instanceof ResponseInterface) { + return $event; } - error_log( - sprintf( - 'Authentication event handlers should return a ResponseInterface, "%s" returned', - is_object($result) ? get_class($result) : gettype($result) - ), - E_USER_WARNING - ); + $template = $event->getParam('template'); + $params = $event->getParams(); + unset($params['template']); + + return new HtmlResponse($this->template->render($template, $params)); + } + + /** + * @param $error + * @return ResponseInterface + */ + protected function prgRedirect($error): ResponseInterface + { + if (is_array($error) || is_string($error)) { + $this->flashMessenger->addError($error); + } elseif ($error instanceof \Exception && $this->isDebug()) { + $this->flashMessenger->addError($error->getMessage()); + } else { + $this->flashMessenger->addError( + $this->options->getMessagesOptions() + ->getMessage(MessagesOptions::AUTHENTICATION_FAIL_UNKNOWN) + ); + } + return new RedirectResponse($this->request->getUri(), 303); + } - return $next($request, $response); + /** + * @return bool + */ + public function isDebug(): bool + { + return $this->debug; } - public function triggerAuthenticateEvent(ServerRequestInterface $request, ResponseInterface $response, $data) + /** + * @param bool $debug + */ + public function setDebug(bool $debug) { - $event = $this->createAuthenticationEvent( - $this->authentication, - AuthenticationEvent::EVENT_AUTHENTICATION_AUTHENTICATE, - $data, - $request, - $response - ); - - $result = $this->getEventManager()->triggerEventUntil(function ($r) { - return ($r instanceof ResponseInterface); - }, $event); - - $result = $result->last(); - return $result; + $this->debug = $debug; } } diff --git a/src/Action/LogoutAction.php b/src/Action/LogoutAction.php index 45479f4..34a6e1c 100644 --- a/src/Action/LogoutAction.php +++ b/src/Action/LogoutAction.php @@ -7,26 +7,29 @@ * Time: 8:37 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Action; use Dot\Authentication\AuthenticationInterface; -use Dot\Authentication\Web\AuthenticationEventTrait; use Dot\Authentication\Web\Event\AuthenticationEvent; +use Dot\Authentication\Web\Event\AuthenticationEventListenerInterface; +use Dot\Authentication\Web\Event\AuthenticationEventListenerTrait; +use Dot\Authentication\Web\Event\DispatchAuthenticationEventTrait; use Dot\Authentication\Web\Options\WebAuthenticationOptions; use Dot\Helpers\Route\RouteOptionHelper; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Diactoros\Response\RedirectResponse; -use Zend\EventManager\EventManagerAwareTrait; /** * Class LogoutAction * @package Dot\Authentication\Web\Action */ -class LogoutAction +class LogoutAction implements AuthenticationEventListenerInterface { - use EventManagerAwareTrait; - use AuthenticationEventTrait; + use AuthenticationEventListenerTrait; + use DispatchAuthenticationEventTrait; /** @var AuthenticationInterface */ protected $authentication; @@ -59,43 +62,30 @@ public function __construct( * @param callable|null $next * @return RedirectResponse|ResponseInterface */ - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) - { + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + callable $next = null + ): ResponseInterface { if (!$this->authentication->hasIdentity()) { return new RedirectResponse($this->routeHelper->getUri($this->options->getAfterLogoutRoute())); } - - $result = $this->triggerLogoutEvent($request, $response); - if ($result instanceof ResponseInterface) { - return $result; + $event = $this->dispatchEvent(AuthenticationEvent::EVENT_BEFORE_LOGOUT, [ + 'request' => $request, + 'authenticationService' => $this->authentication + ]); + if ($event instanceof ResponseInterface) { + return $event; } - error_log( - sprintf( - 'Logout event listeners should return a ResponseInterface, "%s" returned', - is_object($result) ? get_class($result) : gettype($result) - ), - E_USER_WARNING - ); - - return $next($request, $response); - } - - public function triggerLogoutEvent(ServerRequestInterface $request, ResponseInterface $response) - { - $event = $this->createAuthenticationEvent( - $this->authentication, - AuthenticationEvent::EVENT_AUTHENTICATION_LOGOUT, - [], - $request, - $response - ); + $this->authentication->clearIdentity(); - $result = $this->getEventManager()->triggerEventUntil(function ($r) { - return ($r instanceof ResponseInterface); - }, $event); + $this->dispatchEvent(AuthenticationEvent::EVENT_AFTER_LOGOUT, [ + 'request' => $request, + 'authenticationService' => $this->authentication + ]); - $result = $result->last(); - return $result; + $uri = $this->routeHelper->getUri($this->options->getAfterLogoutRoute()); + return new RedirectResponse($uri); } } diff --git a/src/AuthenticationEventTrait.php b/src/AuthenticationEventTrait.php deleted file mode 100644 index d071605..0000000 --- a/src/AuthenticationEventTrait.php +++ /dev/null @@ -1,76 +0,0 @@ -createAuthenticationEvent($authentication, $name, $eventParams, $request, $response); - $event->setError($error); - - return $event; - } - - /** - * @param AuthenticationInterface $authentication - * @param string $name - * @param array $eventParams - * @param ServerRequestInterface|null $request - * @param ResponseInterface|null $response - * @return AuthenticationEvent - */ - protected function createAuthenticationEvent( - AuthenticationInterface $authentication, - $name = AuthenticationEvent::EVENT_AUTHENTICATION_AUTHENTICATE, - array $eventParams = [], - ServerRequestInterface $request = null, - ResponseInterface $response = null - ) { - $event = new AuthenticationEvent(); - $event->setName($name); - $event->setTarget($this); - $event->setAuthenticationService($authentication); - if ($request) { - $event->setRequest($request); - } - if ($response) { - $event->setResponse($response); - } - $event->setParams(array_merge($event->getParams(), $eventParams)); - - return $event; - } -} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index b3aae13..5aca619 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -7,42 +7,25 @@ * Time: 12:54 AM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web; use Dot\Authentication\Web\Action\LoginAction; use Dot\Authentication\Web\Action\LogoutAction; use Dot\Authentication\Web\ErrorHandler\UnauthorizedHandler; -use Dot\Authentication\Web\Factory\DefaultAuthenticationListenerFactory; -use Dot\Authentication\Web\Factory\DefaultLogoutListenerFactory; -use Dot\Authentication\Web\Factory\DefaultUnauthorizedListenerFactory; use Dot\Authentication\Web\Factory\LoginActionFactory; use Dot\Authentication\Web\Factory\LogoutActionFactory; use Dot\Authentication\Web\Factory\UnauthorizedHandlerFactory; use Dot\Authentication\Web\Factory\WebAuthenticationOptionsFactory; -use Dot\Authentication\Web\Listener\DefaultAuthenticationListener; -use Dot\Authentication\Web\Listener\DefaultLogoutListener; -use Dot\Authentication\Web\Listener\DefaultUnauthorizedListener; use Dot\Authentication\Web\Options\WebAuthenticationOptions; class ConfigProvider { - public function __invoke() + public function __invoke(): array { return [ - 'dependencies' => [ - 'factories' => [ - WebAuthenticationOptions::class => WebAuthenticationOptionsFactory::class, - - LoginAction::class => LoginActionFactory::class, - LogoutAction::class => LogoutActionFactory::class, - - UnauthorizedHandler::class => UnauthorizedHandlerFactory::class, - - DefaultAuthenticationListener::class => DefaultAuthenticationListenerFactory::class, - DefaultLogoutListener::class => DefaultLogoutListenerFactory::class, - DefaultUnauthorizedListener::class => DefaultUnauthorizedListenerFactory::class, - ] - ], + 'dependencies' => $this->getDependenciesConfig(), 'middleware_pipeline' => [ 'error' => [ @@ -72,8 +55,10 @@ public function __invoke() 'dot_authentication' => [ 'web' => [ - 'login_route' => 'login', - 'logout_route' => 'logout', + 'event_listeners' => [], + + 'login_route' => ['route_name' => 'login'], + 'logout_route' => ['route_name' => 'logout'], 'messages_options' => [ 'messages' => [], @@ -82,4 +67,16 @@ public function __invoke() ] ]; } + + public function getDependenciesConfig(): array + { + return [ + 'factories' => [ + WebAuthenticationOptions::class => WebAuthenticationOptionsFactory::class, + LoginAction::class => LoginActionFactory::class, + LogoutAction::class => LogoutActionFactory::class, + UnauthorizedHandler::class => UnauthorizedHandlerFactory::class, + ] + ]; + } } diff --git a/src/ErrorHandler/UnauthorizedHandler.php b/src/ErrorHandler/UnauthorizedHandler.php index 5f849d5..5e3f3c8 100644 --- a/src/ErrorHandler/UnauthorizedHandler.php +++ b/src/ErrorHandler/UnauthorizedHandler.php @@ -7,37 +7,73 @@ * Time: 10:45 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\ErrorHandler; use Dot\Authentication\AuthenticationInterface; -use Dot\Authentication\Web\AuthenticationEventTrait; +use Dot\Authentication\Exception\UnauthorizedException; use Dot\Authentication\Web\Event\AuthenticationEvent; +use Dot\Authentication\Web\Event\AuthenticationEventListenerInterface; +use Dot\Authentication\Web\Event\AuthenticationEventListenerTrait; +use Dot\Authentication\Web\Event\DispatchAuthenticationEventTrait; +use Dot\Authentication\Web\Exception\RuntimeException; +use Dot\Authentication\Web\Options\MessagesOptions; +use Dot\Authentication\Web\Options\WebAuthenticationOptions; +use Dot\FlashMessenger\FlashMessengerInterface; +use Dot\Helpers\Route\RouteOptionHelper; +use Dot\Helpers\Route\UriHelperTrait; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Zend\EventManager\EventManagerAwareTrait; +use Psr\Http\Message\UriInterface; +use Zend\Diactoros\Response\RedirectResponse; +use Zend\Diactoros\Uri; /** * Class UnauthorizedHandler * @package Dot\Authentication\Web\ErrorHandler */ -class UnauthorizedHandler +class UnauthorizedHandler implements AuthenticationEventListenerInterface { - use EventManagerAwareTrait; - use AuthenticationEventTrait; + use AuthenticationEventListenerTrait; + use DispatchAuthenticationEventTrait; + use UriHelperTrait; /** @var AuthenticationInterface */ protected $authenticationService; + /** @var WebAuthenticationOptions */ + protected $options; + + /** @var RouteOptionHelper */ + protected $routeHelper; + + /** @var FlashMessengerInterface */ + protected $flashMessenger; + /** @var array */ protected $statusCodes = [401, 407]; + /** @var bool */ + protected $debug = false; + /** * UnauthorizedHandler constructor. * @param AuthenticationInterface $authenticationService + * @param RouteOptionHelper $routeHelper + * @param WebAuthenticationOptions $options + * @param FlashMessengerInterface $flashMessenger */ - public function __construct(AuthenticationInterface $authenticationService) - { + public function __construct( + AuthenticationInterface $authenticationService, + RouteOptionHelper $routeHelper, + WebAuthenticationOptions $options, + FlashMessengerInterface $flashMessenger + ) { $this->authenticationService = $authenticationService; + $this->options = $options; + $this->flashMessenger = $flashMessenger; + $this->routeHelper = $routeHelper; } /** @@ -52,58 +88,116 @@ public function __invoke( ServerRequestInterface $request, ResponseInterface $response, callable $next = null - ) { + ): ResponseInterface { if ($error instanceof \Exception && in_array($error->getCode(), $this->statusCodes) || in_array($response->getStatusCode(), $this->statusCodes) ) { - $result = $this->triggerUnauthorizedEvent($request, $response, $error); - if ($result instanceof ResponseInterface) { - return $result; + $event = $this->dispatchEvent(AuthenticationEvent::EVENT_UNAUTHORIZED, [ + 'request' => $request, + 'authenticationService' => $this->authenticationService, + 'error' => $error + ]); + if ($event instanceof ResponseInterface) { + return $event; + } + + $messages = $this->getErrorMessages($error); + if (empty($messages)) { + $messages = [$this->options->getMessagesOptions()->getMessage(MessagesOptions::UNAUTHORIZED)]; + } + + //add a flash message in case the login page displays errors + if ($this->flashMessenger) { + $this->flashMessenger->addError($messages); } - //if listeners did not return a response, send to next error handlers with an explicit status code - if (!in_array($response->getStatusCode(), $this->statusCodes)) { - $response = $response->withStatus(401); + /** @var Uri $uri */ + $uri = $this->routeHelper->getUri($this->options->getLoginRoute()); + if ($this->areUriEqual($uri, $request->getUri())) { + throw new RuntimeException( + 'Default unauthorized listener has detected that the login route is not authorized either.' . + ' This can result in an endless redirect loop. ' . + 'Please edit your authorization schema to open login route to guests' + ); } + if ($this->options->isEnableWantedUrl()) { + $uri = $this->appendQueryParam( + $uri, + $this->options->getWantedUrlName(), + $request->getUri()->__toString() + ); + } + + return new RedirectResponse($uri); } return $next($request, $response, $error); } - public function triggerUnauthorizedEvent(ServerRequestInterface $request, ResponseInterface $response, $error) + /** + * @param $error + * @return array + */ + protected function getErrorMessages($error): array { - $event = $this->createAuthenticationEventWithError( - $this->authenticationService, - $error, - AuthenticationEvent::EVENT_AUTHENTICATION_UNAUTHORIZED, - [], - $request, - $response - ); - - $result = $this->getEventManager()->triggerEventUntil(function ($r) { - return ($r instanceof ResponseInterface); - }, $event); - - $result = $result->last(); - return $result; + $messages = []; + if (is_array($error) || is_string($error)) { + $error = (array)$error; + foreach ($error as $e) { + if (is_string($e)) { + $messages[] = $e; + } + } + } elseif ($error instanceof \Exception) { + if ($this->isDebug() || $error instanceof UnauthorizedException) { + $messages[] = $error->getMessage(); + } + } + return $messages; } /** * @return AuthenticationInterface */ - public function getAuthenticationService() + public function getAuthenticationService(): AuthenticationInterface { return $this->authenticationService; } /** * @param AuthenticationInterface $authenticationService - * @return UnauthorizedHandler */ - public function setAuthenticationService($authenticationService) + public function setAuthenticationService(AuthenticationInterface $authenticationService) { $this->authenticationService = $authenticationService; - return $this; + } + + /** + * @return bool + */ + public function isDebug(): bool + { + return $this->debug; + } + + /** + * @param bool $debug + */ + public function setDebug(bool $debug) + { + $this->debug = $debug; + } + + /** + * @param UriInterface $uri1 + * @param UriInterface $uri2 + * @return bool + */ + protected function areUriEqual(UriInterface $uri1, UriInterface $uri2): bool + { + return $uri1->getScheme() === $uri2->getScheme() + && $uri1->getHost() === $uri2->getHost() + && $uri1->getPath() === $uri2->getPath() + && $uri1->getPort() === $uri2->getPort(); } } diff --git a/src/Event/AbstractAuthenticationEventListener.php b/src/Event/AbstractAuthenticationEventListener.php new file mode 100644 index 0000000..f19b38a --- /dev/null +++ b/src/Event/AbstractAuthenticationEventListener.php @@ -0,0 +1,20 @@ +identity; - } - - /** - * @param IdentityInterface $identity - * @return AuthenticationEvent - */ - public function setIdentity(IdentityInterface $identity) - { - $this->identity = $identity; - return $this; - } - - /** - * @return AuthenticationResult - */ - public function getAuthenticationResult() - { - return $this->result; - } - - /** - * @param AuthenticationResult $result - * @return AuthenticationEvent - */ - public function setAuthenticationResult(AuthenticationResult $result) - { - $this->result = $result; - return $this; - } - - /** - * @return AuthenticationInterface - */ - public function getAuthenticationService() - { - return $this->authentication; - } - - /** - * @param AuthenticationInterface $authentication - * @return AuthenticationEvent - */ - public function setAuthenticationService(AuthenticationInterface $authentication) - { - $this->authentication = $authentication; - return $this; - } + const EVENT_BEFORE_AUTHENTICATION = 'event.beforeAuthentication'; + const EVENT_AFTER_AUTHENTICATION = 'event.afterAuthentication'; + const EVENT_AUTHENTICATION_SUCCESS = 'event.authenticationSuccess'; + const EVENT_AUTHENTICATION_ERROR = 'event.authenticationError'; + const EVENT_AUTHENTICATION_BEFORE_RENDER = 'event.authenticationBeforeRender'; - /** - * @return mixed - */ - public function getError() - { - return $this->error; - } + const EVENT_BEFORE_LOGOUT = 'event.beforeLogout'; + const EVENT_AFTER_LOGOUT = 'event.afterLogout'; - /** - * @param mixed $error - * @return AuthenticationEvent - */ - public function setError($error) - { - $this->error = $error; - return $this; - } + const EVENT_UNAUTHORIZED = 'event.unauthorized'; } diff --git a/src/Event/AuthenticationEventListenerInterface.php b/src/Event/AuthenticationEventListenerInterface.php new file mode 100644 index 0000000..a60fc4d --- /dev/null +++ b/src/Event/AuthenticationEventListenerInterface.php @@ -0,0 +1,37 @@ +listeners[] = $events->attach( + AuthenticationEvent::EVENT_AUTHENTICATION_BEFORE_RENDER, + [$this, 'onAuthenticationBeforeRender'], + $priority + ); + $this->listeners[] = $events->attach( + AuthenticationEvent::EVENT_BEFORE_AUTHENTICATION, + [$this, 'onBeforeAuthentication'], + $priority + ); + $this->listeners[] = $events->attach( + AuthenticationEvent::EVENT_AFTER_AUTHENTICATION, + [$this, 'onAfterAuthentication'], + $priority + ); + $this->listeners[] = $events->attach( + AuthenticationEvent::EVENT_AUTHENTICATION_SUCCESS, + [$this, 'onAuthenticationSuccess'], + $priority + ); + $this->listeners[] = $events->attach( + AuthenticationEvent::EVENT_AUTHENTICATION_ERROR, + [$this, 'onAuthenticationError'], + $priority + ); + $this->listeners[] = $events->attach( + AuthenticationEvent::EVENT_BEFORE_LOGOUT, + [$this, 'onBeforeLogout'], + $priority + ); + $this->listeners[] = $events->attach( + AuthenticationEvent::EVENT_AFTER_LOGOUT, + [$this, 'onAfterLogout'], + $priority + ); + $this->listeners[] = $events->attach( + AuthenticationEvent::EVENT_UNAUTHORIZED, + [$this, 'onUnauthorized'], + $priority + ); + } + + public function onBeforeAuthentication(AuthenticationEvent $e) + { + // no-op + } + + public function onAfterAuthentication(AuthenticationEvent $e) + { + // no-op + } + + public function onAuthenticationSuccess(AuthenticationEvent $e) + { + // no-op + } + + public function onAuthenticationError(AuthenticationEvent $e) + { + //no-op + } + + public function onAuthenticationBeforeRender(AuthenticationEvent $e) + { + //no-op + } + + public function onBeforeLogout(AuthenticationEvent $e) + { + // no-op + } + + public function onAfterLogout(AuthenticationEvent $e) + { + // no-op + } + + public function onUnauthorized(AuthenticationEvent $e) + { + //no-op + } +} diff --git a/src/Event/DispatchAuthenticationEventTrait.php b/src/Event/DispatchAuthenticationEventTrait.php new file mode 100644 index 0000000..069f9fc --- /dev/null +++ b/src/Event/DispatchAuthenticationEventTrait.php @@ -0,0 +1,48 @@ +getEventManager()->triggerEventUntil(function ($r) { + return ($r instanceof ResponseInterface); + }, $event); + + if ($r->stopped()) { + return $r->last(); + } + + return $event; + } +} diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index 125e999..588370e 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -7,6 +7,8 @@ * Time: 3:00 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Exception; /** diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 4b9a175..a80bf8c 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -1,11 +1,14 @@ get(WebAuthenticationOptions::class); + + if (!empty($options->getEventListeners()) + && is_array($options->getEventListeners()) + ) { + $listeners = $options->getEventListeners(); + foreach ($listeners as $listener) { + if (is_string($listener)) { + $l = $this->getListenerObject($container, $listener); + $p = 1; + $l->attach($eventManager, $p); + } elseif (is_array($listener)) { + $l = $listener['type'] ?? ''; + $p = $listener['priority'] ?? 1; + + $l = $this->getListenerObject($container, $l); + $l->attach($eventManager, $p); + } + } + } + } + + /** + * @param ContainerInterface $container + * @param string $listener + * @return AuthenticationEventListenerInterface + */ + protected function getListenerObject( + ContainerInterface $container, + string $listener + ): AuthenticationEventListenerInterface { + if ($container->has($listener)) { + $listener = $container->get($listener); + } + + if (is_string($listener) && class_exists($listener)) { + $listener = new $listener(); + } + + if (!$listener instanceof AuthenticationEventListenerInterface) { + throw new RuntimeException('Authentication event listener is not an instance of ' + . AuthenticationEventListenerInterface::class); + } + + return $listener; + } +} diff --git a/src/Factory/DefaultAuthenticationListenerFactory.php b/src/Factory/DefaultAuthenticationListenerFactory.php deleted file mode 100644 index 91bc459..0000000 --- a/src/Factory/DefaultAuthenticationListenerFactory.php +++ /dev/null @@ -1,48 +0,0 @@ -get(WebAuthenticationOptions::class); - $authentication = $container->get(AuthenticationInterface::class); - $template = $container->get(TemplateRendererInterface::class); - $routeHelper = $container->get(RouteOptionHelper::class); - $flashMessenger = $container->get(FlashMessengerInterface::class); - - $listener = new DefaultAuthenticationListener( - $authentication, - $template, - $routeHelper, - $flashMessenger, - $options - ); - - return $listener; - } -} diff --git a/src/Factory/DefaultLogoutListenerFactory.php b/src/Factory/DefaultLogoutListenerFactory.php deleted file mode 100644 index 1fda000..0000000 --- a/src/Factory/DefaultLogoutListenerFactory.php +++ /dev/null @@ -1,36 +0,0 @@ -get(WebAuthenticationOptions::class); - $authentication = $container->get(AuthenticationInterface::class); - $routeHelper = $container->get(RouteOptionHelper::class); - - return new DefaultLogoutListener($authentication, $routeHelper, $options); - } -} diff --git a/src/Factory/DefaultUnauthorizedListenerFactory.php b/src/Factory/DefaultUnauthorizedListenerFactory.php deleted file mode 100644 index 7f53b14..0000000 --- a/src/Factory/DefaultUnauthorizedListenerFactory.php +++ /dev/null @@ -1,42 +0,0 @@ -get('config'); - $debug = isset($config['debug']) ? $config['debug'] : false; - - $routeHelper = $container->get(RouteOptionHelper::class); - $flashMessenger = $container->get(FlashMessengerInterface::class); - $options = $container->get(WebAuthenticationOptions::class); - - $listener = new DefaultUnauthorizedListener($routeHelper, $flashMessenger, $options); - $listener->setDebug($debug); - - return $listener; - } -} diff --git a/src/Factory/LoginActionFactory.php b/src/Factory/LoginActionFactory.php index 4319b04..c5807e4 100644 --- a/src/Factory/LoginActionFactory.php +++ b/src/Factory/LoginActionFactory.php @@ -7,46 +7,47 @@ * Time: 8:40 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Factory; use Dot\Authentication\AuthenticationInterface; use Dot\Authentication\Web\Action\LoginAction; -use Dot\Authentication\Web\Listener\DefaultAuthenticationListener; use Dot\Authentication\Web\Options\WebAuthenticationOptions; +use Dot\FlashMessenger\FlashMessengerInterface; use Dot\Helpers\Route\RouteOptionHelper; use Interop\Container\ContainerInterface; -use Zend\EventManager\EventManager; -use Zend\EventManager\EventManagerInterface; +use Zend\Expressive\Template\TemplateRendererInterface; /** * Class LoginActionFactory * @package Dot\Authentication\Web\Factory */ -class LoginActionFactory +class LoginActionFactory extends BaseActionFactory { /** * @param ContainerInterface $container + * @param $requestedName * @return LoginAction */ - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container, string $requestedName): LoginAction { - $eventManager = $container->has(EventManagerInterface::class) - ? $container->get(EventManagerInterface::class) - : new EventManager(); - - /** @var DefaultAuthenticationListener $defaultListeners */ - $defaultListeners = $container->get(DefaultAuthenticationListener::class); - $defaultListeners->attach($eventManager); + $config = $container->get('config'); + $debug = $config['debug'] ?? false; - $authentication = $container->get(AuthenticationInterface::class); - - $action = new LoginAction( - $authentication, + /** @var LoginAction $action */ + $action = new $requestedName( + $container->get(AuthenticationInterface::class), + $container->get(TemplateRendererInterface::class), $container->get(RouteOptionHelper::class), - $container->get(WebAuthenticationOptions::class) + $container->get(WebAuthenticationOptions::class), + $container->get(FlashMessengerInterface::class) ); - $action->setEventManager($eventManager); + $action->setDebug($debug); + + $this->attachListeners($container, $action->getEventManager()); + $action->attach($action->getEventManager(), 1000); return $action; } diff --git a/src/Factory/LogoutActionFactory.php b/src/Factory/LogoutActionFactory.php index 664715a..ada8adf 100644 --- a/src/Factory/LogoutActionFactory.php +++ b/src/Factory/LogoutActionFactory.php @@ -7,43 +7,38 @@ * Time: 8:40 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Factory; use Dot\Authentication\AuthenticationInterface; use Dot\Authentication\Web\Action\LogoutAction; -use Dot\Authentication\Web\Listener\DefaultLogoutListener; use Dot\Authentication\Web\Options\WebAuthenticationOptions; use Dot\Helpers\Route\RouteOptionHelper; use Interop\Container\ContainerInterface; -use Zend\EventManager\EventManager; -use Zend\EventManager\EventManagerInterface; /** * Class LogoutActionFactory * @package Dot\Authentication\Web\Factory */ -class LogoutActionFactory +class LogoutActionFactory extends BaseActionFactory { /** * @param ContainerInterface $container + * @param $requestedName * @return LogoutAction */ - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container, string $requestedName): LogoutAction { - $eventManager = $container->has(EventManagerInterface::class) - ? $container->get(EventManagerInterface::class) - : new EventManager(); - - $defaultListeners = $container->get(DefaultLogoutListener::class); - $defaultListeners->attach($eventManager); - - $action = new LogoutAction( + /** @var LogoutAction $action */ + $action = new $requestedName( $container->get(AuthenticationInterface::class), $container->get(RouteOptionHelper::class), $container->get(WebAuthenticationOptions::class) ); - $action->setEventManager($eventManager); + $this->attachListeners($container, $action->getEventManager()); + $action->attach($action->getEventManager(), 1000); return $action; } diff --git a/src/Factory/UnauthorizedHandlerFactory.php b/src/Factory/UnauthorizedHandlerFactory.php index 3b1df4c..5954120 100644 --- a/src/Factory/UnauthorizedHandlerFactory.php +++ b/src/Factory/UnauthorizedHandlerFactory.php @@ -7,37 +7,40 @@ * Time: 3:15 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Factory; use Dot\Authentication\AuthenticationInterface; use Dot\Authentication\Web\ErrorHandler\UnauthorizedHandler; -use Dot\Authentication\Web\Event\AuthenticationEvent; -use Dot\Authentication\Web\Listener\DefaultUnauthorizedListener; +use Dot\Authentication\Web\Options\WebAuthenticationOptions; +use Dot\FlashMessenger\FlashMessengerInterface; +use Dot\Helpers\Route\RouteOptionHelper; use Interop\Container\ContainerInterface; -use Zend\EventManager\EventManager; -use Zend\EventManager\EventManagerInterface; /** * Class UnauthorizedHandlerFactory * @package Dot\Authentication\Web\Factory */ -class UnauthorizedHandlerFactory +class UnauthorizedHandlerFactory extends BaseActionFactory { /** * @param ContainerInterface $container + * @param $requestedName * @return UnauthorizedHandler */ - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container, string $requestedName): UnauthorizedHandler { - $eventManager = $container->has(EventManagerInterface::class) - ? $container->get(EventManagerInterface::class) - : new EventManager(); - - $defaultListener = $container->get(DefaultUnauthorizedListener::class); - $eventManager->attach(AuthenticationEvent::EVENT_AUTHENTICATION_UNAUTHORIZED, $defaultListener, 1); + /** @var UnauthorizedHandler $handler */ + $handler = new $requestedName( + $container->get(AuthenticationInterface::class), + $container->get(RouteOptionHelper::class), + $container->get(WebAuthenticationOptions::class), + $container->get(FlashMessengerInterface::class) + ); - $handler = new UnauthorizedHandler($container->get(AuthenticationInterface::class)); - $handler->setEventManager($eventManager); + $this->attachListeners($container, $handler->getEventManager()); + $handler->attach($handler->getEventManager(), 1000); return $handler; } diff --git a/src/Factory/WebAuthenticationOptionsFactory.php b/src/Factory/WebAuthenticationOptionsFactory.php index ec04c5c..247d350 100644 --- a/src/Factory/WebAuthenticationOptionsFactory.php +++ b/src/Factory/WebAuthenticationOptionsFactory.php @@ -7,6 +7,8 @@ * Time: 2:50 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Factory; use Dot\Authentication\Web\Options\WebAuthenticationOptions; @@ -20,11 +22,12 @@ class WebAuthenticationOptionsFactory { /** * @param ContainerInterface $container + * @param $requestedName * @return WebAuthenticationOptions */ - public function __invoke(ContainerInterface $container) + public function __invoke(ContainerInterface $container, string $requestedName) { $config = $container->get('config')['dot_authentication']['web']; - return new WebAuthenticationOptions($config); + return new $requestedName($config); } } diff --git a/src/Listener/DefaultAuthenticationListener.php b/src/Listener/DefaultAuthenticationListener.php deleted file mode 100644 index 6db0e00..0000000 --- a/src/Listener/DefaultAuthenticationListener.php +++ /dev/null @@ -1,201 +0,0 @@ -authentication = $authentication; - $this->routeHelper = $routeHelper; - $this->template = $template; - $this->options = $options; - $this->flashMessenger = $flashMessenger; - } - - /** - * @param EventManagerInterface $events - * @param int $priority - */ - public function attach(EventManagerInterface $events, $priority = 1) - { - $this->listeners[] = $events->attach( - AuthenticationEvent::EVENT_AUTHENTICATION_AUTHENTICATE, - [$this, 'prepare'], - 1000 - ); - - $this->listeners[] = $events->attach( - AuthenticationEvent::EVENT_AUTHENTICATION_AUTHENTICATE, - [$this, 'authenticate'], - 1 - ); - - $this->listeners[] = $events->attach( - AuthenticationEvent::EVENT_AUTHENTICATION_AUTHENTICATE, - [$this, 'authenticationPost'], - -1000 - ); - } - - /** - * @param AuthenticationEvent $e - */ - public function prepare(AuthenticationEvent $e) - { - //nothing to prepare for now, let it to implementors - } - - /** - * @param AuthenticationEvent $e - */ - public function authenticate(AuthenticationEvent $e) - { - $request = $e->getRequest(); - $response = $e->getResponse(); - $error = $e->getError(); - if ($request->getMethod() === 'POST' && empty($error)) { - $result = $this->authentication->authenticate($request, $response); - //we get this in case authentication skipped(due to missing credentials in request) - //but for web application, we want to force implemetors to prepare their auth adapter first - //so we throw an exception to be clear developers have missed something - if ($result === false) { - throw new RuntimeException('Authentication service could not authenticate request. ' . - 'Have you forgot to prepare the request first according to authentication adapter needs?'); - } - - if ($result instanceof AuthenticationResult) { - $e->setAuthenticationResult($result); - - if ($result->isValid()) { - $e->setIdentity($result->getIdentity()); - } else { - $e->setError($result->getMessage()); - } - - //set the possibly modified PSR7 messages to the event - if ($result->getRequest()) { - $e->setRequest($result->getRequest()); - } - - if ($result->getResponse()) { - $e->setResponse($result->getResponse()); - } - } - } - } - - /** - * @param AuthenticationEvent $e - * @return HtmlResponse|RedirectResponse - * @throws \Exception - */ - public function authenticationPost(AuthenticationEvent $e) - { - $request = $e->getRequest(); - if ($request->getMethod() === 'POST') { - $error = $e->getError(); - if (!empty($error)) { - return $this->prgRedirect($e); - } - - $result = $e->getAuthenticationResult(); - if ($result && $result->isValid()) { - $uri = $this->routeHelper->getUri($this->options->getAfterLoginRoute()); - - if ($this->options->isAllowRedirectParam()) { - $params = $e->getRequest()->getQueryParams(); - $redirectParam = $this->options->getRedirectParamName(); - - if (isset($params[$redirectParam]) && !empty($params[$redirectParam])) { - $uri = new Uri(urldecode($params[$redirectParam])); - } - } - - return new RedirectResponse($uri); - } - } - - return $this->renderTemplate($e); - } - - protected function prgRedirect(AuthenticationEvent $e) - { - $request = $e->getRequest(); - $error = $e->getError(); - if (is_array($error) || is_string($error)) { - $this->flashMessenger->addError($error); - } elseif ($error instanceof \Exception) { - $this->flashMessenger->addError($error->getMessage()); - } else { - $this->flashMessenger->addError( - $this->options->getMessagesOptions()->getMessage(MessagesOptions::AUTHENTICATION_FAIL_MESSAGE) - ); - } - - return new RedirectResponse($request->getUri(), 303); - } - - /** - * @param AuthenticationEvent $e - * @return HtmlResponse - */ - protected function renderTemplate(AuthenticationEvent $e) - { - return new HtmlResponse($this->template->render($this->options->getLoginTemplate(), $e->getParams())); - } -} diff --git a/src/Listener/DefaultLogoutListener.php b/src/Listener/DefaultLogoutListener.php deleted file mode 100644 index 9ccc599..0000000 --- a/src/Listener/DefaultLogoutListener.php +++ /dev/null @@ -1,88 +0,0 @@ -authentication = $authentication; - $this->options = $options; - $this->routeHelper = $routeHelper; - } - - /** - * @param EventManagerInterface $events - * @param int $priority - */ - public function attach(EventManagerInterface $events, $priority = 1) - { - $this->listeners[] = $events->attach( - AuthenticationEvent::EVENT_AUTHENTICATION_LOGOUT, - [$this, 'logout'], - 1 - ); - - $this->listeners[] = $events->attach( - AuthenticationEvent::EVENT_AUTHENTICATION_LOGOUT, - [$this, 'logoutPost'], - -1000 - ); - } - - /** - * @param AuthenticationEvent $e - */ - public function logout(AuthenticationEvent $e) - { - $this->authentication->clearIdentity(); - } - - /** - * @param AuthenticationEvent $e - * @return RedirectResponse - * @throws \Exception - */ - public function logoutPost(AuthenticationEvent $e) - { - $uri = $this->routeHelper->getUri($this->options->getAfterLogoutRoute()); - return new RedirectResponse($uri); - } -} diff --git a/src/Listener/DefaultUnauthorizedListener.php b/src/Listener/DefaultUnauthorizedListener.php deleted file mode 100644 index 4fd6239..0000000 --- a/src/Listener/DefaultUnauthorizedListener.php +++ /dev/null @@ -1,137 +0,0 @@ -routeHelper = $routeHelper; - $this->options = $options; - $this->flashMessenger = $flashMessenger; - } - - /** - * @param AuthenticationEvent $e - * @return RedirectResponse - * @throws \Exception - */ - public function __invoke(AuthenticationEvent $e) - { - $request = $e->getRequest(); - - $messages = []; - $error = $e->getError(); - if (is_array($error)) { - foreach ($error as $e) { - if (is_string($e)) { - $messages[] = $e; - } - } - } elseif (is_string($error)) { - $messages[] = $error; - } elseif ($error instanceof \Exception) { - if ($this->isDebug() || $error instanceof UnauthorizedException) { - $messages[] = $error->getMessage(); - } - } - - if (empty($messages)) { - $messages = [$this->options->getMessagesOptions()->getMessage(MessagesOptions::UNAUTHORIZED_MESSAGE)]; - } - - //add a flash message in case the login page displays errors - if ($this->flashMessenger) { - foreach ($messages as $message) { - $this->flashMessenger->addError($message); - } - } - - /** @var Uri $uri */ - $uri = $this->routeHelper->getUri($this->options->getLoginRoute()); - if ($this->areUriEqual($uri, $request->getUri())) { - throw new RuntimeException( - 'Default unauthorized listener has detected that the login route is not authorized either.' . - ' This can result in an endless redirect loop. ' . - 'Please edit your authorization schema to open login route to guests' - ); - } - if ($this->options->isAllowRedirectParam()) { - $uri = $this->appendQueryParam($uri, $request->getUri(), $this->options->getRedirectParamName()); - } - - return new RedirectResponse($uri); - } - - /** - * @return boolean - */ - public function isDebug() - { - return $this->debug; - } - - /** - * @param boolean $debug - * @return DefaultUnauthorizedListener - */ - public function setDebug($debug) - { - $this->debug = $debug; - return $this; - } - - protected function areUriEqual(UriInterface $uri1, UriInterface $uri2) - { - return $uri1->getScheme() === $uri2->getScheme() - && $uri1->getHost() === $uri2->getHost() - && $uri1->getPath() === $uri2->getPath() - && $uri1->getPort() === $uri2->getPort(); - } -} diff --git a/src/Options/MessagesOptions.php b/src/Options/MessagesOptions.php index fb75ee5..6c8d8d1 100644 --- a/src/Options/MessagesOptions.php +++ b/src/Options/MessagesOptions.php @@ -7,6 +7,8 @@ * Time: 8:37 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Options; use Zend\Stdlib\AbstractOptions; @@ -18,43 +20,77 @@ */ class MessagesOptions extends AbstractOptions { - const AUTHENTICATION_FAIL_MESSAGE = 0; - const UNAUTHORIZED_MESSAGE = 1; + const AUTHENTICATION_FAILURE = 0; + const AUTHENTICATION_INVALID_CREDENTIALS = 1; + const AUTHENTICATION_IDENTITY_AMBIGUOUS = 2; + const AUTHENTICATION_IDENTITY_NOT_FOUND = 3; + const AUTHENTICATION_UNCATEGORIZED = 4; + const AUTHENTICATION_MISSING_CREDENTIALS = 5; + const AUTHENTICATION_SUCCESS = 6; + const AUTHENTICATION_FAIL_UNKNOWN = 7; + + const UNAUTHORIZED = 8; protected $messages = [ - MessagesOptions::AUTHENTICATION_FAIL_MESSAGE => + MessagesOptions::AUTHENTICATION_FAILURE => + 'Authentication failed. Check your credentials and try again', + + MessagesOptions::AUTHENTICATION_INVALID_CREDENTIALS => + 'Authentication failed. Check your credentials and try again', + + MessagesOptions::AUTHENTICATION_IDENTITY_AMBIGUOUS => + 'Authentication failed. Check your credentials and try again', + + MessagesOptions::AUTHENTICATION_IDENTITY_NOT_FOUND => + 'Authentication failed. Check your credentials and try again', + + MessagesOptions::AUTHENTICATION_UNCATEGORIZED => + 'Authentication failed. Check your credentials and try again', + + MessagesOptions::AUTHENTICATION_MISSING_CREDENTIALS => + 'Authentication failed. Missing or invalid credentials', + + MessagesOptions::AUTHENTICATION_SUCCESS => + 'Welcome! You have successfully signed in', + + MessagesOptions::AUTHENTICATION_FAIL_UNKNOWN => 'Authentication failed. Check your credentials and try again', - MessagesOptions::UNAUTHORIZED_MESSAGE => - 'You must be authenticated to access the requested content', + MessagesOptions::UNAUTHORIZED => 'You must sign in first to access the requested content', ]; - protected $__strictMode__ = false; + /** + * MessagesOptions constructor. + * @param null $options + */ + public function __construct($options = null) + { + $this->__strictMode__ = false; + parent::__construct($options); + } /** * @return array */ - public function getMessages() + public function getMessages(): array { return $this->messages; } /** * @param $messages - * @return $this */ - public function setMessages($messages) + public function setMessages(array $messages) { $this->messages = ArrayUtils::merge($this->messages, $messages, true); - return $this; } /** * @param $key * @return mixed|string */ - public function getMessage($key) + public function getMessage(int $key): string { - return isset($this->messages[$key]) ? $this->messages[$key] : null; + return $this->messages[$key] ?? ''; } } diff --git a/src/Options/WebAuthenticationOptions.php b/src/Options/WebAuthenticationOptions.php index 68aecca..4cff239 100644 --- a/src/Options/WebAuthenticationOptions.php +++ b/src/Options/WebAuthenticationOptions.php @@ -7,9 +7,10 @@ * Time: 9:36 PM */ +declare(strict_types = 1); + namespace Dot\Authentication\Web\Options; -use Dot\Authentication\Web\Exception\InvalidArgumentException; use Zend\Stdlib\AbstractOptions; /** @@ -19,185 +20,186 @@ class WebAuthenticationOptions extends AbstractOptions { /** @var string|array */ - protected $loginRoute = 'login'; + protected $loginRoute = ['route_name' => 'login']; /** @var string|array */ - protected $logoutRoute = 'logout'; + protected $logoutRoute = ['route_name' => 'logout']; /** @var string|array */ - protected $afterLoginRoute = 'home'; + protected $afterLoginRoute = ['route_name' => 'home']; /** @var string|array */ - protected $afterLogoutRoute = 'login'; + protected $afterLogoutRoute = ['route_name' => 'login']; /** @var string */ - protected $loginTemplate; + protected $loginTemplate = ''; /** @var bool */ - protected $allowRedirectParam = true; + protected $enableWantedUrl = true; /** @var string */ - protected $redirectParamName = 'redirect'; + protected $wantedUrlName = 'redirect'; + + /** @var array */ + protected $eventListeners = []; /** @var MessagesOptions */ protected $messagesOptions; - protected $__strictMode__ = false; + /** + * WebAuthenticationOptions constructor. + * @param null $options + */ + public function __construct($options = null) + { + $this->__strictMode__ = false; + parent::__construct($options); + } /** - * @return array|string + * @return array */ - public function getLoginRoute() + public function getLoginRoute(): array { return $this->loginRoute; } /** - * @param array|string $loginRoute - * @return WebAuthenticationOptions + * @param array $loginRoute */ - public function setLoginRoute($loginRoute) + public function setLoginRoute(array $loginRoute) { $this->loginRoute = $loginRoute; - return $this; } /** - * @return array|string + * @return array */ - public function getLogoutRoute() + public function getLogoutRoute(): array { return $this->logoutRoute; } /** - * @param array|string $logoutRoute - * @return WebAuthenticationOptions + * @param array $logoutRoute */ - public function setLogoutRoute($logoutRoute) + public function setLogoutRoute(array $logoutRoute) { $this->logoutRoute = $logoutRoute; - return $this; } /** - * @return array|string + * @return array */ - public function getAfterLoginRoute() + public function getAfterLoginRoute(): array { return $this->afterLoginRoute; } /** - * @param array|string $afterLoginRoute - * @return WebAuthenticationOptions + * @param array $afterLoginRoute */ - public function setAfterLoginRoute($afterLoginRoute) + public function setAfterLoginRoute(array $afterLoginRoute) { $this->afterLoginRoute = $afterLoginRoute; - return $this; } /** - * @return array|string + * @return array */ - public function getAfterLogoutRoute() + public function getAfterLogoutRoute(): array { return $this->afterLogoutRoute; } /** * @param array|string $afterLogoutRoute - * @return WebAuthenticationOptions */ - public function setAfterLogoutRoute($afterLogoutRoute) + public function setAfterLogoutRoute(array $afterLogoutRoute) { $this->afterLogoutRoute = $afterLogoutRoute; - return $this; } /** * @return string */ - public function getLoginTemplate() + public function getLoginTemplate(): string { - return $this->loginTemplate; + return $this->loginTemplate ?? ''; } /** * @param string $loginTemplate - * @return WebAuthenticationOptions */ - public function setLoginTemplate($loginTemplate) + public function setLoginTemplate(string $loginTemplate) { $this->loginTemplate = $loginTemplate; - return $this; } /** - * @return boolean + * @return MessagesOptions */ - public function isAllowRedirectParam() + public function getMessagesOptions(): MessagesOptions { - return $this->allowRedirectParam; + if (!$this->messagesOptions) { + $this->setMessagesOptions([]); + } + return $this->messagesOptions; } /** - * @param boolean $allowRedirectParam - * @return WebAuthenticationOptions + * @param MessagesOptions|array $messagesOptions */ - public function setAllowRedirectParam($allowRedirectParam) + public function setMessagesOptions(array $messagesOptions) { - $this->allowRedirectParam = $allowRedirectParam; - return $this; + $this->messagesOptions = new MessagesOptions($messagesOptions); + } + + /** + * @return bool + */ + public function isEnableWantedUrl(): bool + { + return $this->enableWantedUrl; + } + + /** + * @param bool $enableWantedUrl + */ + public function setEnableWantedUrl(bool $enableWantedUrl) + { + $this->enableWantedUrl = $enableWantedUrl; } /** * @return string */ - public function getRedirectParamName() + public function getWantedUrlName(): string { - return $this->redirectParamName; + return $this->wantedUrlName; } /** - * @param string $redirectParamName - * @return WebAuthenticationOptions + * @param string $wantedUrlName */ - public function setRedirectParamName($redirectParamName) + public function setWantedUrlName(string $wantedUrlName) { - $this->redirectParamName = $redirectParamName; - return $this; + $this->wantedUrlName = $wantedUrlName; } /** - * @return MessagesOptions + * @return array */ - public function getMessagesOptions() + public function getEventListeners(): array { - if (!$this->messagesOptions) { - $this->setMessagesOptions([]); - } - return $this->messagesOptions; + return $this->eventListeners; } /** - * @param MessagesOptions|array $messagesOptions - * @return WebAuthenticationOptions - */ - public function setMessagesOptions($messagesOptions) - { - if (is_array($messagesOptions)) { - $this->messagesOptions = new MessagesOptions($messagesOptions); - } elseif ($messagesOptions instanceof MessagesOptions) { - $this->messagesOptions = $messagesOptions; - } else { - throw new InvalidArgumentException(sprintf( - 'MessageOptions should be an array or an %s object. %s provided.', - MessagesOptions::class, - is_object($messagesOptions) ? get_class($messagesOptions) : gettype($messagesOptions) - )); - } - return $this; + * @param array $eventListeners + */ + public function setEventListeners(array $eventListeners) + { + $this->eventListeners = $eventListeners; } } diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..a10db01 --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,33 @@ + MessagesOptions::AUTHENTICATION_FAILURE, + AuthenticationResult::FAILURE_INVALID_CREDENTIALS => MessagesOptions::AUTHENTICATION_INVALID_CREDENTIALS, + AuthenticationResult::FAILURE_IDENTITY_AMBIGUOUS => MessagesOptions::AUTHENTICATION_IDENTITY_AMBIGUOUS, + AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND => MessagesOptions::AUTHENTICATION_IDENTITY_NOT_FOUND, + AuthenticationResult::FAILURE_UNCATEGORIZED => MessagesOptions::AUTHENTICATION_UNCATEGORIZED, + AuthenticationResult::FAILURE_MISSING_CREDENTIALS => MessagesOptions::AUTHENTICATION_MISSING_CREDENTIALS, + AuthenticationResult::SUCCESS => MessagesOptions::AUTHENTICATION_SUCCESS + ]; +}