├── TODO.md ├── src ├── Exception │ ├── ExceptionInterface.php │ ├── OAuth2ProviderException.php │ └── MissingProviderConfigException.php ├── OAuth2ProviderFactoryFactory.php ├── OAuth2AdapterFactory.php ├── Debug │ ├── DebugResourceOwner.php │ ├── DebugProviderMiddleware.php │ ├── DebugProviderMiddlewareFactory.php │ └── DebugProvider.php ├── RedirectResponseFactoryFactory.php ├── OAuth2User.php ├── OAuth2ProviderFactory.php ├── ConfigProvider.php ├── UnauthorizedResponseFactoryFactory.php ├── OAuth2CallbackMiddlewareFactory.php └── OAuth2Adapter.php ├── templates ├── 401.mustache ├── 401.phtml ├── 401.php └── 401.html.twig ├── config ├── oauth2clientauthentication.global.php └── oauth2clientauthentication.local.php ├── LICENSE.md ├── README.md ├── composer.json └── CHANGELOG.md /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - Add user repository integration. 4 | This needs to be done to allow fetching stored details from the local 5 | application and/or injecting details from a new login. Those details might 6 | include things such as roles. 7 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | get(OAuth2ProviderFactory::class), 18 | $container->get(UnauthorizedResponseFactory::class), 19 | $container->get(RedirectResponseFactory::class) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Debug/DebugResourceOwner.php: -------------------------------------------------------------------------------- 1 | self::USER_ID, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/401.mustache: -------------------------------------------------------------------------------- 1 | {{ 5 |

Unauthorized

6 | 7 |

8 | You are not logged in, and therefore cannot perform this action. 9 |

10 | 11 |

12 | Login to continue: 13 |

14 | 15 |

16 | GitHub 17 | Google 18 | {{#debug}} 19 | Debug 20 | {{/debug}} 21 |
22 | 23 | {{/content}} 24 | {{/layout::layout}} 25 | -------------------------------------------------------------------------------- /templates/401.phtml: -------------------------------------------------------------------------------- 1 | headTitle('Unauthorized') ?> 2 | 3 |
4 |

Unauthorized

5 | 6 |

7 | You are not logged in, and therefore cannot perform this action. 8 |

9 | 10 |

11 | Login to continue: 12 |

13 | 14 |

15 | GitHub 16 | Google 17 | 18 | Debug 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /templates/401.php: -------------------------------------------------------------------------------- 1 | layout('layout::layout', [ 2 | 'title' => 'Unauthorized', 3 | ]); ?> 4 | 5 |
6 |

Unauthorized

7 | 8 |

9 | You are not logged in, and therefore cannot perform this action. 10 |

11 | 12 |

13 | Login to continue: 14 |

15 | 16 |

17 | GitHub 18 | Google 19 | 20 | Debug 21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /templates/401.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@layout/default.html.twig' %} 2 | 3 | {% block title %}404 Not Found{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

Unauthorized

8 | 9 |

10 | You are not logged in, and therefore cannot perform this action. 11 |

12 | 13 |

14 | Login to continue: 15 |

16 | 17 |

18 | GitHub 19 | Google 20 | {% if debug is defined %} 21 | Debug 22 | {% endif %} 23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /src/RedirectResponseFactoryFactory.php: -------------------------------------------------------------------------------- 1 | get(ResponseInterface::class)(); 21 | 22 | return $response 23 | ->withHeader('Location', $url) 24 | ->withStatus(302); 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/OAuth2ProviderException.php: -------------------------------------------------------------------------------- 1 | getMessage() 28 | ), 401, $throwable); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/oauth2clientauthentication.global.php: -------------------------------------------------------------------------------- 1 | [ 12 | // Configure the base path for all OAuth2 client callbacks. By default, 13 | // this is "/auth". 14 | // 'auth_path' => '/auth', 15 | 16 | // Configure the production and debug routes for OAuth2 client callbacks 17 | // if desired. These strings will be relative to the 'auth_path' config 18 | // as specified above. 19 | 'routes' => [ 20 | // Production path. 21 | // 'production' => '/{provider:facebook|github|google|instagram}|linkedin[/oauth2callback]', 22 | 23 | // Debug path. 24 | // 'debug' => '/{provider:debug|facebook|github|google|instagram|linkedin}[/oauth2callback]', 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /src/Exception/MissingProviderConfigException.php: -------------------------------------------------------------------------------- 1 | identity = $identity; 23 | $this->userData = $userData; 24 | } 25 | 26 | public function getIdentity() : string 27 | { 28 | return $this->identity; 29 | } 30 | 31 | public function getRoles() : iterable 32 | { 33 | return $this->userData['roles'] ?? []; 34 | } 35 | 36 | public function getDetail(string $name, $default = null) 37 | { 38 | return $this->userData[$name] ?? $default; 39 | } 40 | 41 | public function getDetails() : array 42 | { 43 | return $this->userData; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Debug/DebugProviderMiddleware.php: -------------------------------------------------------------------------------- 1 | redirectResponseFactory = $redirectResponseFactory; 32 | $this->pathTemplate = $pathTemplate; 33 | } 34 | 35 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 36 | { 37 | $uri = sprintf($this->pathTemplate, DebugProvider::CODE, DebugProvider::STATE); 38 | return ($this->redirectResponseFactory)($uri); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/OAuth2ProviderFactory.php: -------------------------------------------------------------------------------- 1 | container = $container; 23 | } 24 | 25 | /** 26 | * @throws Exception\MissingProviderConfigException 27 | * @param string $name 28 | * @return Provider\AbstractProvider 29 | */ 30 | public function createProvider(string $name) : Provider\AbstractProvider 31 | { 32 | $config = $this->container->get('config')['oauth2clientauthentication'] ?? []; 33 | 34 | if (! isset($config[$name])) { 35 | throw Exception\MissingProviderConfigException::forProvider($name); 36 | } 37 | 38 | if (! isset($config[$name]['provider'])) { 39 | throw Exception\MissingProviderConfigException::forProviderKey($name); 40 | } 41 | 42 | $provider = $config[$name]['provider']; 43 | 44 | return new $provider($config[$name]['options']); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Matthew Weier O'Phinney 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phly-expressive-oauth2clientauthentication 2 | 3 | > :warning: **Archived 2025-08-17** 4 | > 5 | > Use at your own risk. 6 | 7 | [![Build Status](https://secure.travis-ci.org/phly/phly-expressive-oauth2clientauthentication.svg?branch=master)](https://secure.travis-ci.org/phly/phly-expressive-oauth2clientauthentication) 8 | [![Coverage Status](https://coveralls.io/repos/github/phly/phly-expressive-oauth2clientauthentication/badge.svg?branch=master)](https://coveralls.io/github/phly/phly-expressive-oauth2clientauthentication?branch=master) 9 | 10 | This library provides a [league/oauth2-client](http://oauth2-client.thephpleague.com) 11 | adapter for use with [zend-expressive-authentication](https://docs.zendframework.com/zend-expressive-authentication). 12 | 13 | ## Installation 14 | 15 | Run the following to install this library: 16 | 17 | ```bash 18 | $ composer require phly/phly-expressive-oauth2clientauthentication 19 | ``` 20 | 21 | You will also need to install one or more of the OAuth2 providers you wish to 22 | use. As an example: 23 | 24 | ```bash 25 | $ composer require league/oauth2-instagram league/oauth2-google league/oauth2-facebook 26 | ``` 27 | 28 | ## Documentation 29 | 30 | Documentation is [in the doc tree](docs/book/), and can be compiled using [mkdocs](http://www.mkdocs.org): 31 | 32 | ```bash 33 | $ mkdocs build 34 | ``` 35 | 36 | You may also [browse the documentation online](https://phly.github.io/phly-expressive-oauth2clientauthentication/). 37 | -------------------------------------------------------------------------------- /src/Debug/DebugProviderMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | 19 | * 'oauth2clientauthentication' => [ 20 | * 'debug' => [ 21 | * 'callback_uri_template' => '/oauth2/debug/callback?code=%s&state=%s', 22 | * ], 23 | * ], 24 | * 25 | * 26 | * The URI should be in sprintf format. The code and state parameters MUST be 27 | * provided as query string arguments, and the code MUST precede the state. 28 | * 29 | * The path template should match the URI provided under the config key 30 | * oauth2clientauthentication.routes.debug. 31 | */ 32 | class DebugProviderMiddlewareFactory 33 | { 34 | public function __invoke(ContainerInterface $container) 35 | { 36 | $config = $container->has('config') ? $container->get('config') : []; 37 | $pathTemplate = $config['oauth2clientauthentication']['debug']['callback_uri_template'] 38 | ?? DebugProviderMiddleware::DEFAULT_PATH_TEMPLATE; 39 | return new DebugProviderMiddleware( 40 | $container->get(RedirectResponseFactory::class), 41 | $pathTemplate 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 21 | 'oauth2clientauthentication' => [], 22 | 'templates' => $this->getTemplates(), 23 | ]; 24 | } 25 | 26 | public function getDependencies() : array 27 | { 28 | return [ 29 | 'aliases' => [ 30 | AuthenticationInterface::class => OAuth2Adapter::class, 31 | ], 32 | 'factories' => [ 33 | OAuth2Adapter::class => OAuth2AdapterFactory::class, 34 | OAuth2CallbackMiddleware::class => OAuth2CallbackMiddlewareFactory::class, 35 | OAuth2ProviderFactory::class => OAuth2ProviderFactoryFactory::class, 36 | RedirectResponseFactory::class => RedirectResponseFactoryFactory::class, 37 | UnauthorizedResponseFactory::class => UnauthorizedResponseFactoryFactory::class, 38 | ], 39 | ]; 40 | } 41 | 42 | public function getTemplates() : array 43 | { 44 | return [ 45 | 'paths' => [ 46 | 'oauth2clientauthentication' => [__DIR__ . '/../templates'], 47 | ], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/UnauthorizedResponseFactoryFactory.php: -------------------------------------------------------------------------------- 1 | getAttribute('originalRequest', $request); 42 | 43 | $config = $container->has('config') ? $container->get('config') : []; 44 | $debug = array_key_exists('debug', $config) ? $config['debug'] : false; 45 | $authPath = $config['oauth2clientauthentication']['auth_path'] ?? self::DEFAULT_AUTH_PATH; 46 | 47 | $view = [ 48 | 'auth_path' => (string) $request->getUri()->withPath($authPath), 49 | 'redirect' => (string) $originalRequest->getUri(), 50 | 'debug' => (bool) $debug, 51 | ]; 52 | 53 | $response = $container->get(ResponseInterface::class)(); 54 | $renderer = $container->get(TemplateRendererInterface::class); 55 | 56 | $response->getBody()->write( 57 | $renderer->render(self::DEFAULT_TEMPLATE, $view) 58 | ); 59 | return $response->withStatus(401); 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phly/phly-expressive-oauth2clientauthentication", 3 | "description": "league/oauth2-client adapter for zendframework/zend-expressive-authentication", 4 | "license": "BSD-2-Clause", 5 | "keywords": [ 6 | "authentication", 7 | "oauth2", 8 | "expressive", 9 | "psr-7", 10 | "psr-15" 11 | ], 12 | "support": { 13 | "docs": "https://phly.github.io/phly-expressive-oauth2clientauthentication/", 14 | "issues": "https://github.com/phly/phly-expressive-oauth2clientauthentication/issues", 15 | "source": "https://github.com/phly/phly-expressive-oauth2clientauthentication", 16 | "rss": "https://github.com/phly/phly-expressive-oauth2clientauthentication/releases.atom" 17 | }, 18 | "require": { 19 | "php": "^7.1", 20 | "league/oauth2-client": "^2.2", 21 | "zendframework/zend-expressive": "^3.0", 22 | "zendframework/zend-expressive-authentication": "^1.0", 23 | "zendframework/zend-expressive-session": "^1.0" 24 | }, 25 | "require-dev": { 26 | "league/oauth2-github": "^2.0", 27 | "league/oauth2-google": "^2.0", 28 | "league/oauth2-instagram": "^2.0", 29 | "phpunit/phpunit": "^7.0.2", 30 | "zendframework/zend-coding-standard": "~1.0.0", 31 | "zendframework/zend-expressive-fastroute": "^3.0", 32 | "zendframework/zend-expressive-template": "^2.0" 33 | }, 34 | "suggest": { 35 | "league/oauth2-facebook": "To use the Facebook OAuth2 provider", 36 | "league/oauth2-github": "To use the GitHub OAuth2 provider", 37 | "league/oauth2-google": "To use the Google OAuth2 provider", 38 | "league/oauth2-instagram": "To use the Instagram OAuth2 provider", 39 | "league/oauth2-linkedin": "To use the LinkedIn OAuth2 provider", 40 | "zendframework/zend-expressive-template": "To use the shipped UnauthorizedResponseFactory, which includes template support" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Phly\\Expressive\\OAuth2ClientAuthentication\\": "src/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "PhlyTest\\Expressive\\OAuth2ClientAuthentication\\": "test/" 50 | } 51 | }, 52 | "config": { 53 | "sort-packages": true 54 | }, 55 | "extra": { 56 | "branch-alias": { 57 | "dev-master": "1.0.x-dev", 58 | "dev-develop": "1.1.x-dev" 59 | }, 60 | "zf": { 61 | "config-provider": "Phly\\Expressive\\OAuth2ClientAuthentication\\ConfigProvider" 62 | } 63 | }, 64 | "scripts": { 65 | "check": [ 66 | "@cs-check", 67 | "@test" 68 | ], 69 | "cs-check": "phpcs", 70 | "cs-fix": "phpcbf", 71 | "test": "phpunit --colors=always", 72 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Debug/DebugProvider.php: -------------------------------------------------------------------------------- 1 | authorizationUrl = $options['authorization_url'] ?? self::AUTHORIZATION_URL; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getState() 35 | { 36 | return self::STATE; 37 | } 38 | 39 | /** 40 | * @param array $options 41 | * @return string 42 | */ 43 | public function getAuthorizationUrl(array $options = []) 44 | { 45 | return $this->authorizationUrl; 46 | } 47 | 48 | /** 49 | * @param string $grant 50 | * @param array $options 51 | * @return AccessToken 52 | */ 53 | public function getAccessToken($grant, array $options = []) 54 | { 55 | return new AccessToken([ 56 | 'access_token' => self::TOKEN, 57 | ]); 58 | } 59 | 60 | /** 61 | * @param AccessToken $token 62 | * @return DebugResourceOwner 63 | */ 64 | public function getResourceOwner(AccessToken $token) 65 | { 66 | return new DebugResourceOwner(); 67 | } 68 | 69 | /** 70 | * No-op; implemented to fulfill abstract parent class. 71 | */ 72 | public function getBaseAuthorizationUrl() 73 | { 74 | } 75 | 76 | /** 77 | * No-op; implemented to fulfill abstract parent class. 78 | */ 79 | public function getBaseAccessTokenUrl(array $params) 80 | { 81 | } 82 | 83 | /** 84 | * No-op; implemented to fulfill abstract parent class. 85 | */ 86 | public function getResourceOwnerDetailsUrl(AccessToken $token) 87 | { 88 | } 89 | 90 | /** 91 | * No-op; implemented to fulfill abstract parent class. 92 | */ 93 | protected function getDefaultScopes() 94 | { 95 | } 96 | 97 | /** 98 | * No-op; implemented to fulfill abstract parent class. 99 | */ 100 | protected function checkResponse(ResponseInterface $response, $data) 101 | { 102 | } 103 | 104 | /** 105 | * No-op; implemented to fulfill abstract parent class. 106 | */ 107 | protected function createResourceOwner(array $response, AccessToken $token) 108 | { 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /config/oauth2clientauthentication.local.php: -------------------------------------------------------------------------------- 1 | [ 12 | // Configure the various OAuth2 providers. 13 | // 14 | // Each OAuth2 provider has its own configuration. You may need to review 15 | // http://oauth2-client.thephpleague.com/providers/league/ for details 16 | // on each and what configuration options they accept. 17 | 18 | // Debug 19 | // This is the debug provider shipped within this component for purposes 20 | // of testing the OAuth2 client workflow within your applications. 21 | 'debug' => [ 22 | // Provider key must be present for factory creation. 23 | 'provider' => Debug\DebugProvider::class, 24 | 'options' => [ 25 | // Provide this if you have provided an alternate route path via 26 | // the oauth2clientauthentication.routes.debug key: 27 | // 'callback_uri_template' => '/alternate/debug/callback?code=%s&state=%s', 28 | 29 | // Provide this if you want to use an alternate path for the OAuth2 30 | // "server" authorization: 31 | // 'authorization_url' => '/alternate/debug/authorization', 32 | ] 33 | ], 34 | 35 | // Facebook 36 | // 'facebook' => [ 37 | // 'provider' => Provider\Facebook::class, 38 | // 'options' => [ 39 | // 'clientId' => '{facebook-app-id}', 40 | // 'clientSecret' => '{facebook-app-secret}', 41 | // 'redirectUri' => '', // based on the auth_path + production route; must be fully qualifed 42 | // 'graphApiVersion' => 'v2.10', 43 | // ], 44 | // ], 45 | 46 | // GitHub 47 | // 'github' => [ 48 | // 'provider' => Provider\Github::class, 49 | // 'options' => [ 50 | // 'clientId' => '{github-client-id}', 51 | // 'clientSecret' => '{github-client-secret}', 52 | // 'redirectUri' => '', // based on the auth_path + production route; must be fully qualifed 53 | // ], 54 | // ], 55 | 56 | // Google 57 | // 'google' => [ 58 | // 'provider' => Provider\Google::class, 59 | // 'options' => [ 60 | // 'clientId' => '{google-client-id}', 61 | // 'clientSecret' => '{google-client-secret}', 62 | // 'redirectUri' => '', // based on the auth_path + production route; must be fully qualifed 63 | // 'hostedDomain' => '', // scheme + domain of your app 64 | // ], 65 | // ], 66 | 67 | // Instagram 68 | // 'instagram' => [ 69 | // 'provider' => Provider\Instagram::class, 70 | // 'options' => [ 71 | // 'clientId' => '{instagram-client-id}', 72 | // 'clientSecret' => '{instagram-client-secret}', 73 | // 'redirectUri' => '', // based on the auth_path + production route; must be fully qualifed 74 | // 'host' => 'https://api.instagram.com', // Optional; this is the default 75 | // ], 76 | // ], 77 | 78 | // LinkedIn 79 | // 'linkedin' => [ 80 | // 'provider' => Provider\LinkedIn::class, 81 | // 'options' => [ 82 | // 'clientId' => '{linkedin-client-id}', 83 | // 'clientSecret' => '{linkedin-client-secret}', 84 | // 'redirectUri' => '', // based on the auth_path + production route; must be fully qualifed 85 | // ], 86 | // ], 87 | 88 | // Customized 89 | // 'custom' => [ 90 | // 'provider' => Provider\GenericProvider::class, 91 | // 'options' => [ 92 | // 'clientId' => '', 93 | // 'clientSecret' => '', 94 | // 'redirectUri' => '', 95 | // 'urlAuthorize' => '', 96 | // 'urlAccessToken' => '', 97 | // 'urlResourceOwnerDetails' => '', 98 | // ], 99 | // ], 100 | ], 101 | 'dependencies' => [ 102 | 'factories' => [ 103 | // Enable this when in debug mode: 104 | // Debug\DebugProviderMiddleware::class => Debug\DebugProviderMiddlewareFactory::class, 105 | ], 106 | ], 107 | ]; 108 | -------------------------------------------------------------------------------- /src/OAuth2CallbackMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | 28 | * // In config/pipeline.php: 29 | * $app->pipe('/auth', \Phly\Expressive\OAuth2ClientAuthentication\OAuth2CallbackMiddleware::class); 30 | * 31 | * // In config/oauth2clientauthentication.global.php: 32 | * use Phly\Expressive\OAuth2ClientAuthentication\OAuth2CallbackMiddleware; 33 | * use Phly\Expressive\OAuth2ClientAuthentication\OAuth2CallbackMiddlewareFactory; 34 | * 35 | * return [ 36 | * 'dependencies' => [ 37 | * 'factories' => [ 38 | * OAuth2CallbackMiddleware::class => OAuth2CallbackMiddlewareFactory::class, 39 | * ], 40 | * ], 41 | * 'oauth2clientauthentication' => [ 42 | * 'auth_url' => '/auth', 43 | * // ... 44 | * ], 45 | * ]; 46 | * 47 | * 48 | * You may also provide alternate route strings: 49 | * 50 | * 51 | * 'oauth2clientauthentication' => [ 52 | * 'routes' => [ 53 | * // Production route for providers and their callbacks: 54 | * 'production' => '/:provider[/callback]', 55 | * // Debug route for providers and their callbacks: 56 | * 'debug' => '/:provider[/callback]', 57 | * ], 58 | * 'debug' =>> [ 59 | * // Authorization route for the debug provider: 60 | * 'authorization_url' => '/debug/verify', 61 | * ] 62 | * ], 63 | * 64 | */ 65 | class OAuth2CallbackMiddlewareFactory 66 | { 67 | public const ROUTE_DEBUG = '/{provider:debug|facebook|github|google|instagram|linkedin}[/oauth2callback]'; 68 | public const ROUTE_DEBUG_AUTHORIZE = '/debug/authorize'; 69 | public const ROUTE_PROD = '/{provider:facebook|github|google|instagram|linkedin}[/oauth2callback]'; 70 | 71 | public function __invoke(ContainerInterface $container) : MiddlewareInterface 72 | { 73 | $factory = $container->get(MiddlewareFactory::class); 74 | $router = $this->getRouter($container); 75 | 76 | $pipeline = new MiddlewarePipe(); 77 | 78 | $config = $container->has('config') ? $container->get('config') : []; 79 | $debug = $config['debug'] ?? false; 80 | $routes = $config['oauth2clientauthentication']['routes'] ?? []; 81 | $route = $this->getRouteFromConfig($routes, (bool) $debug); 82 | 83 | // OAuth2 providers rely on session to persist the user details 84 | $pipeline->pipe($factory->lazy(SessionMiddleware::class)); 85 | $router->addRoute(new Route( 86 | $route, 87 | $factory->lazy(AuthenticationMiddleware::class), 88 | ['GET'] 89 | )); 90 | 91 | if ($debug) { 92 | $path = $config['oauth2clientauthentication']['debug']['authorization_url'] ?? self::ROUTE_DEBUG_AUTHORIZE; 93 | $router->addRoute(new Route( 94 | $path, 95 | $factory->lazy(Debug\DebugProviderMiddleware::class), 96 | ['GET'] 97 | )); 98 | } 99 | 100 | $pipeline->pipe(new RouteMiddleware($router)); 101 | $pipeline->pipe($factory->lazy(DispatchMiddleware::class)); 102 | 103 | return $pipeline; 104 | } 105 | 106 | private function getRouteFromConfig(array $routes, bool $debug) : string 107 | { 108 | if ($debug) { 109 | return $routes['debug'] ?? self::ROUTE_DEBUG; 110 | } 111 | 112 | return $routes['production'] ?? self::ROUTE_PROD; 113 | } 114 | 115 | private function getRouter(ContainerInterface $container) : RouterInterface 116 | { 117 | $router = $container->get(RouterInterface::class); 118 | $class = get_class($router); 119 | return new $class(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.0.1 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 2.0.0 - 2019-11-12 28 | 29 | ### Added 30 | 31 | - [#3](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/3) adds support for all League OAuth2 Clients that inherits from the upstream `League\OAuth2\Client\Provider\AbstractProvider`. 32 | 33 | - [#3](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/3) adds the method `forProviderKey(string $provider)` in `MissingProviderConfigException` in order to assert that the provider key has been set for providers in the configuration. 34 | 35 | ### Changed 36 | 37 | - [#3](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/3) changes array disposition in the configuration files to include `provider` and `options` keys **(BC break)**. The provider array key tells the factory what to instantiate, and the options value is passed to the `Provider` constructor. Read the documentation on [local/environment-specific configuration](https://phly.github.io/phly-expressive-oauth2clientauthentication/config/) for specific implementation details and examples. 38 | 39 | - [#3](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/3) allows the username to default to `$resourceOwner->getId()` in method `getUsernameFromResourceOwner(ResourceOwnerInterface $resourceOwner) : string` if methods `$resourceOwner->getEmail()` and `$resourceOwner->getNickname()` don't exist, instead of throwing an `UnexpectedResourceOwnerTypeException`. 40 | 41 | ### Deprecated 42 | 43 | - Nothing. 44 | 45 | ### Removed 46 | 47 | - [#3](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/3) removes `UnsupportedProviderException`, as it is not used anymore. 48 | 49 | - [#3](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/3) removes `UnexpectedResourceOwnerTypeException`, as it is not used anymore. 50 | 51 | ### Fixed 52 | 53 | - [#3](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/3) fixes a namespace reference within a shipped config file. 54 | 55 | ## 1.0.0 - 2018-10-18 56 | 57 | ### Added 58 | 59 | - Adds the method `OAuth2User::getDetail(string $name, $default = null)` in 60 | order to fulfill the zend-expressive-authentication 1.0.0 API for the 61 | `UserInterface`. 62 | 63 | ### Changed 64 | 65 | - The method `OAuth2User::getUserRoles() : array` was refactored to 66 | `OAuth2User::getRoles() : iterable` in order to match the 67 | zend-expressive-authentication 1.0.0 API. 68 | 69 | - The method `OAuth2User::getUserData() : array` was refactored to 70 | `OAuth2User::getDetails() : array` in order to match the 71 | zend-expressive-authentication 1.0.0 API. 72 | 73 | ### Deprecated 74 | 75 | - Nothing. 76 | 77 | ### Removed 78 | 79 | - Nothing. 80 | 81 | ### Fixed 82 | 83 | - Nothing. 84 | 85 | ## 0.2.1 - 2018-03-28 86 | 87 | ### Added 88 | 89 | - Nothing. 90 | 91 | ### Changed 92 | 93 | - Nothing. 94 | 95 | ### Deprecated 96 | 97 | - Nothing. 98 | 99 | ### Removed 100 | 101 | - Nothing. 102 | 103 | ### Fixed 104 | 105 | - Fixes how the callback factory produces a pipeline. Instead of using an 106 | `Application` instance derived from the `ApplicationFactory` (which will 107 | receive a shared route collector and shared middleware), it now produces a 108 | `MiddlewarePipe` instance into which it pipes the various middleware. It also 109 | creates a _new_ router, based on the type returned from the container (it 110 | assumes no constructor arguments are necessary), and passes that to a new 111 | `RouteMiddleware` instance to ensure it is sandboxed from the main 112 | application. 113 | 114 | ## 0.2.0 - 2018-03-27 115 | 116 | ### Added 117 | 118 | - [#1](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/1) 119 | adds support for Expressive v3 and zend-expressive-authentication 0.4+. 120 | 121 | ### Changed 122 | 123 | - Nothing. 124 | 125 | ### Deprecated 126 | 127 | - Nothing. 128 | 129 | ### Removed 130 | 131 | - [#1](https://github.com/phly/phly-expressive-oauth2clientauthentication/pull/1) 132 | removes support for Expressive v2 releases, including pre-0.4 releases of 133 | zend-expressive-authentication. 134 | 135 | ### Fixed 136 | 137 | - Nothing. 138 | 139 | ## 0.1.2 - 2017-11-16 140 | 141 | ### Added 142 | 143 | - Nothing. 144 | 145 | ### Changed 146 | 147 | - Nothing. 148 | 149 | ### Deprecated 150 | 151 | - Nothing. 152 | 153 | ### Removed 154 | 155 | - Nothing. 156 | 157 | ### Fixed 158 | 159 | - Fixes an import in the DebugProviderMiddlewareFactory for the 160 | RedirectResponseFactory to ensure it resolves correctly. 161 | 162 | ## 0.1.1 - 2017-11-16 163 | 164 | ### Added 165 | 166 | - Adds templates for PlatesPHP, Twig, and zend-view. 167 | - Adds documentation covering templates. 168 | 169 | ### Changed 170 | 171 | - Nothing. 172 | 173 | ### Deprecated 174 | 175 | - Nothing. 176 | 177 | ### Removed 178 | 179 | - Nothing. 180 | 181 | ### Fixed 182 | 183 | - Nothing. 184 | 185 | ## 0.1.0 - 2017-11-16 186 | 187 | ### Added 188 | 189 | - Everything. 190 | 191 | ### Changed 192 | 193 | - Nothing. 194 | 195 | ### Deprecated 196 | 197 | - Nothing. 198 | 199 | ### Removed 200 | 201 | - Nothing. 202 | 203 | ### Fixed 204 | 205 | - Nothing. 206 | -------------------------------------------------------------------------------- /src/OAuth2Adapter.php: -------------------------------------------------------------------------------- 1 | providerFactory = $providerFactory; 31 | $this->unauthorizedResponseFactory = $unauthorizedResponseFactory; 32 | $this->redirectResponseFactory = $redirectResponseFactory; 33 | } 34 | 35 | /** 36 | * Authenticate the PSR-7 request and return a valid user 37 | * or null if not authenticated. 38 | * 39 | * In the case of a successful authorization request from the provider, 40 | * this method will still return null; this is to allow us to redirect to 41 | * the original page requesting authorization. 42 | * 43 | * On subsequent requests, this method will return the authenticated 44 | * user as retrieved from the session. 45 | */ 46 | public function authenticate(ServerRequestInterface $request) : ?UserInterface 47 | { 48 | $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); 49 | 50 | // Have we authenticated before? If so, return the authenticated user. 51 | if ($this->isAuthenticatedSession($session)) { 52 | return $this->getUserFromSession($session); 53 | } 54 | 55 | $params = $request->getQueryParams(); 56 | 57 | // If the parameters indicate an error, we should raise an exception. 58 | if (! empty($params['error'])) { 59 | return $this->processError($params['error']); 60 | } 61 | 62 | // Is the authentication request from a known route that defines the 63 | // provider to use? If not, we need to display the authentication page. 64 | $providerType = $request->getAttribute('provider'); 65 | if (null === $providerType) { 66 | return null; 67 | } 68 | 69 | $provider = $this->providerFactory->createProvider($providerType); 70 | 71 | $oauth2SessionData = $session->get('auth'); 72 | $oauth2SessionData = is_array($oauth2SessionData) ? $oauth2SessionData : []; 73 | 74 | // No code present in query string, meaning we need to request one from 75 | // the OAuth2 provider. We'll set the session state, so that we can send 76 | // a redirect via the unauthorized response. 77 | if (empty($params['code'])) { 78 | return $this->requestAuthorization( 79 | $provider, 80 | $session, 81 | $oauth2SessionData, 82 | $params['redirect'] ?? '' 83 | ); 84 | } 85 | 86 | // No oauth2 state present, so simply unauthorized 87 | if (empty($params['state']) 88 | || ! isset($oauth2SessionData['state']) 89 | || $params['state'] !== $oauth2SessionData['state'] 90 | ) { 91 | return null; 92 | } 93 | 94 | // Handling redirect from OAuth2 provider 95 | try { 96 | $token = $provider->getAccessToken('authorization_code', [ 97 | 'code' => $params['code'], 98 | ]); 99 | 100 | $resourceOwner = $provider->getResourceOwner($token); 101 | } catch (\Exception $e) { 102 | return $this->processError($e); 103 | } 104 | 105 | // Authenticated! Store details in session so we can redirect to the 106 | // page requesting authorization. 107 | $oauth2SessionData['user'] = array_merge( 108 | $resourceOwner->toArray(), 109 | ['username' => $this->getUsernameFromResourceOwner($resourceOwner)] 110 | ); 111 | 112 | $oauth2SessionData['redirect'] = $oauth2SessionData['redirect'] ?? '/'; 113 | 114 | $session->set('auth', $oauth2SessionData); 115 | 116 | return null; 117 | } 118 | 119 | /** 120 | * Generate the unauthorized response. 121 | * 122 | * In some cases, this is simply generating redirects: 123 | * 124 | * - if a request for authorization has been made 125 | * - if we've received a valid authorization from the provider 126 | * 127 | * Otherwise, we display the login page. 128 | */ 129 | public function unauthorizedResponse(ServerRequestInterface $request) : ResponseInterface 130 | { 131 | $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); 132 | 133 | $oauth2SessionData = $session->get('auth'); 134 | 135 | // Successfully authorized; time to redirect 136 | if (is_array($oauth2SessionData) 137 | && isset($oauth2SessionData['user']) 138 | && isset($oauth2SessionData['redirect']) 139 | ) { 140 | $redirect = $oauth2SessionData['redirect']; 141 | unset($oauth2SessionData['redirect']); 142 | $session->set('auth', $oauth2SessionData); 143 | return ($this->redirectResponseFactory)($redirect); 144 | } 145 | 146 | // Request for authorization has been made 147 | if (is_array($oauth2SessionData) && isset($oauth2SessionData['authorization_url'])) { 148 | $authorizationUrl = $oauth2SessionData['authorization_url']; 149 | unset($oauth2SessionData['authorization_url']); 150 | $session->set('auth', $oauth2SessionData); 151 | return ($this->redirectResponseFactory)($authorizationUrl); 152 | } 153 | 154 | // No credentials or prior authorization request present 155 | return ($this->unauthorizedResponseFactory)($request); 156 | } 157 | 158 | private function isAuthenticatedSession(SessionInterface $session) : bool 159 | { 160 | $data = $session->get('auth'); 161 | return is_array($data) && isset($data['user']['username']); 162 | } 163 | 164 | private function getUserFromSession(SessionInterface $session) : UserInterface 165 | { 166 | $data = $session->get('auth'); 167 | $username = $data['user']['username']; 168 | return new OAuth2User($username, $data['user']); 169 | } 170 | 171 | /** 172 | * @param string|\Throwable 173 | * @throws Exception\OAuth2ProviderException 174 | */ 175 | private function processError($error) 176 | { 177 | if (is_string($error)) { 178 | throw Exception\OAuth2ProviderException::forErrorString($error); 179 | } 180 | throw Exception\OAuth2ProviderException::forThrowable($error); 181 | } 182 | 183 | private function requestAuthorization( 184 | AbstractProvider $provider, 185 | SessionInterface $session, 186 | array $sessionData, 187 | string $redirect 188 | ) { 189 | // Authorization URL MUST be generated BEFORE we retrieve the state, 190 | // as it is responsible for generating the state in the first place! 191 | $authorizationUrl = $provider->getAuthorizationUrl(); 192 | 193 | if (! empty($redirect)) { 194 | $sessionData['redirect'] = $redirect; 195 | } 196 | 197 | $sessionData['state'] = $provider->getState(); 198 | $sessionData['authorization_url'] = $authorizationUrl; 199 | $session->set('auth', $sessionData); 200 | } 201 | 202 | private function getUsernameFromResourceOwner(ResourceOwnerInterface $resourceOwner) : string 203 | { 204 | if (method_exists($resourceOwner, 'getEmail')) { 205 | // All official providers except Instagram 206 | return $resourceOwner->getEmail(); 207 | } 208 | 209 | if (method_exists($resourceOwner, 'getNickname')) { 210 | // Instagram 211 | return $resourceOwner->getNickname(); 212 | } 213 | 214 | // If none of the methods above exists, getId() is always present in a ResourceOwner. 215 | return $resourceOwner->getId(); 216 | } 217 | } 218 | --------------------------------------------------------------------------------