├── .coveralls.yml ├── .gitignore ├── .php_cs ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── JwtMiddleware.php ├── JwtToken.php ├── Manager │ └── JwtManager.php ├── Persistence │ ├── NullTokenPersistence.php │ ├── SimpleCacheTokenPersistence.php │ └── TokenPersistenceInterface.php └── Strategy │ └── Auth │ ├── AbstractBaseAuthStrategy.php │ ├── AuthStrategyInterface.php │ ├── FormAuthStrategy.php │ ├── HttpBasicAuthStrategy.php │ ├── JsonAuthStrategy.php │ └── QueryAuthStrategy.php └── tests ├── JwtMiddlewareTest.php ├── JwtTokenTest.php ├── Manager └── JwtManagerTest.php ├── Persistence └── TokenPersistenceTest.php └── Strategy └── Auth └── AuthStrategyTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | vendor/ 4 | bin/* 5 | .php_cs.cache 6 | .idea/ 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setUsingCache(true) 5 | ->finder( 6 | Symfony\CS\Finder\DefaultFinder::create() 7 | ->in(__DIR__.'/Tests') 8 | ->in(__DIR__.'/src') 9 | ) 10 | ; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | cache: 4 | directories: 5 | - bin 6 | - vendor 7 | 8 | php: 9 | - 7.4 10 | - 8.0 11 | 12 | before_script: 13 | - composer selfupdate 14 | - composer install --dev --no-interaction --prefer-source 15 | 16 | script: 17 | - mkdir -p build/logs 18 | - bin/phpunit --coverage-clover build/logs/clover.xml 19 | 20 | after_success: 21 | - travis_retry wget https://scrutinizer-ci.com/ocular.phar 22 | - travis_retry php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml 23 | - travis_retry php bin/coveralls -v 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | This changelog will be used for bugfix for this version 5 | 6 | # 0.7 7 | 8 | * add property accessor to change node path for token and expire fields 9 | 10 | # 0.7.1 11 | 12 | Fix deprecated notice from composer (road to composer 2.0) 13 | 14 | # 1.0.0 15 | 16 | Stable version ! 17 | 18 | # 1.0.1 19 | 20 | Fix sipmple-cache requirements, thanks to @neclimdul 21 | 22 | # 2.0.0 23 | 24 | For PHP 8 only 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 eljam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guzzle Jwt middleware 2 | 3 | [![Build Status](https://img.shields.io/travis/eljam/guzzle-jwt-middleware.svg?branch=master&style=flat-square)](https://travis-ci.org/eljam/guzzle-jwt-middleware) 4 | [![Code Quality](https://img.shields.io/scrutinizer/g/eljam/guzzle-jwt-middleware.svg?b=master&style=flat-square)](https://scrutinizer-ci.com/g/eljam/guzzle-jwt-middleware/?branch=master) 5 | [![Code Coverage](https://img.shields.io/coveralls/eljam/guzzle-jwt-middleware.svg?style=flat-square)](https://coveralls.io/r/eljam/guzzle-jwt-middleware) 6 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/87bbdd85-2cd8-4556-94c6-5ed9f501cf7d/mini.png)](https://insight.sensiolabs.com/projects/87bbdd85-2cd8-4556-94c6-5ed9f501cf7d) 7 | [![Latest Unstable Version](https://poser.pugx.org/eljam/guzzle-jwt-middleware/v/unstable)](https://packagist.org/packages/eljam/guzzle-jwt-middleware) 8 | [![Latest Stable Version](https://poser.pugx.org/eljam/guzzle-jwt-middleware/v/stable)](https://packagist.org/packages/eljam/guzzle-jwt-middleware) 9 | [![Downloads](https://img.shields.io/packagist/dt/eljam/guzzle-jwt-middleware.svg)](https://packagist.org/packages/eljam/guzzle-jwt-middleware) 10 | [![license](https://img.shields.io/packagist/l/eljam/guzzle-jwt-middleware.svg)](https://github.com/eljam/guzzle-jwt-middleware/blob/master/LICENSE) 11 | 12 | ## Introduction 13 | 14 | Works great with [LexikJWTAuthenticationBundle](https://github.com/lexik/LexikJWTAuthenticationBundle) 15 | 16 | ## Installation 17 | 18 | `composer require eljam/guzzle-jwt-middleware` 19 | 20 | ## Usage 21 | 22 | ```php 23 | 'admin', 'password' => 'admin']); 35 | 36 | //Optionnal: create your persistence strategy 37 | $persistenceStrategy = null; 38 | 39 | $baseUri = 'http://api.example.org/'; 40 | 41 | // Create authClient 42 | $authClient = new Client(['base_uri' => $baseUri]); 43 | 44 | //Create the JwtManager 45 | $jwtManager = new JwtManager( 46 | $authClient, 47 | $authStrategy, 48 | $persistenceStrategy, 49 | [ 50 | 'token_url' => '/api/token', 51 | ] 52 | ); 53 | 54 | // Create a HandlerStack 55 | $stack = HandlerStack::create(); 56 | 57 | // Add middleware 58 | $stack->push(new JwtMiddleware($jwtManager)); 59 | 60 | $client = new Client(['handler' => $stack, 'base_uri' => $baseUri]); 61 | 62 | try { 63 | $response = $client->get('/api/ping'); 64 | echo($response->getBody()); 65 | } catch (TransferException $e) { 66 | echo $e->getMessage(); 67 | } 68 | 69 | //response 70 | //{"data":"pong"} 71 | 72 | ``` 73 | 74 | ## Auth Strategies 75 | 76 | ### QueryAuthStrategy 77 | 78 | ```php 79 | $authStrategy = new QueryAuthStrategy( 80 | [ 81 | 'username' => 'admin', 82 | 'password' => 'admin', 83 | 'query_fields' => ['username', 'password'], 84 | ] 85 | ); 86 | ``` 87 | 88 | ### FormAuthStrategy 89 | 90 | ```php 91 | $authStrategy = new FormAuthStrategy( 92 | [ 93 | 'username' => 'admin', 94 | 'password' => 'admin', 95 | 'form_fields' => ['username', 'password'], 96 | ] 97 | ); 98 | ``` 99 | 100 | ### HttpBasicAuthStrategy 101 | 102 | ```php 103 | $authStrategy = new HttpBasicAuthStrategy( 104 | [ 105 | 'username' => 'admin', 106 | 'password' => 'password', 107 | ] 108 | ); 109 | ``` 110 | ### JsonAuthStrategy 111 | 112 | ```php 113 | $authStrategy = new JsonAuthStrategy( 114 | [ 115 | 'username' => 'admin', 116 | 'password' => 'admin', 117 | 'json_fields' => ['username', 'password'], 118 | ] 119 | ); 120 | ``` 121 | 122 | ## Persistence 123 | 124 | To avoid requesting a token everytime php runs, you can pass to `JwtManager` an implementation of `TokenPersistenceInterface`. 125 | By default `NullTokenPersistence` will be used. 126 | 127 | ### Simpe cache adapter (PSR-16) 128 | 129 | If you have any [PSR-16 compatible cache](https://www.php-fig.org/psr/psr-16/), you can use it as a persistence handler: 130 | 131 | ```php 132 | '/api/token', 247 | 'token_key' => 'payload.token', 248 | 'expire_key' => 'expires_in' 249 | ] 250 | ); 251 | ``` 252 | 253 | ## Default behavior 254 | By default this library assumes your json response has a key `token`, something like this: 255 | 256 | ```javascript 257 | { 258 | token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9..." 259 | } 260 | ``` 261 | 262 | but now you can change the token_key in the JwtManager options: 263 | 264 | ```php 265 | $jwtManager = new JwtManager( 266 | $authClient, 267 | $authStrategy, 268 | $persistenceStrategy, 269 | [ 270 | 'token_url' => '/api/token', 271 | 'token_key' => 'access_token', 272 | ] 273 | ); 274 | ``` 275 | 276 | ## Authorization Header Type 277 | 278 | Some endpoints use different Authorization header types (Bearer, JWT, etc...). 279 | 280 | The default is Bearer, but another type can be supplied in the middleware: 281 | 282 | ```php 283 | $stack->push(new JwtMiddleware($jwtManager, 'JWT')); 284 | ``` 285 | 286 | ## Cached token 287 | 288 | To avoid too many calls between multiple request, there is a cache system. 289 | 290 | Json example: 291 | 292 | ```javascript 293 | { 294 | token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9...", 295 | expires_in: "3600" 296 | } 297 | ``` 298 | 299 | ```php 300 | $jwtManager = new JwtManager( 301 | $authClient, 302 | $authStrategy, 303 | $persistenceStrategy, 304 | [ 305 | 'token_url' => '/api/token', 306 | 'token_key' => 'access_token', 307 | 'expire_key' => 'expires_in', # default is expires_in if not set 308 | ] 309 | ); 310 | ``` 311 | 312 | The bundle natively supports the [exp field](https://tools.ietf.org/html/rfc7519.html#section-4.1.4) in the JWT payload. 313 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eljam/guzzle-jwt-middleware", 3 | "description": "A jwt authentication middleware for guzzle 6", 4 | "keywords": ["guzzle", "guzzle6", "jwt", "auth", "http", "psr7", "handler", "middleware"], 5 | "homepage": "https://github.com/eljam/guzzle-jwt-middleware", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Guillaume Cavana", 10 | "email": "guillaume.cavana@gmail.com" 11 | } 12 | ], 13 | "autoload" : { 14 | "psr-4" : { 15 | "Eljam\\GuzzleJwt\\" : ["src/"] 16 | } 17 | }, 18 | "autoload-dev" : { 19 | "psr-4": { 20 | "Eljam\\GuzzleJwt\\Tests\\": ["tests/"] 21 | } 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^9.5", 25 | "kodus/mock-cache": "^1.0", 26 | "php-coveralls/php-coveralls": "^2.4" 27 | }, 28 | "require": { 29 | "php" : ">=8.0.0", 30 | "guzzlehttp/guzzle": "^7.0", 31 | "psr/simple-cache": "^1 || ^2 || ^3", 32 | "symfony/options-resolver": ">=2.8", 33 | "symfony/property-access":">=2.8" 34 | }, 35 | "config": { 36 | "bin-dir" : "bin/" 37 | }, 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "1.0.0-dev" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | . 21 | 22 | 23 | tests 24 | vendor 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/JwtMiddleware.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class JwtMiddleware 12 | { 13 | /** 14 | * $JwtManager. 15 | * 16 | * @var JwtManager 17 | */ 18 | protected $jwtManager; 19 | 20 | /** 21 | * The Authorization Header Type (defaults to Bearer) 22 | * 23 | * @var string 24 | */ 25 | protected $authorizationHeaderType; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param JwtManager $jwtManager 31 | * @param string $authorizationHeaderType 32 | */ 33 | public function __construct(JwtManager $jwtManager, $authorizationHeaderType = 'Bearer') 34 | { 35 | $this->jwtManager = $jwtManager; 36 | $this->authorizationHeaderType = $authorizationHeaderType; 37 | } 38 | 39 | /** 40 | * Called when the middleware is handled by the client. 41 | * 42 | * @param callable $handler 43 | * 44 | * @return callable 45 | */ 46 | public function __invoke(callable $handler) 47 | { 48 | $manager = $this->jwtManager; 49 | 50 | return function ( 51 | RequestInterface $request, 52 | array $options 53 | ) use ( 54 | $handler, 55 | $manager 56 | ) { 57 | $token = $manager->getJwtToken()->getToken(); 58 | 59 | return $handler($request->withHeader( 60 | 'Authorization', 61 | sprintf('%s %s', $this->authorizationHeaderType, $token) 62 | ), $options); 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/JwtToken.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class JwtToken 9 | { 10 | /** 11 | * $token. 12 | * 13 | * @var string 14 | */ 15 | private $token; 16 | 17 | /** 18 | * @var \DateTime|null 19 | */ 20 | private $expiration; 21 | 22 | /** 23 | * Constructor. 24 | * 25 | * @param string $token 26 | * @param \DateTime $expiration 27 | */ 28 | public function __construct($token, ?\DateTime $expiration = null) 29 | { 30 | $this->token = $token; 31 | $this->expiration = $expiration; 32 | } 33 | 34 | /** 35 | * getToken. 36 | * 37 | * @return string 38 | */ 39 | public function getToken() 40 | { 41 | return $this->token; 42 | } 43 | 44 | /** 45 | * @return bool 46 | */ 47 | public function isValid() 48 | { 49 | if (!$this->expiration) { 50 | return false; 51 | } 52 | 53 | return (new \DateTime('now + 1 seconds')) < $this->expiration; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Manager/JwtManager.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class JwtManager 20 | { 21 | /** 22 | * $client Guzzle Client. 23 | * 24 | * @var ClientInterface 25 | */ 26 | protected $client; 27 | 28 | /** 29 | * $auth Authentication Strategy. 30 | * 31 | * @var AuthStrategyInterface 32 | */ 33 | protected $auth; 34 | 35 | /** 36 | * $options. 37 | * 38 | * @var array 39 | */ 40 | protected $options; 41 | 42 | /** 43 | * $token. 44 | * 45 | * @var JwtToken 46 | */ 47 | protected $token; 48 | 49 | /** 50 | * @var TokenPersistenceInterface 51 | */ 52 | protected $tokenPersistence; 53 | 54 | /** 55 | * $propertyAccessor. 56 | * 57 | * @var PropertyAccessor 58 | */ 59 | protected $propertyAccessor; 60 | /** 61 | * Constructor. 62 | * 63 | * @param ClientInterface $client 64 | * @param AuthStrategyInterface $auth 65 | * @param TokenPersistenceInterface $tokenPersistence 66 | * @param array $options 67 | */ 68 | public function __construct( 69 | ClientInterface $client, 70 | AuthStrategyInterface $auth, 71 | ?TokenPersistenceInterface $tokenPersistence = null, 72 | array $options = [] 73 | ) { 74 | $this->client = $client; 75 | $this->auth = $auth; 76 | 77 | if ($tokenPersistence === null) { 78 | $tokenPersistence = new NullTokenPersistence(); 79 | } 80 | $this->tokenPersistence = $tokenPersistence; 81 | 82 | $resolver = new OptionsResolver(); 83 | $resolver->setDefaults([ 84 | 'token_url' => '/token', 85 | 'timeout' => 1, 86 | 'token_key' => 'token', 87 | 'expire_key' => 'expires_in', 88 | ]); 89 | 90 | $resolver->setRequired(['token_url', 'timeout']); 91 | 92 | $this->options = $resolver->resolve($options); 93 | 94 | $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); 95 | } 96 | 97 | /** 98 | * getToken. 99 | * 100 | * @return JwtToken 101 | */ 102 | public function getJwtToken() 103 | { 104 | // If token is not set try to get it from the persistent storage. 105 | if ($this->token === null) { 106 | $this->token = $this->tokenPersistence->restoreToken(); 107 | } 108 | 109 | if ($this->token !== null && $this->token->isValid()) { 110 | return $this->token; 111 | } 112 | 113 | $this->tokenPersistence->deleteToken(); 114 | 115 | $url = $this->options['token_url']; 116 | 117 | $requestOptions = array_merge( 118 | $this->getDefaultHeaders(), 119 | $this->auth->getRequestOptions() 120 | ); 121 | 122 | $response = $this->client->request('POST', $url, $requestOptions); 123 | $body = json_decode($response->getBody()); 124 | 125 | //Will be throw because it's mandatory 126 | $tokenValue = $this->propertyAccessor->getValue($body, $this->options['token_key']); 127 | 128 | try { 129 | $expiresIn = $this->propertyAccessor->getValue($body, $this->options['expire_key']); 130 | } catch (NoSuchPropertyException $e) { 131 | $expiresIn = null; 132 | } 133 | 134 | if ($expiresIn) { 135 | $expiration = new \DateTime('now + ' . $expiresIn . ' seconds'); 136 | } elseif (count($jwtParts = explode('.', $tokenValue)) === 3 137 | && is_array($payload = json_decode(base64_decode($jwtParts[1]), true)) 138 | // https://tools.ietf.org/html/rfc7519.html#section-4.1.4 139 | && array_key_exists('exp', $payload) 140 | ) { 141 | // Manually process the payload part to avoid having to drag in a new library 142 | $expiration = new \DateTime('@' . $payload['exp']); 143 | } else { 144 | $expiration = null; 145 | } 146 | 147 | $this->token = new JwtToken($tokenValue, $expiration); 148 | $this->tokenPersistence->saveToken($this->token); 149 | 150 | return $this->token; 151 | } 152 | 153 | /** 154 | * getHeaders. Return defaults header. 155 | * 156 | * @return array 157 | */ 158 | private function getDefaultHeaders() 159 | { 160 | return [ 161 | \GuzzleHttp\RequestOptions::HEADERS => [ 162 | 'timeout' => $this->options['timeout'], 163 | ], 164 | ]; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Persistence/NullTokenPersistence.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class NullTokenPersistence implements TokenPersistenceInterface 11 | { 12 | public function saveToken(JwtToken $token) 13 | { 14 | return; 15 | } 16 | 17 | public function restoreToken() 18 | { 19 | return null; 20 | } 21 | 22 | public function deleteToken() 23 | { 24 | return; 25 | } 26 | 27 | public function hasToken() 28 | { 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Persistence/SimpleCacheTokenPersistence.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 33 | $this->ttl = $ttl; 34 | $this->cacheKey = $cacheKey; 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | public function saveToken(JwtToken $token) 41 | { 42 | /* 43 | * TTL does not need to match token expiration, 44 | * it'll be revalidated by manager so we can safely 45 | * return a stale token. 46 | */ 47 | $this->cache->set($this->cacheKey, $token, $this->ttl); 48 | return; 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function restoreToken() 55 | { 56 | return $this->cache->get($this->cacheKey); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function deleteToken() 63 | { 64 | $this->cache->delete($this->cacheKey); 65 | return; 66 | } 67 | 68 | /** 69 | * @inheritDoc 70 | */ 71 | public function hasToken() 72 | { 73 | return $this->cache->has($this->cacheKey); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Persistence/TokenPersistenceInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface TokenPersistenceInterface 11 | { 12 | /** 13 | * Restore the token data into the give token. 14 | * 15 | * @return JwtToken Restored token 16 | */ 17 | public function restoreToken(); 18 | 19 | /** 20 | * Save the token data. 21 | * 22 | * @param JwtToken $token 23 | */ 24 | public function saveToken(JwtToken $token); 25 | 26 | /** 27 | * Delete the saved token data. 28 | */ 29 | public function deleteToken(); 30 | 31 | /** 32 | * Returns true if a token exists (although it may not be valid) 33 | * 34 | * @return bool 35 | */ 36 | public function hasToken(); 37 | } 38 | -------------------------------------------------------------------------------- /src/Strategy/Auth/AbstractBaseAuthStrategy.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | abstract class AbstractBaseAuthStrategy implements AuthStrategyInterface 11 | { 12 | /** 13 | * $options. 14 | * 15 | * @var array 16 | */ 17 | protected $options; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param array $options 23 | */ 24 | public function __construct(array $options = array()) 25 | { 26 | $resolver = new OptionsResolver(); 27 | $this->configureOptions($resolver); 28 | 29 | $this->options = $resolver->resolve($options); 30 | } 31 | 32 | /** 33 | * configureOptions. 34 | * 35 | * @param OptionsResolver $resolver 36 | */ 37 | public function configureOptions(OptionsResolver $resolver) 38 | { 39 | $resolver->setDefaults(array( 40 | 'username' => '', 41 | 'password' => '', 42 | )); 43 | 44 | $resolver->setRequired(['username', 'password']); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | abstract public function getRequestOptions(); 51 | } 52 | -------------------------------------------------------------------------------- /src/Strategy/Auth/AuthStrategyInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface AuthStrategyInterface 9 | { 10 | /** 11 | * getRequestOptions. 12 | * 13 | * @return array 14 | */ 15 | public function getRequestOptions(); 16 | } 17 | -------------------------------------------------------------------------------- /src/Strategy/Auth/FormAuthStrategy.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class FormAuthStrategy extends AbstractBaseAuthStrategy 11 | { 12 | /** 13 | * {@inheritdoc} 14 | */ 15 | public function configureOptions(OptionsResolver $resolver) 16 | { 17 | parent::configureOptions($resolver); 18 | 19 | $resolver->setDefaults([ 20 | 'form_fields' => ['_username', '_password'], 21 | ]); 22 | 23 | $resolver->setRequired(['form_fields']); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getRequestOptions() 30 | { 31 | return [ 32 | \GuzzleHttp\RequestOptions::FORM_PARAMS => [ 33 | $this->options['form_fields'][0] => $this->options['username'], 34 | $this->options['form_fields'][1] => $this->options['password'], 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Strategy/Auth/HttpBasicAuthStrategy.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class HttpBasicAuthStrategy extends AbstractBaseAuthStrategy 9 | { 10 | /** 11 | * {@inheritdoc} 12 | */ 13 | public function getRequestOptions() 14 | { 15 | return [ 16 | \GuzzleHttp\RequestOptions::AUTH => [ 17 | $this->options['username'], 18 | $this->options['password'], 19 | ], 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Strategy/Auth/JsonAuthStrategy.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class JsonAuthStrategy extends AbstractBaseAuthStrategy 11 | { 12 | /** 13 | * {@inheritdoc} 14 | */ 15 | public function configureOptions(OptionsResolver $resolver) 16 | { 17 | parent::configureOptions($resolver); 18 | 19 | $resolver->setDefaults([ 20 | 'json_fields' => ['_username', '_password'], 21 | ]); 22 | 23 | $resolver->setRequired(['json_fields']); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getRequestOptions() 30 | { 31 | return [ 32 | \GuzzleHttp\RequestOptions::JSON => [ 33 | $this->options['json_fields'][0] => $this->options['username'], 34 | $this->options['json_fields'][1] => $this->options['password'], 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Strategy/Auth/QueryAuthStrategy.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class QueryAuthStrategy extends AbstractBaseAuthStrategy 11 | { 12 | /** 13 | * {@inheritdoc} 14 | */ 15 | public function configureOptions(OptionsResolver $resolver) 16 | { 17 | parent::configureOptions($resolver); 18 | 19 | $resolver->setDefaults([ 20 | 'query_fields' => ['username', 'password'], 21 | ]); 22 | 23 | $resolver->setRequired(['query_fields']); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getRequestOptions() 30 | { 31 | return [ 32 | \GuzzleHttp\RequestOptions::QUERY => [ 33 | $this->options['query_fields'][0] => $this->options['username'], 34 | $this->options['query_fields'][1] => $this->options['password'], 35 | ], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/JwtMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class JwtMiddlewareTest extends \PHPUnit\Framework\TestCase 18 | { 19 | /** 20 | * testJwtAuthorizationHeader. 21 | */ 22 | public function testJwtAuthorizationHeader() 23 | { 24 | $authMockHandler = new MockHandler([ 25 | new Response( 26 | 200, 27 | ['Content-Type' => 'application/json'], 28 | json_encode(['token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9']) 29 | ), 30 | ]); 31 | 32 | $authClient = new Client(['handler' => $authMockHandler]); 33 | $jwtManager = new JwtManager( 34 | $authClient, 35 | (new HttpBasicAuthStrategy(['username' => 'test', 'password' => 'test'])) 36 | ); 37 | 38 | $mockHandler = new MockHandler([ 39 | function (RequestInterface $request) { 40 | $this->assertTrue($request->hasHeader('Authorization')); 41 | $this->assertSame( 42 | 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', 43 | $request->getHeader('Authorization')[0] 44 | ); 45 | 46 | return new Response(200, [], json_encode(['data' => 'pong'])); 47 | }, 48 | ]); 49 | 50 | $handler = HandlerStack::create($mockHandler); 51 | $handler->push(new JwtMiddleware($jwtManager)); 52 | 53 | $client = new Client(['handler' => $handler]); 54 | $client->get('http://api.example.com/api/ping'); 55 | } 56 | 57 | /** 58 | * testJwtAuthorizationHeaderType. 59 | */ 60 | public function testJwtAuthorizationHeaderType() 61 | { 62 | $authMockHandler = new MockHandler([ 63 | new Response( 64 | 200, 65 | ['Content-Type' => 'application/json'], 66 | json_encode(['token' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9']) 67 | ), 68 | ]); 69 | 70 | $authClient = new Client(['handler' => $authMockHandler]); 71 | $jwtManager = new JwtManager( 72 | $authClient, 73 | (new HttpBasicAuthStrategy(['username' => 'test', 'password' => 'test'])) 74 | ); 75 | 76 | $mockHandler = new MockHandler([ 77 | function (RequestInterface $request) { 78 | $this->assertTrue($request->hasHeader('Authorization')); 79 | $this->assertSame( 80 | 'JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', 81 | $request->getHeader('Authorization')[0] 82 | ); 83 | 84 | return new Response(200, [], json_encode(['data' => 'pong'])); 85 | }, 86 | ]); 87 | 88 | $handler = HandlerStack::create($mockHandler); 89 | $handler->push(new JwtMiddleware($jwtManager, 'JWT')); 90 | 91 | $client = new Client(['handler' => $handler]); 92 | $client->get('http://api.example.com/api/ping'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/JwtTokenTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($token->isValid()); 14 | } 15 | 16 | public function testTokenShouldNotBeValidIfExpirationIsNow() 17 | { 18 | $token = new JwtToken('foo', new \DateTime('now')); 19 | 20 | $this->assertFalse($token->isValid()); 21 | } 22 | 23 | public function testTokenShouldBeValidIfExpirationIsInTheFuture() 24 | { 25 | $token = new JwtToken('foo', new \DateTime('now + 5 minutes')); 26 | 27 | $this->assertTrue($token->isValid()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Manager/JwtManagerTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class JwtManagerTest extends \PHPUnit\Framework\TestCase 18 | { 19 | 20 | /** 21 | * testGetTokenExpiredKeyException 22 | */ 23 | public function testGetTokenExpiredKeyException() 24 | { 25 | $mockHandler = new MockHandler([ 26 | function (RequestInterface $request) { 27 | 28 | $this->assertTrue($request->hasHeader('timeout')); 29 | $this->assertEquals( 30 | 3, 31 | $request->getHeaderLine('timeout') 32 | ); 33 | 34 | $jsonPayload = << 'application/json'], 48 | $jsonPayload 49 | ); 50 | }, 51 | ]); 52 | 53 | $handler = HandlerStack::create($mockHandler); 54 | 55 | $authClient = new Client([ 56 | 'handler' => $handler, 57 | ]); 58 | 59 | $authStrategy = new QueryAuthStrategy(['username' => 'admin', 'password' => 'admin']); 60 | 61 | $jwtManager = new JwtManager( 62 | $authClient, 63 | $authStrategy, 64 | null, 65 | [ 66 | 'token_url' => '/api/token', 67 | 'timeout' => 3, 68 | 'token_key' => 'token', 69 | 'expire_key' => 'expires_in' 70 | ] 71 | ); 72 | 73 | $this->expectException('Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException'); 74 | 75 | $token = $jwtManager->getJwtToken(); 76 | } 77 | 78 | /** 79 | * testGetTokenWithSublevelResponse 80 | */ 81 | public function testGetTokenWithSublevelResponse() 82 | { 83 | $mockHandler = new MockHandler([ 84 | function (RequestInterface $request) { 85 | 86 | $this->assertTrue($request->hasHeader('timeout')); 87 | $this->assertEquals( 88 | 3, 89 | $request->getHeaderLine('timeout') 90 | ); 91 | 92 | $jsonPayload = << 'application/json'], 106 | $jsonPayload 107 | ); 108 | }, 109 | ]); 110 | 111 | $handler = HandlerStack::create($mockHandler); 112 | 113 | $authClient = new Client([ 114 | 'handler' => $handler, 115 | ]); 116 | 117 | $authStrategy = new QueryAuthStrategy(['username' => 'admin', 'password' => 'admin']); 118 | 119 | $jwtManager = new JwtManager( 120 | $authClient, 121 | $authStrategy, 122 | null, 123 | [ 124 | 'token_url' => '/api/token', 125 | 'timeout' => 3, 126 | 'token_key' => 'payload.token', 127 | 'expire_key' => 'expires_in' 128 | ] 129 | ); 130 | $token = $jwtManager->getJwtToken(); 131 | 132 | $this->assertInstanceOf(JwtToken::class, $token); 133 | $this->assertEquals('1453720507', $token->getToken()); 134 | } 135 | 136 | /** 137 | * testGetToken. 138 | */ 139 | public function testGetToken() 140 | { 141 | $mockHandler = new MockHandler([ 142 | function (RequestInterface $request) { 143 | 144 | $this->assertTrue($request->hasHeader('timeout')); 145 | $this->assertEquals( 146 | 3, 147 | $request->getHeaderLine('timeout') 148 | ); 149 | 150 | return new Response( 151 | 200, 152 | ['Content-Type' => 'application/json'], 153 | json_encode(['token' => '1453720507', 'expires_in' => 3600]) 154 | ); 155 | }, 156 | ]); 157 | 158 | $handler = HandlerStack::create($mockHandler); 159 | 160 | $authClient = new Client([ 161 | 'handler' => $handler, 162 | ]); 163 | 164 | $authStrategy = new QueryAuthStrategy(['username' => 'admin', 'password' => 'admin']); 165 | 166 | $jwtManager = new JwtManager( 167 | $authClient, 168 | $authStrategy, 169 | null, 170 | ['token_url' => '/api/token', 'timeout' => 3] 171 | ); 172 | $token = $jwtManager->getJwtToken(); 173 | 174 | $this->assertInstanceOf(JwtToken::class, $token); 175 | $this->assertEquals('1453720507', $token->getToken()); 176 | } 177 | 178 | public function testGetTokenWithTokenKeyOption() 179 | { 180 | $mockHandler = new MockHandler([ 181 | function (RequestInterface $request) { 182 | 183 | $this->assertTrue($request->hasHeader('timeout')); 184 | $this->assertEquals( 185 | 3, 186 | $request->getHeaderLine('timeout') 187 | ); 188 | 189 | return new Response( 190 | 200, 191 | ['Content-Type' => 'application/json'], 192 | json_encode(['tokenkey' => '1453720507']) 193 | ); 194 | }, 195 | ]); 196 | 197 | $handler = HandlerStack::create($mockHandler); 198 | 199 | $authClient = new Client([ 200 | 'handler' => $handler, 201 | ]); 202 | 203 | $authStrategy = new QueryAuthStrategy(['username' => 'admin', 'password' => 'admin']); 204 | 205 | $jwtManager = new JwtManager( 206 | $authClient, 207 | $authStrategy, 208 | null, 209 | ['token_url' => '/api/token', 'timeout' => 3, 'token_key' => 'tokenkey'] 210 | ); 211 | $token = $jwtManager->getJwtToken(); 212 | 213 | $this->assertInstanceOf(JwtToken::class, $token); 214 | $this->assertEquals('1453720507', $token->getToken()); 215 | } 216 | 217 | public function testGetTokenShouldGetNewTokenIfCachedTokenIsNotValid() 218 | { 219 | $mockHandler = new MockHandler( 220 | [ 221 | function (RequestInterface $request) { 222 | 223 | $this->assertTrue($request->hasHeader('timeout')); 224 | $this->assertEquals( 225 | 3, 226 | $request->getHeaderLine('timeout') 227 | ); 228 | 229 | return new Response( 230 | 200, 231 | ['Content-Type' => 'application/json'], 232 | json_encode(['token' => '1453720507']) 233 | ); 234 | }, 235 | function (RequestInterface $request) { 236 | 237 | $this->assertTrue($request->hasHeader('timeout')); 238 | $this->assertEquals( 239 | 3, 240 | $request->getHeaderLine('timeout') 241 | ); 242 | 243 | return new Response( 244 | 200, 245 | ['Content-Type' => 'application/json'], 246 | json_encode(['token' => 'foo123']) 247 | ); 248 | }, 249 | ] 250 | ); 251 | 252 | $handler = HandlerStack::create($mockHandler); 253 | 254 | $authClient = new Client([ 255 | 'handler' => $handler, 256 | ]); 257 | 258 | $authStrategy = new QueryAuthStrategy(['username' => 'admin', 'password' => 'admin']); 259 | 260 | $jwtManager = new JwtManager( 261 | $authClient, 262 | $authStrategy, 263 | null, 264 | ['token_url' => '/api/token', 'timeout' => 3] 265 | ); 266 | $token = $jwtManager->getJwtToken(); 267 | 268 | $this->assertInstanceOf(JwtToken::class, $token); 269 | $this->assertEquals('1453720507', $token->getToken()); 270 | 271 | $token = $jwtManager->getJwtToken(); 272 | 273 | $this->assertInstanceOf(JwtToken::class, $token); 274 | $this->assertEquals('foo123', $token->getToken()); 275 | } 276 | 277 | public function testGetTokenShouldUseTheCachedTokenIfItIsValid() 278 | { 279 | $mockHandler = new MockHandler( 280 | [ 281 | function (RequestInterface $request) { 282 | 283 | $this->assertTrue($request->hasHeader('timeout')); 284 | $this->assertEquals( 285 | 3, 286 | $request->getHeaderLine('timeout') 287 | ); 288 | 289 | return new Response( 290 | 200, 291 | ['Content-Type' => 'application/json'], 292 | json_encode(['token' => '1453720507', 'expires_in' => 3600]) 293 | ); 294 | }, 295 | function (RequestInterface $request) { 296 | 297 | $this->assertTrue($request->hasHeader('timeout')); 298 | $this->assertEquals( 299 | 3, 300 | $request->getHeaderLine('timeout') 301 | ); 302 | 303 | return new Response( 304 | 200, 305 | ['Content-Type' => 'application/json'], 306 | json_encode(['token' => 'foo123']) 307 | ); 308 | }, 309 | ] 310 | ); 311 | 312 | $handler = HandlerStack::create($mockHandler); 313 | 314 | $authClient = new Client([ 315 | 'handler' => $handler, 316 | ]); 317 | 318 | $authStrategy = new QueryAuthStrategy(['username' => 'admin', 'password' => 'admin']); 319 | 320 | $jwtManager = new JwtManager( 321 | $authClient, 322 | $authStrategy, 323 | null, 324 | ['token_url' => '/api/token', 'timeout' => 3] 325 | ); 326 | $token = $jwtManager->getJwtToken(); 327 | 328 | $this->assertInstanceOf(JwtToken::class, $token); 329 | $this->assertEquals('1453720507', $token->getToken()); 330 | 331 | $token = $jwtManager->getJwtToken(); 332 | 333 | $this->assertInstanceOf(JwtToken::class, $token); 334 | $this->assertEquals('1453720507', $token->getToken()); 335 | } 336 | 337 | public function testGetTokenShouldUseTheCachedTokenIfItIsValidBasedOnExpField() 338 | { 339 | $jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' 340 | . '.eyJleHAiOiIzMjUwMzY4MDAwMCJ9' 341 | . '.k4YJmJooaa9B4pAM_U8Pi-4ss6RdKFtj9iQqLIAndVA'; 342 | 343 | $mockHandler = new MockHandler( 344 | [ 345 | function (RequestInterface $request) use ($jwtToken) { 346 | 347 | $this->assertTrue($request->hasHeader('timeout')); 348 | $this->assertEquals( 349 | 3, 350 | $request->getHeaderLine('timeout') 351 | ); 352 | 353 | return new Response( 354 | 200, 355 | ['Content-Type' => 'application/json'], 356 | json_encode(['token' => $jwtToken]) 357 | ); 358 | }, 359 | function (RequestInterface $request) { 360 | 361 | $this->assertTrue($request->hasHeader('timeout')); 362 | $this->assertEquals( 363 | 3, 364 | $request->getHeaderLine('timeout') 365 | ); 366 | 367 | return new Response( 368 | 200, 369 | ['Content-Type' => 'application/json'], 370 | json_encode(['token' => uniqid('token', true)]) 371 | ); 372 | }, 373 | ] 374 | ); 375 | 376 | $handler = HandlerStack::create($mockHandler); 377 | 378 | $authClient = new Client([ 379 | 'handler' => $handler, 380 | ]); 381 | 382 | $authStrategy = new QueryAuthStrategy(['username' => 'admin', 'password' => 'admin']); 383 | 384 | $jwtManager = new JwtManager( 385 | $authClient, 386 | $authStrategy, 387 | null, 388 | ['token_url' => '/api/token', 'timeout' => 3] 389 | ); 390 | $token = $jwtManager->getJwtToken(); 391 | 392 | $this->assertInstanceOf(JwtToken::class, $token); 393 | $this->assertEquals($jwtToken, $token->getToken()); 394 | 395 | $token = $jwtManager->getJwtToken(); 396 | 397 | $this->assertInstanceOf(JwtToken::class, $token); 398 | $this->assertEquals($jwtToken, $token->getToken()); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /tests/Persistence/TokenPersistenceTest.php: -------------------------------------------------------------------------------- 1 | saveToken($token); 25 | 26 | $this->assertFalse($tokenPersistence->hasToken()); 27 | $this->assertNull($tokenPersistence->restoreToken()); 28 | } 29 | 30 | /** 31 | * testSimpleCacheTokenPersistenceInterface. 32 | * Makes sure we only use the interface methods. 33 | */ 34 | public function testSimpleCacheTokenPersistenceInterface() 35 | { 36 | $simpleCache = $this->createMock(CacheInterface::class); 37 | $tokenPersistence = new SimpleCacheTokenPersistence($simpleCache); 38 | $token = new JwtToken('foo', new \DateTime('now')); 39 | 40 | $this->assertNull($tokenPersistence->saveToken($token)); 41 | $this->assertNull($tokenPersistence->hasToken()); 42 | $this->assertNull($tokenPersistence->restoreToken()); 43 | $this->assertNull($tokenPersistence->deleteToken()); 44 | } 45 | 46 | /** 47 | * testSimpleCacheTokenPersistence. 48 | */ 49 | public function testSimpleCacheTokenPersistence() 50 | { 51 | $simpleCache = new MockCache(); 52 | $tokenPersistence = new SimpleCacheTokenPersistence($simpleCache); 53 | $token = new JwtToken('foo', new \DateTime('now')); 54 | 55 | $tokenPersistence->saveToken($token); 56 | 57 | $this->assertTrue($tokenPersistence->hasToken()); 58 | $this->assertEquals($tokenPersistence->restoreToken()->getToken(), $token->getToken()); 59 | 60 | $tokenPersistence->deleteToken(); 61 | 62 | $this->assertFalse($tokenPersistence->hasToken()); 63 | $this->assertNull($tokenPersistence->restoreToken()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Strategy/Auth/AuthStrategyTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class AuthStrategyTest extends \PHPUnit\Framework\TestCase 14 | { 15 | /** 16 | * testFormAuthStrategy. 17 | */ 18 | public function testFormAuthStrategy() 19 | { 20 | $authStrategy = new FormAuthStrategy( 21 | [ 22 | 'username' => 'admin', 23 | 'password' => 'admin', 24 | 'form_fields' => ['login', 'password'], 25 | ] 26 | ); 27 | 28 | $this->assertTrue(array_key_exists('login', $authStrategy->getRequestOptions()['form_params'])); 29 | 30 | $this->assertTrue(array_key_exists('password', $authStrategy->getRequestOptions()['form_params'])); 31 | 32 | $this->assertEquals('admin', $authStrategy->getRequestOptions()['form_params']['login']); 33 | $this->assertEquals('admin', $authStrategy->getRequestOptions()['form_params']['password']); 34 | } 35 | 36 | /** 37 | * tesQueryAuthStrategy. 38 | */ 39 | public function testQueryAuthStrategy() 40 | { 41 | $authStrategy = new QueryAuthStrategy( 42 | [ 43 | 'username' => 'admin', 44 | 'password' => 'admin', 45 | 'query_fields' => ['username', 'password'], 46 | ] 47 | ); 48 | 49 | $this->assertTrue(array_key_exists('username', $authStrategy->getRequestOptions()['query'])); 50 | $this->assertTrue(array_key_exists('password', $authStrategy->getRequestOptions()['query'])); 51 | 52 | $this->assertEquals('admin', $authStrategy->getRequestOptions()['query']['username']); 53 | $this->assertEquals('admin', $authStrategy->getRequestOptions()['query']['password']); 54 | } 55 | 56 | /** 57 | * testHttpBasicAuthStrategy. 58 | */ 59 | public function testHttpBasicAuthStrategy() 60 | { 61 | $authStrategy = new HttpBasicAuthStrategy( 62 | [ 63 | 'username' => 'admin', 64 | 'password' => 'password', 65 | ] 66 | ); 67 | 68 | $this->assertEquals('admin', $authStrategy->getRequestOptions()['auth'][0]); 69 | $this->assertEquals('password', $authStrategy->getRequestOptions()['auth'][1]); 70 | } 71 | 72 | /** 73 | * testJsonAuthStrategy. 74 | */ 75 | public function testJsonAuthStrategy() 76 | { 77 | $authStrategy = new JsonAuthStrategy( 78 | [ 79 | 'username' => 'admin', 80 | 'password' => 'admin', 81 | 'json_fields' => ['login', 'password'], 82 | ] 83 | ); 84 | 85 | $this->assertTrue(array_key_exists('login', $authStrategy->getRequestOptions()['json'])); 86 | 87 | $this->assertTrue(array_key_exists('password', $authStrategy->getRequestOptions()['json'])); 88 | 89 | $this->assertEquals('admin', $authStrategy->getRequestOptions()['json']['login']); 90 | $this->assertEquals('admin', $authStrategy->getRequestOptions()['json']['password']); 91 | } 92 | } 93 | --------------------------------------------------------------------------------