├── .github └── workflows │ └── phpunit.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── DependencyInjection │ └── GuzzleBundleOAuth2Extension.php ├── GuzzleBundleOAuth2Plugin.php ├── Middleware │ ├── CachedOAuthMiddleware.php │ └── PersistentOAuthMiddleware.php └── Resources │ └── config │ └── services.xml └── tests ├── DependencyInjection └── GuzzleBundleOAuth2ExtensionTest.php └── GuzzleBundleOAuth2PluginTest.php /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [ubuntu-latest] 14 | php: 15 | - '7.2' 16 | - '8.3' 17 | symfony: 18 | - '5.0.*' 19 | - '5.4.*' # LTS 20 | - '6.0.*' 21 | - '7.0.*' 22 | exclude: 23 | - php: '7.2' 24 | symfony: '6.0.*' # requires PHP >=8.1 25 | - php: '7.2' 26 | symfony: '7.0.*' # requires PHP >=8.2 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | env: 31 | SYMFONY: ${{ matrix.symfony }} 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Install PHP 37 | uses: shivammathur/setup-php@v2 38 | with: 39 | php-version: ${{ matrix.php }} 40 | ini-values: date.timezone='UTC' 41 | tools: composer:v2 42 | 43 | - name: Require symfony 44 | run: composer --no-update require symfony/symfony:"${SYMFONY}" 45 | 46 | - name: Install dependencies 47 | run: | 48 | composer update 49 | vendor/bin/simple-phpunit install 50 | - name: Test 51 | run: | 52 | composer validate --strict --no-check-lock 53 | vendor/bin/simple-phpunit --coverage-text --verbose 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore potentially sensitive phpunit file 2 | /phpunit.xml 3 | 4 | # Ignore composer generated files 5 | /composer.lock 6 | /vendor/ 7 | 8 | # Ignore generated files 9 | /build/ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Gregurco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guzzle Bundle OAuth2 Plugin 2 | 3 | [![Build Status](https://travis-ci.org/gregurco/GuzzleBundleOAuth2Plugin.svg?branch=master)](https://travis-ci.org/gregurco/GuzzleBundleOAuth2Plugin) 4 | [![Coverage Status](https://coveralls.io/repos/gregurco/GuzzleBundleOAuth2Plugin/badge.svg?branch=master)](https://coveralls.io/r/gregurco/GuzzleBundleOAuth2Plugin) 5 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/eba4f2e6-2c2a-4e92-85b6-c32ab3ac3aa7/mini.png)](https://insight.sensiolabs.com/projects/eba4f2e6-2c2a-4e92-85b6-c32ab3ac3aa7) 6 | 7 | This plugin integrates [OAuth2][1] functionality into [Guzzle Bundle][2], a bundle for building RESTful web service clients. 8 | 9 | ---- 10 | 11 | ## Prerequisites 12 | - PHP 7.2 or above 13 | - [Guzzle Bundle][2] 14 | - [guzzle-oauth2-plugin][3] 15 | 16 | ## Installation 17 | 18 | To install this bundle, run the command below on the command line and you will get the latest stable version from [Packagist][4]. 19 | 20 | ``` bash 21 | composer require gregurco/guzzle-bundle-oauth2-plugin 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Enable bundle 27 | 28 | Find next lines in `src/Kernel.php`: 29 | 30 | ```php 31 | foreach ($contents as $class => $envs) { 32 | if (isset($envs['all']) || isset($envs[$this->environment])) { 33 | yield new $class(); 34 | } 35 | } 36 | ``` 37 | 38 | and replace them by: 39 | 40 | ```php 41 | foreach ($contents as $class => $envs) { 42 | if (isset($envs['all']) || isset($envs[$this->environment])) { 43 | if ($class === \EightPoints\Bundle\GuzzleBundle\EightPointsGuzzleBundle::class) { 44 | yield new $class([ 45 | new \Gregurco\Bundle\GuzzleBundleOAuth2Plugin\GuzzleBundleOAuth2Plugin(), 46 | ]); 47 | } else { 48 | yield new $class(); 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ### Basic configuration 55 | 56 | #### With default grant type (client) 57 | 58 | ``` yaml 59 | # app/config/config.yml 60 | 61 | eight_points_guzzle: 62 | clients: 63 | api_payment: 64 | base_url: "http://api.domain.tld" 65 | 66 | options: 67 | auth: oauth2 68 | 69 | # plugin settings 70 | plugin: 71 | oauth2: 72 | base_uri: "https://example.com" 73 | token_url: "/oauth/token" 74 | client_id: "test-client-id" 75 | client_secret: "test-client-secret" # optional 76 | scope: "administration" 77 | ``` 78 | 79 | #### With password grant type 80 | 81 | ``` yaml 82 | # app/config/config.yml 83 | 84 | eight_points_guzzle: 85 | clients: 86 | api_payment: 87 | base_url: "http://api.domain.tld" 88 | 89 | options: 90 | auth: oauth2 91 | 92 | # plugin settings 93 | plugin: 94 | oauth2: 95 | base_uri: "https://example.com" 96 | token_url: "/oauth/token" 97 | client_id: "test-client-id" 98 | username: "johndoe" 99 | password: "A3ddj3w" 100 | scope: "administration" 101 | grant_type: "Sainsburys\\Guzzle\\Oauth2\\GrantType\\PasswordCredentials" 102 | ``` 103 | 104 | #### With client credentials in body 105 | 106 | ``` yaml 107 | # app/config/config.yml 108 | 109 | eight_points_guzzle: 110 | clients: 111 | api_payment: 112 | base_url: "http://api.domain.tld" 113 | 114 | options: 115 | auth: oauth2 116 | 117 | # plugin settings 118 | plugin: 119 | oauth2: 120 | base_uri: "https://example.com" 121 | token_url: "/oauth/token" 122 | client_id: "test-client-id" 123 | scope: "administration" 124 | auth_location: "body" 125 | ``` 126 | 127 | ### Options 128 | 129 | | Key | Description | Required | Example | 130 | | --- | --- | --- | --- | 131 | | base_uri | URL of oAuth2 server.| yes | https://example.com | 132 | | token_url | The path that will be concatenated with base_uri.
Default: `/oauth2/token`| no | /oauth/token | 133 | | client_id | The client identifier issued to the client during the registration process | yes | s6BhdRkqt3 | 134 | | client_secret | The client secret | no | 7Fjfp0ZBr1KtDRbnfVdmIw | 135 | | username | The resource owner username | for PasswordCredentials grant type | johndoe | 136 | | password | The resource owner password | for PasswordCredentials grant type | A3ddj3w | 137 | | auth_location | The place where to put client_id and client_secret in auth request.
Default: headers. Allowed values: body, headers. | no | body | 138 | | resource | The App ID URI of the web API (secured resource) | no | https://service.contoso.com/ | 139 | | private_key | Path to private key | for JwtBearer grant type | `"%kernel.root_dir%/path/to/private.key"` | 140 | | scope | One or more scope values indicating which parts of the user's account you wish to access | no | administration | 141 | | audience | | no | | 142 | | grant_type | Grant type class path. Class should implement GrantTypeInterface.
Default: `Sainsburys\\Guzzle\\Oauth2\\GrantType\\ClientCredentials` | no | `Sainsburys\\Guzzle\\Oauth2\\GrantType\\PasswordCredentials`
`Sainsburys\\Guzzle\\Oauth2\\GrantType\\AuthorizationCode`
`Sainsburys\\Guzzle\\Oauth2\\GrantType\\JwtBearer` | 143 | | persistent | Token will be stored in session unless grant_type is client credentials; in which case it will be stored in the app cache.
Default: false | no | | 144 | | retry_limit | How many times request will be repeated on failure.
Default: 5 | no | | 145 | 146 | See more information about middleware [here][3]. 147 | 148 | ## License 149 | 150 | This middleware is licensed under the MIT License - see the LICENSE file for details 151 | 152 | [1]: http://www.xml.com/pub/a/2003/12/17/dive.html 153 | [2]: https://github.com/8p/EightPointsGuzzleBundle 154 | [3]: https://github.com/Sainsburys/guzzle-oauth2-plugin 155 | [4]: https://packagist.org/packages/gregurco/guzzle-bundle-oauth2-plugin 156 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gregurco/guzzle-bundle-oauth2-plugin", 3 | "type": "library", 4 | "description": "OAuth2 Plugin for Guzzle Bundle, a PHP HTTP client library and framework for building RESTful web service clients", 5 | "keywords": ["oauth2", "middleware", "plugin", "framework", "http", "rest", "web service", "curl", "client", "HTTP client"], 6 | "homepage": "https://github.com/gregurco/GuzzleBundleOAuth2Plugin", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Gregurco Vlad", 11 | "email": "gregurco.vlad@gmail.com", 12 | "homepage": "https://github.com/gregurco" 13 | }, 14 | { 15 | "name": "Community", 16 | "homepage": "https://github.com/gregurco/GuzzleBundleOAuth2Plugin/contributors" 17 | } 18 | ], 19 | 20 | "require": { 21 | "php": ">=7.2", 22 | "guzzlehttp/guzzle": "^6.5.8|^7.4.5", 23 | "eightpoints/guzzle-bundle": "^8.0", 24 | "symfony/http-kernel": "~5.0|~6.0|~7.0", 25 | "symfony/config": "~5.0|~6.0|~7.0", 26 | "symfony/dependency-injection": "~5.0|~6.0|~7.0", 27 | "symfony/expression-language": "~5.0|~6.0|~7.0", 28 | "sainsburys/guzzle-oauth2-plugin": "^3.0" 29 | }, 30 | 31 | "require-dev": { 32 | "symfony/phpunit-bridge": "~5.0|~6.0|~7.0", 33 | "php-coveralls/php-coveralls": "^2.2" 34 | }, 35 | 36 | "autoload": { 37 | "psr-4": { 38 | "Gregurco\\Bundle\\GuzzleBundleOAuth2Plugin\\": "src" 39 | } 40 | }, 41 | 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Gregurco\\Bundle\\GuzzleBundleOAuth2Plugin\\Tests\\": "tests" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | 20 | ./src 21 | 22 | ./src/Resources/ 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/DependencyInjection/GuzzleBundleOAuth2Extension.php: -------------------------------------------------------------------------------- 1 | load('services.xml'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/GuzzleBundleOAuth2Plugin.php: -------------------------------------------------------------------------------- 1 | load($configs, $container); 33 | } 34 | 35 | /** 36 | * @param array $config 37 | * @param ContainerBuilder $container 38 | * @param string $clientName 39 | * @param Definition $handler 40 | */ 41 | public function loadForClient(array $config, ContainerBuilder $container, string $clientName, Definition $handler) : void 42 | { 43 | if ($config['enabled']) { 44 | $middlewareConfig = [ 45 | PasswordCredentials::CONFIG_USERNAME => $config['username'], 46 | PasswordCredentials::CONFIG_PASSWORD => $config['password'], 47 | GrantTypeBase::CONFIG_CLIENT_ID => $config['client_id'], 48 | GrantTypeBase::CONFIG_CLIENT_SECRET => $config['client_secret'], 49 | GrantTypeBase::CONFIG_TOKEN_URL => $config['token_url'], 50 | GrantTypeBase::CONFIG_AUTH_LOCATION => $config['auth_location'], 51 | GrantTypeBase::CONFIG_RESOURCE => $config['resource'], 52 | JwtBearer::CONFIG_PRIVATE_KEY => null, 53 | 'scope' => $config['scope'], 54 | 'audience' => $config['audience'], 55 | ]; 56 | 57 | if ($config['private_key']) { 58 | // Define Client 59 | $privateKeyDefinitionName = sprintf('guzzle_bundle_oauth2_plugin.private_key.%s', $clientName); 60 | $privateKeyDefinition = new Definition(\SplFileObject::class); 61 | $privateKeyDefinition->addArgument($config['private_key']); 62 | $privateKeyDefinition->setPublic(true); 63 | $container->setDefinition($privateKeyDefinitionName, $privateKeyDefinition); 64 | 65 | $middlewareConfig[JwtBearer::CONFIG_PRIVATE_KEY] = new Reference($privateKeyDefinitionName); 66 | } 67 | 68 | // Define Client 69 | $oauthClientDefinitionName = sprintf('guzzle_bundle_oauth2_plugin.client.%s', $clientName); 70 | $oauthClientDefinition = new Definition(Client::class); 71 | $oauthClientDefinition->addArgument(['base_uri' => $config['base_uri']]); 72 | $oauthClientDefinition->setPublic(true); 73 | $container->setDefinition($oauthClientDefinitionName, $oauthClientDefinition); 74 | 75 | // Define password credentials 76 | $passwordCredentialsDefinitionName = sprintf('guzzle_bundle_oauth2_plugin.password_credentials.%s', $clientName); 77 | $passwordCredentialsDefinition = new Definition($config['grant_type']); 78 | $passwordCredentialsDefinition->addArgument(new Reference($oauthClientDefinitionName)); 79 | $passwordCredentialsDefinition->addArgument($middlewareConfig); 80 | $passwordCredentialsDefinition->setPublic(true); 81 | $container->setDefinition($passwordCredentialsDefinitionName, $passwordCredentialsDefinition); 82 | 83 | // Define refresh token 84 | $refreshTokenDefinitionName = sprintf('guzzle_bundle_oauth2_plugin.refresh_token.%s', $clientName); 85 | $refreshTokenDefinition = new Definition(RefreshToken::class); 86 | $refreshTokenDefinition->addArgument(new Reference($oauthClientDefinitionName)); 87 | $refreshTokenDefinition->addArgument($middlewareConfig); 88 | $refreshTokenDefinition->setPublic(true); 89 | $container->setDefinition($refreshTokenDefinitionName, $refreshTokenDefinition); 90 | 91 | //Define middleware 92 | $oAuth2MiddlewareDefinitionName = sprintf('guzzle_bundle_oauth2_plugin.middleware.%s', $clientName); 93 | if ($config['persistent']) { 94 | if ($config['grant_type'] === ClientCredentials::class) { 95 | $oAuth2MiddlewareDefinition = new Definition('%guzzle_bundle_oauth2_plugin.cached_middleware.class%'); 96 | $oAuth2MiddlewareDefinition->setArguments( 97 | [ 98 | new Reference($oauthClientDefinitionName), 99 | new Reference($passwordCredentialsDefinitionName), 100 | new Reference($refreshTokenDefinitionName), 101 | new Reference(AdapterInterface::class), 102 | $clientName 103 | ] 104 | ); 105 | } else { 106 | $oAuth2MiddlewareDefinition = new Definition('%guzzle_bundle_oauth2_plugin.persistent_middleware.class%'); 107 | $oAuth2MiddlewareDefinition->setArguments( 108 | [ 109 | new Reference($oauthClientDefinitionName), 110 | new Reference($passwordCredentialsDefinitionName), 111 | new Reference($refreshTokenDefinitionName), 112 | new Reference('session'), 113 | $clientName 114 | ] 115 | ); 116 | } 117 | } else { 118 | $oAuth2MiddlewareDefinition = new Definition('%guzzle_bundle_oauth2_plugin.middleware.class%'); 119 | $oAuth2MiddlewareDefinition->setArguments([ 120 | new Reference($oauthClientDefinitionName), 121 | new Reference($passwordCredentialsDefinitionName), 122 | new Reference($refreshTokenDefinitionName) 123 | ]); 124 | } 125 | 126 | $oAuth2MiddlewareDefinition->setPublic(true); 127 | $container->setDefinition($oAuth2MiddlewareDefinitionName, $oAuth2MiddlewareDefinition); 128 | 129 | $onBeforeExpression = new Expression(sprintf('service("%s").onBefore()', $oAuth2MiddlewareDefinitionName)); 130 | $onFailureExpression = new Expression(sprintf( 131 | 'service("%s").onFailure(%d)', 132 | $oAuth2MiddlewareDefinitionName, 133 | $config['retry_limit'] 134 | )); 135 | 136 | $handler->addMethodCall('push', [$onBeforeExpression]); 137 | $handler->addMethodCall('push', [$onFailureExpression]); 138 | } 139 | } 140 | 141 | /** 142 | * @param ArrayNodeDefinition $pluginNode 143 | */ 144 | public function addConfiguration(ArrayNodeDefinition $pluginNode) : void 145 | { 146 | $pluginNode 147 | ->canBeEnabled() 148 | ->validate() 149 | ->ifTrue(function (array $config) { 150 | return $config['enabled'] === true && empty($config['base_uri']); 151 | }) 152 | ->thenInvalid('base_uri is required') 153 | ->end() 154 | ->validate() 155 | ->ifTrue(function (array $config) { 156 | return $config['enabled'] === true && empty($config['client_id']); 157 | }) 158 | ->thenInvalid('client_id is required') 159 | ->end() 160 | ->validate() 161 | ->ifTrue(function (array $config) { 162 | return $config['enabled'] === true && 163 | $config['grant_type'] === PasswordCredentials::class && 164 | (empty($config['username']) || empty($config['password'])); 165 | }) 166 | ->thenInvalid('username and password are required') 167 | ->end() 168 | ->validate() 169 | ->ifTrue(function (array $config) { 170 | return $config['enabled'] === true && 171 | $config['grant_type'] === JwtBearer::class && 172 | empty($config['private_key']); 173 | }) 174 | ->thenInvalid('private_key is required') 175 | ->end() 176 | ->children() 177 | ->scalarNode('base_uri')->defaultNull()->end() 178 | ->scalarNode('username')->defaultNull()->end() 179 | ->scalarNode('password')->defaultNull()->end() 180 | ->scalarNode('client_id')->defaultNull()->end() 181 | ->scalarNode('client_secret')->defaultNull()->end() 182 | ->scalarNode('token_url')->defaultNull()->end() 183 | ->scalarNode('scope')->defaultNull()->end() 184 | ->scalarNode('audience')->defaultNull()->end() 185 | ->scalarNode('resource')->defaultNull()->end() 186 | ->scalarNode('private_key')->defaultNull()->end() 187 | ->scalarNode('auth_location') 188 | ->defaultValue('headers') 189 | ->validate() 190 | ->ifNotInArray(['headers', 'body']) 191 | ->thenInvalid('Invalid auth_location %s. Allowed values: headers, body.') 192 | ->end() 193 | ->end() 194 | ->scalarNode('grant_type') 195 | ->defaultValue(ClientCredentials::class) 196 | ->validate() 197 | ->ifTrue(function ($v) { 198 | return !is_subclass_of($v, GrantTypeInterface::class); 199 | }) 200 | ->thenInvalid(sprintf('Use instance of %s in grant_type', GrantTypeInterface::class)) 201 | ->end() 202 | ->end() 203 | ->booleanNode('persistent')->defaultFalse()->end() 204 | ->booleanNode('retry_limit')->defaultValue(5)->end() 205 | ->end(); 206 | } 207 | 208 | /** 209 | * @return string 210 | */ 211 | public function getPluginName() : string 212 | { 213 | return 'oauth2'; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Middleware/CachedOAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | cacheClient = $cacheClient; 44 | $this->clientName = $clientName; 45 | } 46 | 47 | /** 48 | * Get a new access token. 49 | * 50 | * @throws InvalidArgumentException 51 | * 52 | * @return AccessToken|null 53 | */ 54 | protected function acquireAccessToken() 55 | { 56 | $token = parent::acquireAccessToken(); 57 | 58 | $this->cacheToken($token); 59 | 60 | return $token; 61 | } 62 | 63 | /** 64 | * cacheToken sets the token in the cache adapter 65 | * 66 | * @param AccessToken $token 67 | * 68 | * @throws InvalidArgumentException 69 | */ 70 | protected function cacheToken(AccessToken $token) 71 | { 72 | $item = $this->cacheClient->getItem(sprintf('oauth.token.%s', $this->clientName)); 73 | 74 | $expires = $token->getExpires(); 75 | 76 | $item->set( 77 | [ 78 | 'token' => $token->getToken(), 79 | 'type' => $token->getType(), 80 | 'data' => array_merge($token->getData(), ['expires' => $expires->getTimestamp()]), 81 | ] 82 | ); 83 | 84 | if ($expires) { 85 | $item->expiresAt($expires->sub(\DateInterval::createFromDateString('10 seconds'))); 86 | } 87 | 88 | $this->cacheClient->saveDeferred($item); 89 | } 90 | 91 | /** 92 | * getAccessToken will get the oauth token from the cache if available 93 | * 94 | * @throws \Exception 95 | * @throws InvalidArgumentException 96 | * 97 | * @return null|AccessToken 98 | */ 99 | public function getAccessToken() 100 | { 101 | if ($this->accessToken === null) { 102 | $this->restoreTokenFromCache(); 103 | } 104 | 105 | return parent::getAccessToken(); 106 | } 107 | 108 | /** 109 | * restoreTokenFromCache 110 | * 111 | * @throws \Exception 112 | * @throws InvalidArgumentException 113 | */ 114 | protected function restoreTokenFromCache() 115 | { 116 | $item = $this->cacheClient->getItem(sprintf('oauth.token.%s', $this->clientName)); 117 | 118 | if ($item->isHit()) { 119 | $tokenData = $item->get(); 120 | 121 | $this->setAccessToken( 122 | new AccessToken( 123 | $tokenData['token'], 124 | $tokenData['type'], 125 | $tokenData['data'] 126 | ) 127 | ); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Middleware/PersistentOAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | session = $session; 39 | $this->clientName = $clientName; 40 | } 41 | 42 | /** 43 | * Get a new access token. 44 | * 45 | * @return AccessToken|null 46 | */ 47 | protected function acquireAccessToken() 48 | { 49 | $token = parent::acquireAccessToken(); 50 | 51 | $this->storeTokenInSession($token); 52 | 53 | return $token; 54 | } 55 | 56 | /** 57 | * @param AccessToken $token 58 | */ 59 | protected function storeTokenInSession(AccessToken $token) 60 | { 61 | $expires = $token->getExpires(); 62 | 63 | $this->session->start(); 64 | $this->session->set($this->clientName . '_token', [ 65 | 'token' => $token->getToken(), 66 | 'type' => $token->getType(), 67 | 'data' => array_merge($token->getData(), ['expires' => $expires->getTimestamp()]), 68 | ]); 69 | $this->session->save(); 70 | } 71 | 72 | /** 73 | * @return null|AccessToken 74 | */ 75 | public function getAccessToken() 76 | { 77 | if ($this->accessToken === null) { 78 | $this->restoreTokenFromSession(); 79 | } 80 | 81 | return parent::getAccessToken(); 82 | } 83 | 84 | protected function restoreTokenFromSession() 85 | { 86 | if ($this->session->has($this->clientName . '_token')) { 87 | $sessionTokenData = $this->session->get($this->clientName . '_token'); 88 | 89 | $this->setAccessToken(new AccessToken( 90 | $sessionTokenData['token'], 91 | $sessionTokenData['type'], 92 | $sessionTokenData['data'] 93 | )); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Sainsburys\Guzzle\Oauth2\Middleware\OAuthMiddleware 8 | Gregurco\Bundle\GuzzleBundleOAuth2Plugin\Middleware\PersistentOAuthMiddleware 9 | Gregurco\Bundle\GuzzleBundleOAuth2Plugin\Middleware\CachedOAuthMiddleware 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/DependencyInjection/GuzzleBundleOAuth2ExtensionTest.php: -------------------------------------------------------------------------------- 1 | load([], $container); 18 | 19 | $this->assertTrue($container->hasParameter('guzzle_bundle_oauth2_plugin.middleware.class')); 20 | $this->assertEquals( 21 | OAuthMiddleware::class, 22 | $container->getParameter('guzzle_bundle_oauth2_plugin.middleware.class') 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/GuzzleBundleOAuth2PluginTest.php: -------------------------------------------------------------------------------- 1 | plugin = new GuzzleBundleOAuth2Plugin(); 32 | } 33 | 34 | public function testSubClassesOfPlugin() : void 35 | { 36 | $this->assertInstanceOf(PluginInterface::class, $this->plugin); 37 | $this->assertInstanceOf(Bundle::class, $this->plugin); 38 | } 39 | 40 | public function testAddConfiguration() : void 41 | { 42 | $arrayNode = new ArrayNodeDefinition('node'); 43 | 44 | $this->plugin->addConfiguration($arrayNode); 45 | 46 | $node = $arrayNode->getNode(); 47 | 48 | $this->assertFalse($node->isRequired()); 49 | $this->assertTrue($node->hasDefaultValue()); 50 | $this->assertSame( 51 | [ 52 | 'enabled' => false, 53 | 'base_uri' => null, 54 | 'username' => null, 55 | 'password' => null, 56 | 'client_id' => null, 57 | 'client_secret' => null, 58 | 'token_url' => null, 59 | 'scope' => null, 60 | 'audience' => null, 61 | 'resource' => null, 62 | 'private_key' => null, 63 | 'auth_location' => 'headers', 64 | 'grant_type' => ClientCredentials::class, 65 | 'persistent' => false, 66 | 'retry_limit' => 5, 67 | ], 68 | $node->getDefaultValue() 69 | ); 70 | } 71 | 72 | public function testGetPluginName() : void 73 | { 74 | $this->assertEquals('oauth2', $this->plugin->getPluginName()); 75 | } 76 | 77 | public function testLoad() : void 78 | { 79 | $container = new ContainerBuilder(); 80 | 81 | $this->plugin->load([], $container); 82 | 83 | $this->assertTrue($container->hasParameter('guzzle_bundle_oauth2_plugin.middleware.class')); 84 | $this->assertEquals( 85 | OAuthMiddleware::class, 86 | $container->getParameter('guzzle_bundle_oauth2_plugin.middleware.class') 87 | ); 88 | } 89 | 90 | public function testLoadForClient() : void 91 | { 92 | $handler = new Definition(); 93 | $container = new ContainerBuilder(); 94 | 95 | $this->plugin->loadForClient( 96 | [ 97 | 'enabled' => true, 98 | 'base_uri' => 'https://example.com', 99 | 'token_url' => '/oauth/token', 100 | 'username' => null, 101 | 'password' => null, 102 | 'client_id' => 'test-client-id', 103 | 'client_secret' => '', 104 | 'scope' => 'administration', 105 | 'audience' => null, 106 | 'resource' => null, 107 | 'private_key' => null, 108 | 'auth_location' => 'headers', 109 | 'grant_type' => ClientCredentials::class, 110 | 'persistent' => false, 111 | 'retry_limit' => 5, 112 | ], 113 | $container, 'api_payment', $handler 114 | ); 115 | 116 | $this->assertTrue($container->hasDefinition('guzzle_bundle_oauth2_plugin.middleware.api_payment')); 117 | $this->assertCount(2, $handler->getMethodCalls()); 118 | 119 | $clientMiddlewareDefinition = $container->getDefinition('guzzle_bundle_oauth2_plugin.middleware.api_payment'); 120 | $this->assertCount(3, $clientMiddlewareDefinition->getArguments()); 121 | } 122 | 123 | public function testLoadForClientWithPrivateKey() : void 124 | { 125 | $handler = new Definition(); 126 | $container = new ContainerBuilder(); 127 | 128 | $this->plugin->loadForClient( 129 | [ 130 | 'enabled' => true, 131 | 'base_uri' => 'https://example.com', 132 | 'token_url' => '/oauth/token', 133 | 'username' => null, 134 | 'password' => null, 135 | 'client_id' => 'test-client-id', 136 | 'client_secret' => '', 137 | 'scope' => 'administration', 138 | 'audience' => null, 139 | 'resource' => null, 140 | 'private_key' => '/path/to/private.key', 141 | 'auth_location' => 'headers', 142 | 'grant_type' => JwtBearer::class, 143 | 'persistent' => false, 144 | 'retry_limit' => 5, 145 | ], 146 | $container, 'api_payment', $handler 147 | ); 148 | 149 | $this->assertTrue($container->hasDefinition('guzzle_bundle_oauth2_plugin.middleware.api_payment')); 150 | $this->assertCount(2, $handler->getMethodCalls()); 151 | 152 | $clientMiddlewareDefinition = $container->getDefinition('guzzle_bundle_oauth2_plugin.middleware.api_payment'); 153 | $this->assertCount(3, $clientMiddlewareDefinition->getArguments()); 154 | 155 | $this->assertTrue($container->hasDefinition('guzzle_bundle_oauth2_plugin.private_key.api_payment')); 156 | $clientMiddlewareDefinition = $container->getDefinition('guzzle_bundle_oauth2_plugin.private_key.api_payment'); 157 | $this->assertCount(1, $clientMiddlewareDefinition->getArguments()); 158 | $this->assertEquals('/path/to/private.key', $clientMiddlewareDefinition->getArgument(0)); 159 | } 160 | 161 | /** 162 | * @dataProvider provideValidConfigurationData 163 | * 164 | * @param array $pluginConfiguration 165 | */ 166 | public function testAddConfigurationWithData(array $pluginConfiguration) : void 167 | { 168 | $config = [ 169 | 'eight_points_guzzle' => [ 170 | 'clients' => [ 171 | 'test_client' => [ 172 | 'plugin' => [ 173 | 'oauth2' => $pluginConfiguration, 174 | ] 175 | ] 176 | ] 177 | ] 178 | ]; 179 | 180 | $processor = new Processor(); 181 | $processedConfig = $processor->processConfiguration(new Configuration('eight_points_guzzle', false, [new GuzzleBundleOAuth2Plugin()]), $config); 182 | 183 | $this->assertIsArray($processedConfig); 184 | } 185 | 186 | /** 187 | * @return array 188 | */ 189 | public function provideValidConfigurationData() : array 190 | { 191 | return [ 192 | 'plugin is disabled' => [[ 193 | 'enabled' => false, 194 | ]], 195 | 'plugin is enabled' => [[ 196 | 'enabled' => true, 197 | 'base_uri' => 'https://example.com', 198 | 'client_id' => 's6BhdRkqt3', 199 | ]], 200 | 'PasswordCredentials in grant_type' => [[ 201 | 'base_uri' => 'https://example.com', 202 | 'client_id' => 's6BhdRkqt3', 203 | 'username' => 'johndoe', 204 | 'password' => 'A3ddj3w', 205 | 'grant_type' => PasswordCredentials::class, 206 | ]], 207 | 'ClientCredentials in grant_type' => [[ 208 | 'base_uri' => 'https://example.com', 209 | 'client_id' => 's6BhdRkqt3', 210 | 'grant_type' => ClientCredentials::class, 211 | ]], 212 | 'RefreshToken in grant_type' => [[ 213 | 'base_uri' => 'https://example.com', 214 | 'client_id' => 's6BhdRkqt3', 215 | 'grant_type' => RefreshToken::class, 216 | ]], 217 | 'JwtBearer in grant_type' => [[ 218 | 'base_uri' => 'https://example.com', 219 | 'client_id' => 's6BhdRkqt3', 220 | 'private_key' => '/path/to/private/key', 221 | 'grant_type' => JwtBearer::class, 222 | ]], 223 | 'headers in auth_location' => [[ 224 | 'base_uri' => 'https://example.com', 225 | 'client_id' => 's6BhdRkqt3', 226 | 'auth_location' => 'headers', 227 | ]], 228 | 'body in auth_location' => [[ 229 | 'base_uri' => 'https://example.com', 230 | 'client_id' => 's6BhdRkqt3', 231 | 'auth_location' => 'body', 232 | ]], 233 | ]; 234 | } 235 | 236 | /** 237 | * @dataProvider provideInvalidConfigurationData 238 | * 239 | * @param array $pluginConfiguration 240 | * @param string $message 241 | */ 242 | public function testAddConfigurationWithInvalidData(array $pluginConfiguration, string $message) : void 243 | { 244 | $this->expectException(InvalidConfigurationException::class); 245 | $this->expectExceptionMessage($message); 246 | 247 | $config = [ 248 | 'eight_points_guzzle' => [ 249 | 'clients' => [ 250 | 'test_client' => [ 251 | 'plugin' => [ 252 | 'oauth2' => $pluginConfiguration, 253 | ] 254 | ] 255 | ] 256 | ] 257 | ]; 258 | 259 | $processor = new Processor(); 260 | $processor->processConfiguration(new Configuration('eight_points_guzzle', false, [new GuzzleBundleOAuth2Plugin()]), $config); 261 | } 262 | 263 | /** 264 | * @return array 265 | */ 266 | public function provideInvalidConfigurationData() : array 267 | { 268 | return [ 269 | 'without base_uri' => [ 270 | 'config' => [ 271 | 'enabled' => true, 272 | 'client_id' => 's6BhdRkqt3', 273 | ], 274 | 'exception message' => 'base_uri is required', 275 | ], 276 | 'without client_id' => [ 277 | 'config' => [ 278 | 'enabled' => true, 279 | 'base_uri' => 'https://example.com', 280 | ], 281 | 'exception message' => 'client_id is required', 282 | ], 283 | 'invalid type in grant_type' => [ 284 | 'config' => [ 285 | 'base_uri' => 'https://example.com', 286 | 'client_id' => 's6BhdRkqt3', 287 | 'grant_type' => true, 288 | ], 289 | 'exception message' => sprintf('Use instance of %s in grant_type', GrantTypeInterface::class), 290 | ], 291 | 'invalid class in grant_type' => [ 292 | 'config' => [ 293 | 'base_uri' => 'https://example.com', 294 | 'client_id' => 's6BhdRkqt3', 295 | 'grant_type' => \stdClass::class, 296 | ], 297 | 'exception message' => sprintf('Use instance of %s in grant_type', GrantTypeInterface::class), 298 | ], 299 | 'invalid auth_location' => [ 300 | 'config' => [ 301 | 'base_uri' => 'https://example.com', 302 | 'client_id' => 's6BhdRkqt3', 303 | 'auth_location' => 'somewhere', 304 | ], 305 | 'exception message' => 'Invalid auth_location "somewhere". Allowed values: headers, body.', 306 | ], 307 | 'PasswordCredentials grant type without username' => [ 308 | 'config' => [ 309 | 'base_uri' => 'https://example.com', 310 | 'client_id' => 's6BhdRkqt3', 311 | 'password' => 'A3ddj3w', 312 | 'grant_type' => PasswordCredentials::class, 313 | ], 314 | 'exception message' => 'username and password are required', 315 | ], 316 | 'PasswordCredentials grant type without password' => [ 317 | 'config' => [ 318 | 'base_uri' => 'https://example.com', 319 | 'client_id' => 's6BhdRkqt3', 320 | 'username' => 'johndoe', 321 | 'grant_type' => PasswordCredentials::class, 322 | ], 323 | 'exception message' => 'username and password are required', 324 | ], 325 | 'JwtBearer grant type without private_key' => [ 326 | 'config' => [ 327 | 'base_uri' => 'https://example.com', 328 | 'client_id' => 's6BhdRkqt3', 329 | 'grant_type' => JwtBearer::class, 330 | ], 331 | 'exception message' => 'private_key is required', 332 | ], 333 | ]; 334 | } 335 | } 336 | --------------------------------------------------------------------------------