├── .gitignore ├── .php-cs-fixer.php ├── .scrutinizer.yml ├── .travis.yml ├── CODEOWNERS ├── LICENSE ├── README.md ├── build └── .gitignore ├── composer.json ├── docker-compose.yml ├── phpunit.xml ├── src ├── AccessTokenCacheHandler.php ├── AddAuthorizationHeader.php ├── ClientBuilder.php └── RetryOnAuthorizationError.php └── tests ├── AccessTokenCacheHandlerTest.php ├── AddAuthorizationHeaderTest.php ├── ClientBuilderTest.php └── RetryOnAuthorizationErrorTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .php_cs.cache 4 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__.'/src') 8 | ->in(__DIR__.'/tests'); 9 | 10 | $config = new Config(); 11 | return $config->setRules([ 12 | '@PSR2' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'concat_space' => ['spacing' => 'one'], 15 | 'new_with_braces' => true, 16 | 'no_blank_lines_after_phpdoc' => true, 17 | 'no_empty_phpdoc' => true, 18 | 'no_empty_comment' => true, 19 | 'no_leading_import_slash' => true, 20 | 'no_trailing_comma_in_singleline_array' => true, 21 | 'no_unused_imports' => true, 22 | 'ordered_imports' => ['imports_order' => null, 'sort_algorithm' => 'alpha'], 23 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true], 24 | 'phpdoc_align' => true, 25 | 'phpdoc_no_empty_return' => true, 26 | 'phpdoc_order' => true, 27 | 'phpdoc_scalar' => true, 28 | 'phpdoc_separation' => true, 29 | 'phpdoc_to_comment' => true, 30 | 'phpdoc_types' => true, 31 | 'psr_autoloading' => true, 32 | 'return_type_declaration' => ['space_before' => 'none'], 33 | 'single_blank_line_before_namespace' => true, 34 | 'single_quote' => true, 35 | 'space_after_semicolon' => true, 36 | 'ternary_operator_spaces' => true, 37 | 'trailing_comma_in_multiline' => true, 38 | 'trim_array_spaces' => true, 39 | 'whitespace_after_comma_in_array' => true, 40 | ]) 41 | ->setFinder($finder); 42 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: [src/*] 3 | excluded_paths: [tests/*] 4 | checks: 5 | php: 6 | code_rating: true 7 | tools: 8 | external_code_coverage: 9 | timeout: 600 10 | runs: 2 11 | php_code_coverage: false 12 | php_loc: 13 | enabled: true 14 | excluded_dirs: [tests, vendor] 15 | php_cpd: 16 | enabled: true 17 | excluded_dirs: [tests, vendor] 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | matrix: 6 | include: 7 | - php: 7.1 8 | env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false 9 | - php: 7.2 10 | env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false 11 | - php: 7.3 12 | env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=true 13 | - php: master 14 | env: COLLECT_COVERAGE=true VALIDATE_CODING_STYLE=false 15 | allow_failures: 16 | - php: master 17 | fast_finish: true 18 | 19 | cache: 20 | directories: 21 | - $HOME/.composer/cache 22 | 23 | before_install: 24 | - travis_retry composer self-update 25 | 26 | install: 27 | - travis_retry composer update --no-interaction --prefer-source 28 | 29 | script: 30 | - export XDEBUG_MODE=coverage 31 | - composer phpunit 32 | 33 | after_script: 34 | - if [ "$COLLECT_COVERAGE" == "true" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover build/clover.xml; fi 35 | - if [ "$VALIDATE_CODING_STYLE" == "true" ]; then composer phpcs; fi 36 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @joskfg @rccrdpccl @akira28 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Softonic International S.A. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Guzzle OAuth2 Middleware 2 | ===== 3 | 4 | [![Latest Version](https://img.shields.io/github/release/softonic/guzzle-oauth2-middleware.svg?style=flat-square)](https://github.com/softonic/guzzle-oauth2-middleware/releases) 5 | [![Software License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE.md) 6 | [![Build Status](https://img.shields.io/travis/softonic/guzzle-oauth2-middleware/master.svg?style=flat-square)](https://travis-ci.org/softonic/guzzle-oauth2-middleware) 7 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/softonic/guzzle-oauth2-middleware.svg?style=flat-square)](https://scrutinizer-ci.com/g/softonic/guzzle-oauth2-middleware/code-structure) 8 | [![Quality Score](https://img.shields.io/scrutinizer/g/softonic/guzzle-oauth2-middleware.svg?style=flat-square)](https://scrutinizer-ci.com/g/softonic/guzzle-oauth2-middleware) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/softonic/guzzle-oauth2-middleware.svg?style=flat-square)](https://packagist.org/packages/softonic/guzzle-oauth2-middleware) 10 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/softonic/guzzle-oauth2-middleware.svg?style=flat-square)](http://isitmaintained.com/project/softonic/guzzle-oauth2-middleware "Average time to resolve an issue") 11 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/softonic/guzzle-oauth2-middleware.svg?style=flat-square)](http://isitmaintained.com/project/softonic/guzzle-oauth2-middleware "Percentage of issues still open") 12 | 13 | This package provides middleware for [guzzle](https://github.com/guzzle/guzzle/) for handling OAuth2 token negotiation and renewal on expiry transparently. It accecpts PHP League's [OAuth 2.0 Clients](https://github.com/thephpleague/oauth2-client). 14 | 15 | Installation 16 | ------- 17 | 18 | To install, use composer: 19 | 20 | ``` 21 | composer require softonic/guzzle-oauth2-middleware 22 | ``` 23 | 24 | Usage 25 | ------- 26 | 27 | ``` php 28 | 'myclient', 31 | 'clientSecret' => 'mysecret' 32 | ]; 33 | 34 | // Any provider extending League\OAuth2\Client\Provider\AbstractProvider will do 35 | $provider = new Softonic\OAuth2\Client\Provider\Softonic($options); 36 | 37 | // Send OAuth2 parameters and use token_options for any other parameters your OAuth2 provider needs 38 | $config = ['grant_type' => 'client_credentials', 'scope' => 'myscope', 'token_options' => ['audience' => 'test_audience']]; 39 | 40 | // Any implementation of PSR-6 Cache will do 41 | $cache = new \Symfony\Component\Cache\Adapter\FilesystemAdapter(); 42 | 43 | $client = \Softonic\OAuth2\Guzzle\Middleware\ClientBuilder::build( 44 | $provider, 45 | $config, 46 | $cache, 47 | ['base_uri' => 'https://foo.bar/'] 48 | ); 49 | $response = $client->request('POST', 'qux); 50 | 51 | 52 | ``` 53 | 54 | 55 | Testing 56 | ------- 57 | 58 | `softonic/guzzle-oauth2-middleware` has a [PHPUnit](https://phpunit.de) test suite and a coding style compliance test suite using [PHP CS Fixer](http://cs.sensiolabs.org/). 59 | 60 | To run the tests, run the following command from the project folder. 61 | 62 | ``` bash 63 | $ docker-compose run test 64 | ``` 65 | 66 | To run interactively using [PsySH](http://psysh.org/): 67 | ``` bash 68 | $ docker-compose run psysh 69 | ``` 70 | 71 | License 72 | ------- 73 | 74 | The Apache 2.0 license. Please see [LICENSE](LICENSE) for more information. 75 | 76 | [PSR-2]: http://www.php-fig.org/psr/psr-2/ 77 | [PSR-4]: http://www.php-fig.org/psr/psr-4/ 78 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softonic/guzzle-oauth2-middleware", 3 | "type": "library", 4 | "description" : "Guzzle middleware with OAuth2 integration", 5 | "keywords": ["guzzle", "oauth2", "middleware"], 6 | "license": "Apache-2.0", 7 | "homepage": "https://github.com/softonic/guzzle-oauth2-middleware", 8 | "support": { 9 | "issues": "https://github.com/softonic/guzzle-oauth2-middleware/issues" 10 | }, 11 | "require": { 12 | "php": ">=7.1", 13 | "league/oauth2-client": "^2.2", 14 | "psr/cache": "^1.0|^2.0|^3.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^6.0", 18 | "friendsofphp/php-cs-fixer": "^2.4" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Softonic\\OAuth2\\Guzzle\\Middleware\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Softonic\\OAuth2\\Guzzle\\Middleware\\Test\\": "tests/" 28 | } 29 | }, 30 | "scripts": { 31 | "test": "phpunit --coverage-text; php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", 32 | "phpunit": "phpunit --coverage-text", 33 | "phpcs": "php-cs-fixer fix -v --diff --dry-run --allow-risky=yes;", 34 | "fix-cs": "php-cs-fixer fix -v --diff --allow-risky=yes;" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | test: 5 | volumes: 6 | - ./:/app 7 | image: ricc/composer-prestissimo:latest 8 | command: composer run test 9 | 10 | fixcs: 11 | volumes: 12 | - ./:/app 13 | image: ricc/composer-prestissimo:latest 14 | command: composer run fix-cs 15 | 16 | psysh: 17 | volumes: 18 | - ./:/app 19 | image: ricc/psysh:latest 20 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | tests 17 | 18 | 19 | 20 | 21 | 22 | src 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/AccessTokenCacheHandler.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 19 | } 20 | 21 | /** 22 | * @param OAuth2Provider $provider 23 | * @param array $options 24 | * 25 | * @throws InvalidArgumentException 26 | * 27 | * @return string|bool False if no token can be found in cache, the token's value otherwise. 28 | */ 29 | public function getTokenByProvider(OAuth2Provider $provider, array $options) 30 | { 31 | $cacheKey = $this->getCacheKey($provider, $options); 32 | $cacheItem = $this->cache->getItem($cacheKey); 33 | if ($cacheItem->isHit()) { 34 | return $cacheItem->get(); 35 | } 36 | return false; 37 | } 38 | 39 | public function saveTokenByProvider(AccessToken $accessToken, OAuth2Provider $provider, array $options): bool 40 | { 41 | $cacheKey = $this->getCacheKey($provider, $options); 42 | $cacheItem = $this->cache->getItem($cacheKey); 43 | $cacheItem->set( 44 | $accessToken->getToken() 45 | ); 46 | $expiration = new \DateTime(); 47 | $expiration->setTimestamp($accessToken->getExpires()); 48 | $cacheItem->expiresAt( 49 | $expiration 50 | ); 51 | return $this->cache->save($cacheItem); 52 | } 53 | 54 | public function deleteItemByProvider(OAuth2Provider $provider, array $options): bool 55 | { 56 | return $this->cache->deleteItem($this->getCacheKey($provider, $options)); 57 | } 58 | 59 | public function getCacheKey(OAuth2Provider $provider, array $options): string 60 | { 61 | parse_str(parse_url($provider->getAuthorizationUrl(), PHP_URL_QUERY), $query); 62 | return static::CACHE_KEY_PREFIX 63 | . md5($provider->getBaseAuthorizationUrl() . ($query['client_id'] ?? '') . serialize($options)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/AddAuthorizationHeader.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 21 | $this->config = $config; 22 | $this->cacheHandler = $cacheHandler; 23 | } 24 | 25 | public function __invoke(RequestInterface $request): RequestInterface 26 | { 27 | $token = $this->cacheHandler->getTokenByProvider($this->provider, $this->config); 28 | if (false === $token) { 29 | $accessToken = $this->getAccessToken(); 30 | $token = $accessToken->getToken(); 31 | $this->cacheHandler->saveTokenByProvider($accessToken, $this->provider, $this->config); 32 | } 33 | 34 | foreach ($this->provider->getHeaders($token) as $name => $value) { 35 | $request = $request->withHeader($name, $value); 36 | } 37 | 38 | return $request; 39 | } 40 | 41 | private function getAccessToken(): AccessToken 42 | { 43 | $options = $this->getOptions(); 44 | $grantType = $this->getGrantType(); 45 | return $this->provider->getAccessToken($grantType, $options); 46 | } 47 | 48 | private function getGrantType(): string 49 | { 50 | if (empty($this->config['grant_type'])) { 51 | throw new InvalidArgumentException('Config value `grant_type` needs to be specified.'); 52 | } 53 | return $this->config['grant_type']; 54 | } 55 | 56 | private function getOptions(): array 57 | { 58 | $options = []; 59 | if (!empty($this->config['scope'])) { 60 | $options['scope'] = $this->config['scope']; 61 | } 62 | 63 | if (!empty($this->config['token_options'])) { 64 | $tokenOptions = $this->config['token_options']; 65 | foreach ($tokenOptions as $key => $value) { 66 | $options[$key] = $value; 67 | } 68 | } 69 | 70 | return $options; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ClientBuilder.php: -------------------------------------------------------------------------------- 1 | setHandler(new CurlHandler()); 25 | 26 | $stack = static::addHeaderMiddlewareToStack( 27 | $stack, 28 | $oauthProvider, 29 | $tokenOptions, 30 | $cacheHandler 31 | ); 32 | $stack = static::addRetryMiddlewareToStack( 33 | $stack, 34 | $oauthProvider, 35 | $tokenOptions, 36 | $cacheHandler 37 | ); 38 | 39 | $defaultOptions = [ 40 | 'handler' => $stack, 41 | ]; 42 | $guzzleOptions = static::mergeOptions($defaultOptions, $guzzleOptions); 43 | 44 | return new Client($guzzleOptions); 45 | } 46 | 47 | protected static function addHeaderMiddlewareToStack( 48 | HandlerStack $stack, 49 | OAuth2Provider $oauthProvider, 50 | array $tokenOptions, 51 | AccessTokenCacheHandler $cacheHandler 52 | ): HandlerStack { 53 | $addAuthorizationHeader = new AddAuthorizationHeader( 54 | $oauthProvider, 55 | $tokenOptions, 56 | $cacheHandler 57 | ); 58 | 59 | $stack->push(Middleware::mapRequest($addAuthorizationHeader)); 60 | return $stack; 61 | } 62 | 63 | protected static function addRetryMiddlewareToStack( 64 | HandlerStack $stack, 65 | OAuth2Provider $oauthProvider, 66 | array $tokenOptions, 67 | AccessTokenCacheHandler $cacheHandler 68 | ): HandlerStack { 69 | $retryOnAuthorizationError = new RetryOnAuthorizationError( 70 | $oauthProvider, 71 | $tokenOptions, 72 | $cacheHandler 73 | ); 74 | 75 | $stack->push(Middleware::retry($retryOnAuthorizationError)); 76 | return $stack; 77 | } 78 | 79 | protected static function mergeOptions(array $defaultOptions, ?array $options = null): array 80 | { 81 | $options = $options ?? []; 82 | return array_merge($options, $defaultOptions); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/RetryOnAuthorizationError.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 18 | $this->config = $config; 19 | $this->cacheHandler = $cacheHandler; 20 | } 21 | 22 | public function __invoke( 23 | int $retries, 24 | RequestInterface $request, 25 | ?ResponseInterface $response = null, 26 | ?\Exception $exception = null 27 | ): bool { 28 | if ($this->isUnauthorizedResponse($retries, $response)) { 29 | $this->cacheHandler->deleteItemByProvider($this->provider, $this->config); 30 | return true; 31 | } 32 | return false; 33 | } 34 | 35 | private function isUnauthorizedResponse(int $retries, ?ResponseInterface $response = null) 36 | { 37 | return !empty($response) && $retries < 1 && $response->getStatusCode() === 401; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/AccessTokenCacheHandlerTest.php: -------------------------------------------------------------------------------- 1 | createMock(CacheItemPoolInterface::class); 18 | 19 | $options = []; 20 | $providerA = $this->createMock(AbstractProvider::class); 21 | $providerA->expects($this->any()) 22 | ->method('getAuthorizationUrl') 23 | ->willReturn('http://example.com?client_id=a'); 24 | 25 | $providerB = $this->createMock(AbstractProvider::class); 26 | $providerB->expects($this->any()) 27 | ->method('getAccessToken') 28 | ->willReturn('http://example.com?client_id=b'); 29 | 30 | $cacheHandler = new AccessTokenCacheHandler($mockCache); 31 | $this->assertNotEquals( 32 | $cacheHandler->getCacheKey($providerA, $options), 33 | $cacheHandler->getCacheKey($providerB, $options) 34 | ); 35 | } 36 | 37 | public function testGetCacheKeyIsEqualForSameProvider() 38 | { 39 | $mockCache = $this->createMock(CacheItemPoolInterface::class); 40 | 41 | $options = []; 42 | $providerA = $this->createMock(AbstractProvider::class); 43 | $providerB = $this->createMock(AbstractProvider::class); 44 | 45 | $cacheHandler = new AccessTokenCacheHandler($mockCache); 46 | $this->assertEquals( 47 | $cacheHandler->getCacheKey($providerA, $options), 48 | $cacheHandler->getCacheKey($providerB, $options) 49 | ); 50 | } 51 | 52 | public function testGetCacheKeyIsDifferentBetweenSameProviderButDifferentOptions() 53 | { 54 | $mockCache = $this->createMock(CacheItemPoolInterface::class); 55 | 56 | $optionsA = [ 57 | 'grant_type' => 'client_credentials', 58 | 'scope' => 'myscopeA', 59 | ]; 60 | $optionsB = [ 61 | 'grant_type' => 'client_credentials', 62 | 'scope' => 'myscopeB', 63 | ]; 64 | 65 | $provider = $this->createMock(AbstractProvider::class); 66 | 67 | $cacheHandler = new AccessTokenCacheHandler($mockCache); 68 | $this->assertNotEquals( 69 | $cacheHandler->getCacheKey($provider, $optionsA), 70 | $cacheHandler->getCacheKey($provider, $optionsB) 71 | ); 72 | } 73 | 74 | public function testGetTokenByProviderWhenNotSet() 75 | { 76 | $mockProvider = $this->createMock(AbstractProvider::class); 77 | $mockCache = $this->createMock(CacheItemPoolInterface::class); 78 | $mockCacheItem = $this->createMock(CacheItemInterface::class); 79 | 80 | $mockCache->expects($this->once()) 81 | ->method('getItem') 82 | ->with($this->matchCacheKey()) 83 | ->willReturn($mockCacheItem); 84 | 85 | $mockCacheItem->expects($this->once()) 86 | ->method('isHit') 87 | ->willReturn(false); 88 | 89 | $cacheHandler = new AccessTokenCacheHandler($mockCache); 90 | $this->assertFalse($cacheHandler->getTokenByprovider($mockProvider, [])); 91 | } 92 | 93 | public function testGetTokenByProviderWhenSet() 94 | { 95 | $mockProvider = $this->createMock(AbstractProvider::class); 96 | $mockCache = $this->createMock(CacheItemPoolInterface::class); 97 | $mockCacheItem = $this->createMock(CacheItemInterface::class); 98 | 99 | $mockCache->expects($this->once()) 100 | ->method('getItem') 101 | ->with($this->matchCacheKey()) 102 | ->willReturn($mockCacheItem); 103 | 104 | $mockCacheItem->expects($this->once()) 105 | ->method('isHit') 106 | ->willReturn(true); 107 | 108 | $mockCacheItem->expects($this->once()) 109 | ->method('get') 110 | ->willReturn('mytoken'); 111 | 112 | $cacheHandler = new AccessTokenCacheHandler($mockCache); 113 | $this->assertSame('mytoken', $cacheHandler->getTokenByprovider($mockProvider, [])); 114 | } 115 | 116 | public function testSaveTokenByProvider() 117 | { 118 | $mockProvider = $this->createMock(AbstractProvider::class); 119 | $mockCache = $this->createMock(CacheItemPoolInterface::class); 120 | $mockCacheItem = $this->createMock(CacheItemInterface::class); 121 | $mockAccessToken = $this->createMock(AccessToken::class); 122 | 123 | $expiryTimestamp = 1498146237; 124 | $mockAccessToken->expects($this->once()) 125 | ->method('getToken') 126 | ->willReturn('mytoken'); 127 | $mockAccessToken->expects($this->once()) 128 | ->method('getExpires') 129 | ->willReturn($expiryTimestamp); 130 | 131 | $mockCache->expects($this->once()) 132 | ->method('getItem') 133 | ->with($this->matchCacheKey()) 134 | ->willReturn($mockCacheItem); 135 | 136 | $mockCache->expects($this->once()) 137 | ->method('save') 138 | ->with($mockCacheItem) 139 | ->willReturn(true); 140 | 141 | $mockCacheItem->expects($this->once()) 142 | ->method('set') 143 | ->with('mytoken'); 144 | 145 | $mockCacheItem->expects($this->once()) 146 | ->method('expiresAt') 147 | ->with( 148 | $this->isInstanceOf(DateTime::class) 149 | ); 150 | 151 | $cacheHandler = new AccessTokenCacheHandler($mockCache); 152 | $this->assertTrue($cacheHandler->saveTokenByProvider($mockAccessToken, $mockProvider, [])); 153 | } 154 | 155 | public function testDeleteItemByProvider() 156 | { 157 | $mockProvider = $this->createMock(AbstractProvider::class); 158 | $mockCache = $this->createMock(CacheItemPoolInterface::class); 159 | 160 | $mockCache->expects($this->once()) 161 | ->method('deleteItem') 162 | ->with($this->matchCacheKey()) 163 | ->willReturn(true); 164 | 165 | $cacheHandler = new AccessTokenCacheHandler($mockCache); 166 | $this->assertTrue($cacheHandler->deleteItemByprovider($mockProvider, [])); 167 | } 168 | 169 | private function matchCacheKey() 170 | { 171 | return $this->matchesRegularExpression('/^oauth2_token_[a-f0-9]{32}$/'); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/AddAuthorizationHeaderTest.php: -------------------------------------------------------------------------------- 1 | mockOauth2Provider = $this->createMock(\League\OAuth2\Client\Provider\AbstractProvider::class); 16 | $this->mockAccessToken = $this->createMock(\League\OAuth2\Client\Token\AccessToken::class); 17 | } 18 | 19 | public function testMiddlewareWhenGrantTypeNotSpecified() 20 | { 21 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 22 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 23 | 24 | $mockCacheHandler->expects($this->once()) 25 | ->method('getTokenByProvider') 26 | ->with($this->mockOauth2Provider, []) 27 | ->willReturn(false); 28 | 29 | $addAuthorizationHeader = new AddAuthorizationHeader( 30 | $this->mockOauth2Provider, 31 | [], 32 | $mockCacheHandler 33 | ); 34 | 35 | $this->expectException(\InvalidArgumentException::class); 36 | $this->expectExceptionMessage('Config value `grant_type` needs to be specified.'); 37 | 38 | 39 | $addAuthorizationHeader($mockRequest); 40 | } 41 | 42 | public function testMiddlewareWhenProviderThrowsIdentityProviderException() 43 | { 44 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 45 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 46 | 47 | $mockCacheHandler->expects($this->once()) 48 | ->method('getTokenByProvider') 49 | ->with($this->mockOauth2Provider) 50 | ->willReturn(false); 51 | 52 | $this->mockOauth2Provider->expects($this->once()) 53 | ->method('getAccessToken') 54 | ->with( 55 | 'client_credentials', 56 | ['scope' => 'myscope'] 57 | ) 58 | ->willThrowException(new \League\OAuth2\Client\Provider\Exception\IdentityProviderException('custom message', 500, [])); 59 | 60 | $config = [ 61 | 'grant_type' => 'client_credentials', 62 | 'scope' => 'myscope', 63 | ]; 64 | $this->expectException(\League\OAuth2\Client\Provider\Exception\IdentityProviderException::class); 65 | $this->expectExceptionMessage('custom message'); 66 | 67 | $addAuthorizationHeader = new AddAuthorizationHeader( 68 | $this->mockOauth2Provider, 69 | $config, 70 | $mockCacheHandler 71 | ); 72 | $addAuthorizationHeader($mockRequest); 73 | } 74 | 75 | public function testMiddlewareAddingAuthorizationHeader() 76 | { 77 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 78 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 79 | 80 | $config = [ 81 | 'grant_type' => 'client_credentials', 82 | 'scope' => 'myscope', 83 | ]; 84 | 85 | $this->mockAccessToken->expects($this->once()) 86 | ->method('getToken') 87 | ->willReturn('mytoken'); 88 | 89 | $mockCacheHandler->expects($this->once()) 90 | ->method('getTokenByProvider') 91 | ->with($this->mockOauth2Provider) 92 | ->willReturn(false); 93 | $mockCacheHandler->expects($this->once()) 94 | ->method('saveTokenByProvider') 95 | ->with( 96 | $this->mockAccessToken, 97 | $this->mockOauth2Provider, 98 | $config 99 | ); 100 | 101 | $this->mockOauth2Provider->expects($this->once()) 102 | ->method('getAccessToken') 103 | ->with( 104 | 'client_credentials', 105 | ['scope' => 'myscope'] 106 | ) 107 | ->willReturn($this->mockAccessToken); 108 | 109 | $this->mockOauth2Provider->expects($this->once()) 110 | ->method('getHeaders') 111 | ->with('mytoken') 112 | ->willReturn(['Authorization' => 'Bearer mytoken']); 113 | 114 | $mockRequest->expects($this->once()) 115 | ->method('withHeader') 116 | ->with('Authorization', 'Bearer mytoken') 117 | ->willReturnSelf(); 118 | 119 | $addAuthorizationHeader = new AddAuthorizationHeader( 120 | $this->mockOauth2Provider, 121 | $config, 122 | $mockCacheHandler 123 | ); 124 | $request = $addAuthorizationHeader($mockRequest); 125 | 126 | $this->assertSame($mockRequest, $request); 127 | } 128 | 129 | public function testMiddlewareAddingAuthorizationHeaderWithoutScope() 130 | { 131 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 132 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 133 | $config = [ 134 | 'grant_type' => 'client_credentials', 135 | ]; 136 | 137 | $this->mockAccessToken->expects($this->once()) 138 | ->method('getToken') 139 | ->willReturn('mytoken'); 140 | 141 | $this->mockOauth2Provider->expects($this->once()) 142 | ->method('getHeaders') 143 | ->with('mytoken') 144 | ->willReturn(['Authorization' => 'Bearer mytoken']); 145 | 146 | $mockCacheHandler->expects($this->once()) 147 | ->method('getTokenByProvider') 148 | ->with( 149 | $this->mockOauth2Provider, 150 | $config 151 | ) 152 | ->willReturn(false); 153 | $mockCacheHandler->expects($this->once()) 154 | ->method('saveTokenByProvider') 155 | ->with( 156 | $this->mockAccessToken, 157 | $this->mockOauth2Provider, 158 | $config 159 | ); 160 | 161 | $mockRequest->expects($this->once()) 162 | ->method('withHeader') 163 | ->with('Authorization', 'Bearer mytoken') 164 | ->willReturnSelf(); 165 | 166 | $this->mockOauth2Provider->expects($this->once()) 167 | ->method('getAccessToken') 168 | ->with( 169 | 'client_credentials', 170 | [] 171 | ) 172 | ->willReturn($this->mockAccessToken); 173 | 174 | $addAuthorizationHeader = new AddAuthorizationHeader( 175 | $this->mockOauth2Provider, 176 | $config, 177 | $mockCacheHandler 178 | ); 179 | $request = $addAuthorizationHeader($mockRequest); 180 | 181 | $this->assertSame($mockRequest, $request); 182 | } 183 | 184 | public function testMiddlewareAddingAuthorizationHeaderWithTokenOptions() 185 | { 186 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 187 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 188 | 189 | $config = [ 190 | 'grant_type' => 'client_credentials', 191 | 'scope' => 'myscope', 192 | 'token_options' => ['audience' => 'test_audience'], 193 | ]; 194 | 195 | $this->mockAccessToken->expects($this->once()) 196 | ->method('getToken') 197 | ->willReturn('mytoken'); 198 | 199 | $this->mockOauth2Provider->expects($this->once()) 200 | ->method('getHeaders') 201 | ->with('mytoken') 202 | ->willReturn(['Authorization' => 'Bearer mytoken']); 203 | 204 | $mockCacheHandler->expects($this->once()) 205 | ->method('getTokenByProvider') 206 | ->with($this->mockOauth2Provider) 207 | ->willReturn(false); 208 | $mockCacheHandler->expects($this->once()) 209 | ->method('saveTokenByProvider') 210 | ->with( 211 | $this->mockAccessToken, 212 | $this->mockOauth2Provider, 213 | $config 214 | ); 215 | 216 | $this->mockOauth2Provider->expects($this->once()) 217 | ->method('getAccessToken') 218 | ->with( 219 | 'client_credentials', 220 | ['scope' => 'myscope', 'audience' => 'test_audience'] 221 | ) 222 | ->willReturn($this->mockAccessToken); 223 | 224 | $mockRequest->expects($this->once()) 225 | ->method('withHeader') 226 | ->with('Authorization', 'Bearer mytoken') 227 | ->willReturnSelf(); 228 | 229 | $addAuthorizationHeader = new AddAuthorizationHeader( 230 | $this->mockOauth2Provider, 231 | $config, 232 | $mockCacheHandler 233 | ); 234 | $request = $addAuthorizationHeader($mockRequest); 235 | 236 | $this->assertSame($mockRequest, $request); 237 | } 238 | 239 | public function testMiddlewareAddingAuthorizationHeaderWithTokenOptionsDeclaringScope() 240 | { 241 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 242 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 243 | 244 | $config = [ 245 | 'grant_type' => 'client_credentials', 246 | 'scope' => 'ignoredscope', 247 | 'token_options' => ['audience' => 'test_audience', 'scope' => 'myscope'], 248 | ]; 249 | 250 | $this->mockAccessToken->expects($this->once()) 251 | ->method('getToken') 252 | ->willReturn('mytoken'); 253 | 254 | $this->mockOauth2Provider->expects($this->once()) 255 | ->method('getHeaders') 256 | ->with('mytoken') 257 | ->willReturn(['Authorization' => 'Bearer mytoken']); 258 | 259 | $mockCacheHandler->expects($this->once()) 260 | ->method('getTokenByProvider') 261 | ->with($this->mockOauth2Provider) 262 | ->willReturn(false); 263 | $mockCacheHandler->expects($this->once()) 264 | ->method('saveTokenByProvider') 265 | ->with( 266 | $this->mockAccessToken, 267 | $this->mockOauth2Provider, 268 | $config 269 | ); 270 | 271 | $this->mockOauth2Provider->expects($this->once()) 272 | ->method('getAccessToken') 273 | ->with( 274 | 'client_credentials', 275 | ['scope' => 'myscope', 'audience' => 'test_audience'] 276 | ) 277 | ->willReturn($this->mockAccessToken); 278 | 279 | $mockRequest->expects($this->once()) 280 | ->method('withHeader') 281 | ->with('Authorization', 'Bearer mytoken') 282 | ->willReturnSelf(); 283 | 284 | $addAuthorizationHeader = new AddAuthorizationHeader( 285 | $this->mockOauth2Provider, 286 | $config, 287 | $mockCacheHandler 288 | ); 289 | $request = $addAuthorizationHeader($mockRequest); 290 | 291 | $this->assertSame($mockRequest, $request); 292 | } 293 | 294 | public function testMiddlewareNotNegotiatingTokenWhenIsCached() 295 | { 296 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 297 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 298 | 299 | $config = [ 300 | 'grant_type' => 'client_credentials', 301 | ]; 302 | 303 | $mockCacheHandler->expects($this->once()) 304 | ->method('getTokenByProvider') 305 | ->with( 306 | $this->mockOauth2Provider, 307 | $config 308 | ) 309 | ->willReturn('mytoken'); 310 | $mockCacheHandler->expects($this->never()) 311 | ->method('saveTokenByProvider'); 312 | 313 | $mockRequest->expects($this->once()) 314 | ->method('withHeader') 315 | ->with('Authorization', 'Bearer mytoken') 316 | ->willReturnSelf(); 317 | 318 | $this->mockOauth2Provider->expects($this->once()) 319 | ->method('getHeaders') 320 | ->with('mytoken') 321 | ->willReturn(['Authorization' => 'Bearer mytoken']); 322 | 323 | $this->mockOauth2Provider->expects($this->never()) 324 | ->method('getAccessToken'); 325 | 326 | $addAuthorizationHeader = new AddAuthorizationHeader( 327 | $this->mockOauth2Provider, 328 | $config, 329 | $mockCacheHandler 330 | ); 331 | $request = $addAuthorizationHeader($mockRequest); 332 | 333 | $this->assertSame($mockRequest, $request); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /tests/ClientBuilderTest.php: -------------------------------------------------------------------------------- 1 | createMock(\Psr\Cache\CacheItemPoolInterface::class); 13 | $mockProvider = $this->createMock(\League\OAuth2\Client\Provider\AbstractProvider::class); 14 | $mockTokenOptions = []; 15 | 16 | $client = ClientBuilder::build($mockProvider, $mockTokenOptions, $mockCache); 17 | $this->assertInstanceOf(\GuzzleHttp\ClientInterface::class, $client); 18 | } 19 | 20 | public function testClientBuilderWithGuzzleOptions() 21 | { 22 | $mockCache = $this->createMock(\Psr\Cache\CacheItemPoolInterface::class); 23 | $mockProvider = $this->createMock(\League\OAuth2\Client\Provider\AbstractProvider::class); 24 | $mockTokenOptions = []; 25 | $baseUri = 'https://foo.bar/'; 26 | $mockGuzzleOptions = [ 27 | 'base_uri' => $baseUri, 28 | ]; 29 | $client = ClientBuilder::build( 30 | $mockProvider, 31 | $mockTokenOptions, 32 | $mockCache, 33 | $mockGuzzleOptions 34 | ); 35 | $this->assertInstanceOf(\GuzzleHttp\ClientInterface::class, $client); 36 | 37 | $guzzleConfig = $client->getConfig(); 38 | $this->assertEquals($baseUri, (string) $guzzleConfig['base_uri']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/RetryOnAuthorizationErrorTest.php: -------------------------------------------------------------------------------- 1 | createMock(\Psr\Http\Message\ResponseInterface::class); 13 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 14 | $mockProvider = $this->createMock(\League\OAuth2\Client\Provider\AbstractProvider::class); 15 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 16 | 17 | $mockResponse->expects($this->exactly(1)) 18 | ->method('getStatusCode') 19 | ->willReturn(200); 20 | 21 | $decider = new RetryOnAuthorizationError($mockProvider, [], $mockCacheHandler); 22 | $this->assertFalse($decider(0, $mockRequest, $mockResponse)); 23 | } 24 | 25 | public function testRetryOnceOn401Response() 26 | { 27 | $mockResponse = $this->createMock(\Psr\Http\Message\ResponseInterface::class); 28 | $mockRequest = $this->createMock(\Psr\Http\Message\RequestInterface::class); 29 | $mockProvider = $this->createMock(\League\OAuth2\Client\Provider\AbstractProvider::class); 30 | $mockCacheHandler = $this->createMock(\Softonic\OAuth2\Guzzle\Middleware\AccessTokenCacheHandler::class); 31 | 32 | $mockResponse->expects($this->exactly(1)) 33 | ->method('getStatusCode') 34 | ->willReturn(401); 35 | 36 | $mockCacheHandler->expects($this->once()) 37 | ->method('deleteItemByProvider') 38 | ->with($mockProvider, []); 39 | 40 | $decider = new RetryOnAuthorizationError($mockProvider, [], $mockCacheHandler); 41 | $this->assertTrue($decider(0, $mockRequest, $mockResponse)); 42 | $this->assertFalse($decider(1, $mockRequest, $mockResponse)); 43 | } 44 | 45 | private function matchCacheKey() 46 | { 47 | return $this->matchesRegularExpression('/^oauth2-token-[a-f0-9]{32}$/'); 48 | } 49 | } 50 | --------------------------------------------------------------------------------