From 13d763a2d4ea76d0e0b6f4746ffd92403ecca873 Mon Sep 17 00:00:00 2001 From: Evgeny Kuchuk Date: Tue, 5 May 2020 23:42:18 +0300 Subject: [PATCH] Initial commit --- LICENSE | 21 ++ README.md | 40 ++ composer.json | 51 +++ phpunit.xml.dist | 34 ++ src/Blacklist.php | 113 ++++++ src/Claims/Audience.php | 22 ++ src/Claims/Claim.php | 121 ++++++ src/Claims/ClaimInterface.php | 45 +++ src/Claims/Custom.php | 25 ++ src/Claims/Expiration.php | 33 ++ src/Claims/Factory.php | 55 +++ src/Claims/IssuedAt.php | 33 ++ src/Claims/Issuer.php | 22 ++ src/Claims/JwtId.php | 22 ++ src/Claims/NotBefore.php | 33 ++ src/Claims/Subject.php | 22 ++ src/Commands/JWTGenerateCommand.php | 81 +++++ src/Exceptions/InvalidClaimException.php | 20 + src/Exceptions/JWTException.php | 49 +++ src/Exceptions/PayloadException.php | 20 + src/Exceptions/TokenBlacklistedException.php | 20 + src/Exceptions/TokenExpiredException.php | 20 + src/Exceptions/TokenInvalidException.php | 20 + src/Facades/JWTAuth.php | 27 ++ src/Facades/JWTFactory.php | 27 ++ src/JWTAuth.php | 343 ++++++++++++++++++ src/JWTManager.php | 183 ++++++++++ src/Middleware/BaseMiddleware.php | 64 ++++ src/Middleware/GetUserFromToken.php | 48 +++ src/Middleware/RefreshToken.php | 43 +++ src/Payload.php | 175 +++++++++ src/PayloadFactory.php | 245 +++++++++++++ src/Providers/Auth/AuthInterface.php | 38 ++ src/Providers/Auth/IlluminateAuthAdapter.php | 62 ++++ src/Providers/JWT/JWTInterface.php | 27 ++ src/Providers/JWT/JWTProvider.php | 82 +++++ src/Providers/JWT/NamshiAdapter.php | 76 ++++ src/Providers/JWTAuthServiceProvider.php | 278 ++++++++++++++ .../Storage/IlluminateCacheAdapter.php | 94 +++++ src/Providers/Storage/StorageInterface.php | 39 ++ src/Providers/User/EloquentUserAdapter.php | 44 +++ src/Providers/User/UserInterface.php | 24 ++ src/Token.php | 54 +++ src/Utils.php | 38 ++ src/Validators/AbstractValidator.php | 52 +++ src/Validators/PayloadValidator.php | 127 +++++++ src/Validators/TokenValidator.php | 42 +++ src/Validators/ValidatorInterface.php | 31 ++ src/config/config.php | 173 +++++++++ tests/BlacklistTest.php | 153 ++++++++ tests/Commands/JWTGenerateCommandTest.php | 44 +++ tests/JWTAuthTest.php | 235 ++++++++++++ tests/JWTManagerTest.php | 188 ++++++++++ tests/Middleware/GetUserFromTokenTest.php | 108 ++++++ tests/PayloadFactoryTest.php | 136 +++++++ tests/PayloadTest.php | 136 +++++++ .../Auth/IlluminateAuthAdapterTest.php | 65 ++++ tests/Providers/JWT/JWTProviderTest.php | 36 ++ tests/Providers/JWT/NamshiAdapterTest.php | 77 ++++ .../Storage/IlluminateCacheAdapterTest.php | 63 ++++ .../User/EloquentUserAdapterTest.php | 41 +++ tests/Stubs/JWTProviderStub.php | 18 + tests/TokenTest.php | 34 ++ tests/Validators/PayloadValidatorTest.php | 142 ++++++++ tests/Validators/TokenValidatorTest.php | 42 +++ 65 files changed, 4876 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/Blacklist.php create mode 100644 src/Claims/Audience.php create mode 100644 src/Claims/Claim.php create mode 100644 src/Claims/ClaimInterface.php create mode 100644 src/Claims/Custom.php create mode 100644 src/Claims/Expiration.php create mode 100644 src/Claims/Factory.php create mode 100644 src/Claims/IssuedAt.php create mode 100644 src/Claims/Issuer.php create mode 100644 src/Claims/JwtId.php create mode 100644 src/Claims/NotBefore.php create mode 100644 src/Claims/Subject.php create mode 100644 src/Commands/JWTGenerateCommand.php create mode 100644 src/Exceptions/InvalidClaimException.php create mode 100644 src/Exceptions/JWTException.php create mode 100644 src/Exceptions/PayloadException.php create mode 100644 src/Exceptions/TokenBlacklistedException.php create mode 100644 src/Exceptions/TokenExpiredException.php create mode 100644 src/Exceptions/TokenInvalidException.php create mode 100644 src/Facades/JWTAuth.php create mode 100644 src/Facades/JWTFactory.php create mode 100644 src/JWTAuth.php create mode 100644 src/JWTManager.php create mode 100644 src/Middleware/BaseMiddleware.php create mode 100644 src/Middleware/GetUserFromToken.php create mode 100644 src/Middleware/RefreshToken.php create mode 100644 src/Payload.php create mode 100644 src/PayloadFactory.php create mode 100644 src/Providers/Auth/AuthInterface.php create mode 100644 src/Providers/Auth/IlluminateAuthAdapter.php create mode 100644 src/Providers/JWT/JWTInterface.php create mode 100644 src/Providers/JWT/JWTProvider.php create mode 100644 src/Providers/JWT/NamshiAdapter.php create mode 100644 src/Providers/JWTAuthServiceProvider.php create mode 100644 src/Providers/Storage/IlluminateCacheAdapter.php create mode 100644 src/Providers/Storage/StorageInterface.php create mode 100644 src/Providers/User/EloquentUserAdapter.php create mode 100644 src/Providers/User/UserInterface.php create mode 100644 src/Token.php create mode 100644 src/Utils.php create mode 100644 src/Validators/AbstractValidator.php create mode 100644 src/Validators/PayloadValidator.php create mode 100644 src/Validators/TokenValidator.php create mode 100644 src/Validators/ValidatorInterface.php create mode 100644 src/config/config.php create mode 100644 tests/BlacklistTest.php create mode 100644 tests/Commands/JWTGenerateCommandTest.php create mode 100644 tests/JWTAuthTest.php create mode 100644 tests/JWTManagerTest.php create mode 100644 tests/Middleware/GetUserFromTokenTest.php create mode 100644 tests/PayloadFactoryTest.php create mode 100644 tests/PayloadTest.php create mode 100644 tests/Providers/Auth/IlluminateAuthAdapterTest.php create mode 100644 tests/Providers/JWT/JWTProviderTest.php create mode 100644 tests/Providers/JWT/NamshiAdapterTest.php create mode 100644 tests/Providers/Storage/IlluminateCacheAdapterTest.php create mode 100644 tests/Providers/User/EloquentUserAdapterTest.php create mode 100644 tests/Stubs/JWTProviderStub.php create mode 100644 tests/TokenTest.php create mode 100644 tests/Validators/PayloadValidatorTest.php create mode 100644 tests/Validators/TokenValidatorTest.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..72a2739 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Sean Tymon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcf76e0 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# jwt-auth + +> JSON Web Token Authentication for Laravel + +[![Build Status](http://img.shields.io/travis/tymondesigns/jwt-auth/master.svg?style=flat-square)](https://travis-ci.org/tymondesigns/jwt-auth) +[![Scrutinizer Code Quality](http://img.shields.io/scrutinizer/g/tymondesigns/jwt-auth.svg?style=flat-square)](https://scrutinizer-ci.com/g/tymondesigns/jwt-auth/) +[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/tymondesigns/jwt-auth.svg?style=flat-square)](https://scrutinizer-ci.com/g/tymondesigns/jwt-auth/code-structure) +[![StyleCI](https://styleci.io/repos/23680678/shield?style=flat-square)](https://styleci.io/repos/23680678) +[![HHVM](https://img.shields.io/hhvm/tymon/jwt-auth.svg?style=flat-square)](http://hhvm.h4cc.de/package/tymon/jwt-auth) +[![Latest Version](http://img.shields.io/packagist/v/tymon/jwt-auth.svg?style=flat-square)](https://packagist.org/packages/tymon/jwt-auth) +[![Latest Dev Version](https://img.shields.io/packagist/vpre/tymon/jwt-auth.svg?style=flat-square)](https://packagist.org/packages/tymon/jwt-auth#dev-develop) +[![Monthly Downloads](https://img.shields.io/packagist/dm/tymon/jwt-auth.svg?style=flat-square)](https://packagist.org/packages/tymon/jwt-auth) + +See the [WIKI](https://github.com/tymondesigns/jwt-auth/wiki) for documentation + +## License + +The MIT License (MIT) + +Copyright (c) 2014 Sean Tymon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +[![Gratipay](https://img.shields.io/gratipay/tymondesigns.svg?style=flat-square)](https://gratipay.com/~tymondesigns) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1faf3a6 --- /dev/null +++ b/composer.json @@ -0,0 +1,51 @@ +{ + "name": "tymon/jwt-auth", + "description": "JSON Web Token Authentication for Laravel 4 and 5", + "keywords": [ + "jwt", + "auth", + "authentication", + "tymon", + "laravel", + "json web token" + ], + "homepage": "https://github.com/tymondesigns/jwt-auth", + "license": "MIT", + "authors": [ + { + "name": "Sean Tymon", + "email": "tymon148@gmail.com", + "homepage": "http://tymondesigns.com", + "role": "Developer" + } + ], + "require": { + "php": ">=5.4.0", + "illuminate/support": "~5.0", + "illuminate/http": "~5.0", + "namshi/jose": "^5.0 || ^7.0", + "nesbot/carbon": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*", + "mockery/mockery": "0.9.*", + "illuminate/auth": "~5.0", + "illuminate/database": "~5.0", + "illuminate/console" : "~5.0" + }, + "autoload": { + "psr-4": { + "Tymon\\JWTAuth\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tymon\\JWTAuth\\Test\\": "tests" + } + }, + "extra": { + "branch-alias": { + "dev-develop": "0.5-dev" + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..7836abe --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + tests + + + + + src/ + + src/Providers/JWTAuthServiceProvider.php + src/config/ + src/Facades/ + + + + + + + + + + + diff --git a/src/Blacklist.php b/src/Blacklist.php new file mode 100644 index 0000000..f562b2a --- /dev/null +++ b/src/Blacklist.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth; + +use Tymon\JWTAuth\Providers\Storage\StorageInterface; + +class Blacklist +{ + /** + * @var \Tymon\JWTAuth\Providers\Storage\StorageInterface + */ + protected $storage; + + /** + * Number of minutes from issue date in which a JWT can be refreshed. + * + * @var int + */ + protected $refreshTTL = 20160; + + /** + * @param \Tymon\JWTAuth\Providers\Storage\StorageInterface $storage + */ + public function __construct(StorageInterface $storage) + { + $this->storage = $storage; + } + + /** + * Add the token (jti claim) to the blacklist. + * + * @param \Tymon\JWTAuth\Payload $payload + * @return bool + */ + public function add(Payload $payload) + { + $exp = Utils::timestamp($payload['exp']); + $refreshExp = Utils::timestamp($payload['iat'])->addMinutes($this->refreshTTL); + + // there is no need to add the token to the blacklist + // if the token has already expired AND the refresh_ttl + // has gone by + if ($exp->isPast() && $refreshExp->isPast()) { + return false; + } + + // Set the cache entry's lifetime to be equal to the amount + // of refreshable time it has remaining (which is the larger + // of `exp` and `iat+refresh_ttl`), rounded up a minute + $cacheLifetime = $exp->max($refreshExp)->addMinute()->diffInMinutes(); + + $this->storage->add($payload['jti'], [], $cacheLifetime); + + return true; + } + + /** + * Determine whether the token has been blacklisted. + * + * @param \Tymon\JWTAuth\Payload $payload + * @return bool + */ + public function has(Payload $payload) + { + return $this->storage->has($payload['jti']); + } + + /** + * Remove the token (jti claim) from the blacklist. + * + * @param \Tymon\JWTAuth\Payload $payload + * @return bool + */ + public function remove(Payload $payload) + { + return $this->storage->destroy($payload['jti']); + } + + /** + * Remove all tokens from the blacklist. + * + * @return bool + */ + public function clear() + { + $this->storage->flush(); + + return true; + } + + /** + * Set the refresh time limit. + * + * @param int + * + * @return $this + */ + public function setRefreshTTL($ttl) + { + $this->refreshTTL = (int) $ttl; + + return $this; + } +} diff --git a/src/Claims/Audience.php b/src/Claims/Audience.php new file mode 100644 index 0000000..5a1854a --- /dev/null +++ b/src/Claims/Audience.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class Audience extends Claim +{ + /** + * The claim name. + * + * @var string + */ + protected $name = 'aud'; +} diff --git a/src/Claims/Claim.php b/src/Claims/Claim.php new file mode 100644 index 0000000..1067d2a --- /dev/null +++ b/src/Claims/Claim.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +use Tymon\JWTAuth\Exceptions\InvalidClaimException; + +abstract class Claim implements ClaimInterface +{ + /** + * The claim name. + * + * @var string + */ + protected $name; + + /** + * The claim value. + * + * @var mixed + */ + private $value; + + /** + * @param mixed $value + */ + public function __construct($value) + { + $this->setValue($value); + } + + /** + * Set the claim value, and call a validate method if available. + * + * @param $value + * @throws \Tymon\JWTAuth\Exceptions\InvalidClaimException + * @return $this + */ + public function setValue($value) + { + if (! $this->validate($value)) { + throw new InvalidClaimException('Invalid value provided for claim "'.$this->getName().'": '.$value); + } + + $this->value = $value; + + return $this; + } + + /** + * Get the claim value. + * + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * Set the claim name. + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get the claim name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Validate the Claim value. + * + * @param $value + * @return bool + */ + protected function validate($value) + { + return true; + } + + /** + * Build a key value array comprising of the claim name and value. + * + * @return array + */ + public function toArray() + { + return [$this->getName() => $this->getValue()]; + } + + /** + * Get the claim as a string. + * + * @return string + */ + public function __toString() + { + return json_encode($this->toArray(), JSON_UNESCAPED_SLASHES); + } +} diff --git a/src/Claims/ClaimInterface.php b/src/Claims/ClaimInterface.php new file mode 100644 index 0000000..8768c71 --- /dev/null +++ b/src/Claims/ClaimInterface.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +interface ClaimInterface +{ + /** + * Set the claim value, and call a validate method if available. + * + * @param mixed + * @return Claim + */ + public function setValue($value); + + /** + * Get the claim value. + * + * @return mixed + */ + public function getValue(); + + /** + * Set the claim name. + * + * @param string $name + * @return Claim + */ + public function setName($name); + + /** + * Get the claim name. + * + * @return string + */ + public function getName(); +} diff --git a/src/Claims/Custom.php b/src/Claims/Custom.php new file mode 100644 index 0000000..b9a8f23 --- /dev/null +++ b/src/Claims/Custom.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class Custom extends Claim +{ + /** + * @param string $name + * @param mixed $value + */ + public function __construct($name, $value) + { + parent::__construct($value); + $this->setName($name); + } +} diff --git a/src/Claims/Expiration.php b/src/Claims/Expiration.php new file mode 100644 index 0000000..48f5171 --- /dev/null +++ b/src/Claims/Expiration.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class Expiration extends Claim +{ + /** + * The claim name. + * + * @var string + */ + protected $name = 'exp'; + + /** + * Validate the expiry claim. + * + * @param mixed $value + * @return bool + */ + protected function validate($value) + { + return is_numeric($value); + } +} diff --git a/src/Claims/Factory.php b/src/Claims/Factory.php new file mode 100644 index 0000000..696e30b --- /dev/null +++ b/src/Claims/Factory.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class Factory +{ + /** + * @var array + */ + private static $classMap = [ + 'aud' => 'Tymon\JWTAuth\Claims\Audience', + 'exp' => 'Tymon\JWTAuth\Claims\Expiration', + 'iat' => 'Tymon\JWTAuth\Claims\IssuedAt', + 'iss' => 'Tymon\JWTAuth\Claims\Issuer', + 'jti' => 'Tymon\JWTAuth\Claims\JwtId', + 'nbf' => 'Tymon\JWTAuth\Claims\NotBefore', + 'sub' => 'Tymon\JWTAuth\Claims\Subject', + ]; + + /** + * Get the instance of the claim when passing the name and value. + * + * @param string $name + * @param mixed $value + * @return \Tymon\JWTAuth\Claims\Claim + */ + public function get($name, $value) + { + if ($this->has($name)) { + return new self::$classMap[$name]($value); + } + + return new Custom($name, $value); + } + + /** + * Check whether the claim exists. + * + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, self::$classMap); + } +} diff --git a/src/Claims/IssuedAt.php b/src/Claims/IssuedAt.php new file mode 100644 index 0000000..89bea75 --- /dev/null +++ b/src/Claims/IssuedAt.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class IssuedAt extends Claim +{ + /** + * The claim name. + * + * @var string + */ + protected $name = 'iat'; + + /** + * Validate the issued at claim. + * + * @param mixed $value + * @return bool + */ + protected function validate($value) + { + return is_numeric($value); + } +} diff --git a/src/Claims/Issuer.php b/src/Claims/Issuer.php new file mode 100644 index 0000000..c20ba82 --- /dev/null +++ b/src/Claims/Issuer.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class Issuer extends Claim +{ + /** + * The claim name. + * + * @var string + */ + protected $name = 'iss'; +} diff --git a/src/Claims/JwtId.php b/src/Claims/JwtId.php new file mode 100644 index 0000000..15a3287 --- /dev/null +++ b/src/Claims/JwtId.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class JwtId extends Claim +{ + /** + * The claim name. + * + * @var string + */ + protected $name = 'jti'; +} diff --git a/src/Claims/NotBefore.php b/src/Claims/NotBefore.php new file mode 100644 index 0000000..ee15a53 --- /dev/null +++ b/src/Claims/NotBefore.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class NotBefore extends Claim +{ + /** + * The claim name. + * + * @var string + */ + protected $name = 'nbf'; + + /** + * Validate the not before claim. + * + * @param mixed $value + * @return bool + */ + protected function validate($value) + { + return is_numeric($value); + } +} diff --git a/src/Claims/Subject.php b/src/Claims/Subject.php new file mode 100644 index 0000000..71b0514 --- /dev/null +++ b/src/Claims/Subject.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Claims; + +class Subject extends Claim +{ + /** + * The claim name. + * + * @var string + */ + protected $name = 'sub'; +} diff --git a/src/Commands/JWTGenerateCommand.php b/src/Commands/JWTGenerateCommand.php new file mode 100644 index 0000000..a62e8f9 --- /dev/null +++ b/src/Commands/JWTGenerateCommand.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Commands; + +use Illuminate\Support\Str; +use Illuminate\Console\Command; +use Symfony\Component\Console\Input\InputOption; + +class JWTGenerateCommand extends Command +{ + /** + * The console command name. + * + * @var string + */ + protected $name = 'jwt:generate'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Set the JWTAuth secret key used to sign the tokens'; + + /** + * Execute the console command. + * + * @return void + */ + public function fire() + { + $key = $this->getRandomKey(); + + if ($this->option('show')) { + return $this->line(''.$key.''); + } + + $path = config_path('jwt.php'); + + if (file_exists($path)) { + file_put_contents($path, str_replace( + $this->laravel['config']['jwt.secret'], $key, file_get_contents($path) + )); + } + + $this->laravel['config']['jwt.secret'] = $key; + + $this->info("jwt-auth secret [$key] set successfully."); + } + + /** + * Generate a random key for the JWT Auth secret. + * + * @return string + */ + protected function getRandomKey() + { + return Str::random(32); + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return [ + ['show', null, InputOption::VALUE_NONE, 'Simply display the key instead of modifying files.'], + ]; + } +} diff --git a/src/Exceptions/InvalidClaimException.php b/src/Exceptions/InvalidClaimException.php new file mode 100644 index 0000000..b164672 --- /dev/null +++ b/src/Exceptions/InvalidClaimException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Exceptions; + +class InvalidClaimException extends JWTException +{ + /** + * @var int + */ + protected $statusCode = 400; +} diff --git a/src/Exceptions/JWTException.php b/src/Exceptions/JWTException.php new file mode 100644 index 0000000..5473f04 --- /dev/null +++ b/src/Exceptions/JWTException.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Exceptions; + +class JWTException extends \Exception +{ + /** + * @var int + */ + protected $statusCode = 500; + + /** + * @param string $message + * @param int $statusCode + */ + public function __construct($message = 'An error occurred', $statusCode = null) + { + parent::__construct($message); + + if (! is_null($statusCode)) { + $this->setStatusCode($statusCode); + } + } + + /** + * @param int $statusCode + */ + public function setStatusCode($statusCode) + { + $this->statusCode = $statusCode; + } + + /** + * @return int the status code + */ + public function getStatusCode() + { + return $this->statusCode; + } +} diff --git a/src/Exceptions/PayloadException.php b/src/Exceptions/PayloadException.php new file mode 100644 index 0000000..4767abd --- /dev/null +++ b/src/Exceptions/PayloadException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Exceptions; + +class PayloadException extends JWTException +{ + /** + * @var int + */ + protected $statusCode = 500; +} diff --git a/src/Exceptions/TokenBlacklistedException.php b/src/Exceptions/TokenBlacklistedException.php new file mode 100644 index 0000000..58bd582 --- /dev/null +++ b/src/Exceptions/TokenBlacklistedException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Exceptions; + +class TokenBlacklistedException extends TokenInvalidException +{ + /** + * @var int + */ + protected $statusCode = 401; +} diff --git a/src/Exceptions/TokenExpiredException.php b/src/Exceptions/TokenExpiredException.php new file mode 100644 index 0000000..d613577 --- /dev/null +++ b/src/Exceptions/TokenExpiredException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Exceptions; + +class TokenExpiredException extends JWTException +{ + /** + * @var int + */ + protected $statusCode = 401; +} diff --git a/src/Exceptions/TokenInvalidException.php b/src/Exceptions/TokenInvalidException.php new file mode 100644 index 0000000..6740d59 --- /dev/null +++ b/src/Exceptions/TokenInvalidException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Exceptions; + +class TokenInvalidException extends JWTException +{ + /** + * @var int + */ + protected $statusCode = 400; +} diff --git a/src/Facades/JWTAuth.php b/src/Facades/JWTAuth.php new file mode 100644 index 0000000..419b590 --- /dev/null +++ b/src/Facades/JWTAuth.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Facades; + +use Illuminate\Support\Facades\Facade; + +class JWTAuth extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() + { + return 'tymon.jwt.auth'; + } +} diff --git a/src/Facades/JWTFactory.php b/src/Facades/JWTFactory.php new file mode 100644 index 0000000..f43ff46 --- /dev/null +++ b/src/Facades/JWTFactory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Facades; + +use Illuminate\Support\Facades\Facade; + +class JWTFactory extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() + { + return 'tymon.jwt.payload.factory'; + } +} diff --git a/src/JWTAuth.php b/src/JWTAuth.php new file mode 100644 index 0000000..9e8b627 --- /dev/null +++ b/src/JWTAuth.php @@ -0,0 +1,343 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth; + +use Illuminate\Http\Request; +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Providers\Auth\AuthInterface; +use Tymon\JWTAuth\Providers\User\UserInterface; + +class JWTAuth +{ + /** + * @var \Tymon\JWTAuth\JWTManager + */ + protected $manager; + + /** + * @var \Tymon\JWTAuth\Providers\User\UserInterface + */ + protected $user; + + /** + * @var \Tymon\JWTAuth\Providers\Auth\AuthInterface + */ + protected $auth; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * @var string + */ + protected $identifier = 'id'; + + /** + * @var \Tymon\JWTAuth\Token + */ + protected $token; + + /** + * @param \Tymon\JWTAuth\JWTManager $manager + * @param \Tymon\JWTAuth\Providers\User\UserInterface $user + * @param \Tymon\JWTAuth\Providers\Auth\AuthInterface $auth + * @param \Illuminate\Http\Request $request + */ + public function __construct(JWTManager $manager, UserInterface $user, AuthInterface $auth, Request $request) + { + $this->manager = $manager; + $this->user = $user; + $this->auth = $auth; + $this->request = $request; + } + + /** + * Find a user using the user identifier in the subject claim. + * + * @param bool|string $token + * + * @return mixed + */ + public function toUser($token = false) + { + $payload = $this->getPayload($token); + + if (! $user = $this->user->getBy($this->identifier, $payload['sub'])) { + return false; + } + + return $user; + } + + /** + * Generate a token using the user identifier as the subject claim. + * + * @param mixed $user + * @param array $customClaims + * + * @return string + */ + public function fromUser($user, array $customClaims = []) + { + $payload = $this->makePayload($user->{$this->identifier}, $customClaims); + + return $this->manager->encode($payload)->get(); + } + + /** + * Attempt to authenticate the user and return the token. + * + * @param array $credentials + * @param array $customClaims + * + * @return false|string + */ + public function attempt(array $credentials = [], array $customClaims = []) + { + if (! $this->auth->byCredentials($credentials)) { + return false; + } + + return $this->fromUser($this->auth->user(), $customClaims); + } + + /** + * Authenticate a user via a token. + * + * @param mixed $token + * + * @return mixed + */ + public function authenticate($token = false) + { + $id = $this->getPayload($token)->get('sub'); + + if (! $this->auth->byId($id)) { + return false; + } + + return $this->auth->user(); + } + + /** + * Refresh an expired token. + * + * @param mixed $token + * + * @return string + */ + public function refresh($token = false) + { + $this->requireToken($token); + + return $this->manager->refresh($this->token)->get(); + } + + /** + * Invalidate a token (add it to the blacklist). + * + * @param mixed $token + * + * @return bool + */ + public function invalidate($token = false) + { + $this->requireToken($token); + + return $this->manager->invalidate($this->token); + } + + /** + * Get the token. + * + * @return bool|string + */ + public function getToken() + { + if (! $this->token) { + try { + $this->parseToken(); + } catch (JWTException $e) { + return false; + } + } + + return $this->token; + } + + /** + * Get the raw Payload instance. + * + * @param mixed $token + * + * @return \Tymon\JWTAuth\Payload + */ + public function getPayload($token = false) + { + $this->requireToken($token); + + return $this->manager->decode($this->token); + } + + /** + * Parse the token from the request. + * + * @param string $query + * + * @return JWTAuth + */ + public function parseToken($method = 'bearer', $header = 'authorization', $query = 'token') + { + if (! $token = $this->parseAuthHeader($header, $method)) { + if (! $token = $this->request->query($query, false)) { + throw new JWTException('The token could not be parsed from the request', 400); + } + } + + return $this->setToken($token); + } + + /** + * Parse token from the authorization header. + * + * @param string $header + * @param string $method + * + * @return false|string + */ + protected function parseAuthHeader($header = 'authorization', $method = 'bearer') + { + $header = $this->request->headers->get($header); + + if (! starts_with(strtolower($header), $method)) { + return false; + } + + return trim(str_ireplace($method, '', $header)); + } + + /** + * Create a Payload instance. + * + * @param mixed $subject + * @param array $customClaims + * + * @return \Tymon\JWTAuth\Payload + */ + protected function makePayload($subject, array $customClaims = []) + { + return $this->manager->getPayloadFactory()->make( + array_merge($customClaims, ['sub' => $subject]) + ); + } + + /** + * Set the identifier. + * + * @param string $identifier + * + * @return $this + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + + return $this; + } + + /** + * Get the identifier. + * + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Set the token. + * + * @param string $token + * + * @return $this + */ + public function setToken($token) + { + $this->token = new Token($token); + + return $this; + } + + /** + * Ensure that a token is available. + * + * @param mixed $token + * + * @return JWTAuth + * + * @throws \Tymon\JWTAuth\Exceptions\JWTException + */ + protected function requireToken($token) + { + if ($token) { + return $this->setToken($token); + } elseif ($this->token) { + return $this; + } else { + throw new JWTException('A token is required', 400); + } + } + + /** + * Set the request instance. + * + * @param Request $request + */ + public function setRequest(Request $request) + { + $this->request = $request; + + return $this; + } + + /** + * Get the JWTManager instance. + * + * @return \Tymon\JWTAuth\JWTManager + */ + public function manager() + { + return $this->manager; + } + + /** + * Magically call the JWT Manager. + * + * @param string $method + * @param array $parameters + * + * @return mixed + * + * @throws \BadMethodCallException + */ + public function __call($method, $parameters) + { + if (method_exists($this->manager, $method)) { + return call_user_func_array([$this->manager, $method], $parameters); + } + + throw new \BadMethodCallException("Method [$method] does not exist."); + } +} diff --git a/src/JWTManager.php b/src/JWTManager.php new file mode 100644 index 0000000..603e705 --- /dev/null +++ b/src/JWTManager.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth; + +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Providers\JWT\JWTInterface; +use Tymon\JWTAuth\Exceptions\TokenBlacklistedException; + +class JWTManager +{ + /** + * @var \Tymon\JWTAuth\Providers\JWT\JWTInterface + */ + protected $jwt; + + /** + * @var \Tymon\JWTAuth\Blacklist + */ + protected $blacklist; + + /** + * @var \Tymon\JWTAuth\PayloadFactory + */ + protected $payloadFactory; + + /** + * @var bool + */ + protected $blacklistEnabled = true; + + /** + * @var bool + */ + protected $refreshFlow = false; + + /** + * @param \Tymon\JWTAuth\Providers\JWT\JWTInterface $jwt + * @param \Tymon\JWTAuth\Blacklist $blacklist + * @param \Tymon\JWTAuth\PayloadFactory $payloadFactory + */ + public function __construct(JWTInterface $jwt, Blacklist $blacklist, PayloadFactory $payloadFactory) + { + $this->jwt = $jwt; + $this->blacklist = $blacklist; + $this->payloadFactory = $payloadFactory; + } + + /** + * Encode a Payload and return the Token. + * + * @param \Tymon\JWTAuth\Payload $payload + * @return \Tymon\JWTAuth\Token + */ + public function encode(Payload $payload) + { + $token = $this->jwt->encode($payload->get()); + + return new Token($token); + } + + /** + * Decode a Token and return the Payload. + * + * @param \Tymon\JWTAuth\Token $token + * @return Payload + * @throws TokenBlacklistedException + */ + public function decode(Token $token) + { + $payloadArray = $this->jwt->decode($token->get()); + + $payload = $this->payloadFactory->setRefreshFlow($this->refreshFlow)->make($payloadArray); + + if ($this->blacklistEnabled && $this->blacklist->has($payload)) { + throw new TokenBlacklistedException('The token has been blacklisted'); + } + + return $payload; + } + + /** + * Refresh a Token and return a new Token. + * + * @param \Tymon\JWTAuth\Token $token + * @return \Tymon\JWTAuth\Token + */ + public function refresh(Token $token) + { + $payload = $this->setRefreshFlow()->decode($token); + + if ($this->blacklistEnabled) { + // invalidate old token + $this->blacklist->add($payload); + } + + // return the new token + return $this->encode( + $this->payloadFactory->make([ + 'sub' => $payload['sub'], + 'iat' => $payload['iat'], + ]) + ); + } + + /** + * Invalidate a Token by adding it to the blacklist. + * + * @param Token $token + * @return bool + */ + public function invalidate(Token $token) + { + if (! $this->blacklistEnabled) { + throw new JWTException('You must have the blacklist enabled to invalidate a token.'); + } + + return $this->blacklist->add($this->decode($token)); + } + + /** + * Get the PayloadFactory instance. + * + * @return \Tymon\JWTAuth\PayloadFactory + */ + public function getPayloadFactory() + { + return $this->payloadFactory; + } + + /** + * Get the JWTProvider instance. + * + * @return \Tymon\JWTAuth\Providers\JWT\JWTInterface + */ + public function getJWTProvider() + { + return $this->jwt; + } + + /** + * Get the Blacklist instance. + * + * @return \Tymon\JWTAuth\Blacklist + */ + public function getBlacklist() + { + return $this->blacklist; + } + + /** + * Set whether the blacklist is enabled. + * + * @param bool $enabled + */ + public function setBlacklistEnabled($enabled) + { + $this->blacklistEnabled = $enabled; + + return $this; + } + + /** + * Set the refresh flow. + * + * @param bool $refreshFlow + * @return $this + */ + public function setRefreshFlow($refreshFlow = true) + { + $this->refreshFlow = $refreshFlow; + + return $this; + } +} diff --git a/src/Middleware/BaseMiddleware.php b/src/Middleware/BaseMiddleware.php new file mode 100644 index 0000000..9715f0c --- /dev/null +++ b/src/Middleware/BaseMiddleware.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Middleware; + +use Tymon\JWTAuth\JWTAuth; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Routing\ResponseFactory; + +abstract class BaseMiddleware +{ + /** + * @var \Illuminate\Contracts\Routing\ResponseFactory + */ + protected $response; + + /** + * @var \Illuminate\Contracts\Events\Dispatcher + */ + protected $events; + + /** + * @var \Tymon\JWTAuth\JWTAuth + */ + protected $auth; + + /** + * Create a new BaseMiddleware instance. + * + * @param \Illuminate\Contracts\Routing\ResponseFactory $response + * @param \Illuminate\Contracts\Events\Dispatcher $events + * @param \Tymon\JWTAuth\JWTAuth $auth + */ + public function __construct(ResponseFactory $response, Dispatcher $events, JWTAuth $auth) + { + $this->response = $response; + $this->events = $events; + $this->auth = $auth; + } + + /** + * Fire event and return the response. + * + * @param string $event + * @param string $error + * @param int $status + * @param array $payload + * @return mixed + */ + protected function respond($event, $error, $status, $payload = []) + { + $response = $this->events->fire($event, $payload, true); + + return $response ?: $this->response->json(['error' => $error], $status); + } +} diff --git a/src/Middleware/GetUserFromToken.php b/src/Middleware/GetUserFromToken.php new file mode 100644 index 0000000..af3b21c --- /dev/null +++ b/src/Middleware/GetUserFromToken.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Middleware; + +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\TokenExpiredException; + +class GetUserFromToken extends BaseMiddleware +{ + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, \Closure $next) + { + if (! $token = $this->auth->setRequest($request)->getToken()) { + return $this->respond('tymon.jwt.absent', 'token_not_provided', 400); + } + + try { + $user = $this->auth->authenticate($token); + } catch (TokenExpiredException $e) { + return $this->respond('tymon.jwt.expired', 'token_expired', $e->getStatusCode(), [$e]); + } catch (JWTException $e) { + return $this->respond('tymon.jwt.invalid', 'token_invalid', $e->getStatusCode(), [$e]); + } + + if (! $user) { + return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 404); + } + + $this->events->fire('tymon.jwt.valid', $user); + + return $next($request); + } +} diff --git a/src/Middleware/RefreshToken.php b/src/Middleware/RefreshToken.php new file mode 100644 index 0000000..a54774c --- /dev/null +++ b/src/Middleware/RefreshToken.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Middleware; + +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\TokenExpiredException; + +class RefreshToken extends BaseMiddleware +{ + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, \Closure $next) + { + $response = $next($request); + + try { + $newToken = $this->auth->setRequest($request)->parseToken()->refresh(); + } catch (TokenExpiredException $e) { + return $this->respond('tymon.jwt.expired', 'token_expired', $e->getStatusCode(), [$e]); + } catch (JWTException $e) { + return $this->respond('tymon.jwt.invalid', 'token_invalid', $e->getStatusCode(), [$e]); + } + + // send the refreshed token back to the client + $response->headers->set('Authorization', 'Bearer '.$newToken); + + return $response; + } +} diff --git a/src/Payload.php b/src/Payload.php new file mode 100644 index 0000000..79cdb8d --- /dev/null +++ b/src/Payload.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth; + +use Tymon\JWTAuth\Claims\Claim; +use Tymon\JWTAuth\Exceptions\PayloadException; +use Tymon\JWTAuth\Validators\PayloadValidator; + +class Payload implements \ArrayAccess +{ + /** + * The array of claims. + * + * @var \Tymon\JWTAuth\Claims\Claim[] + */ + private $claims = []; + + /** + * Build the Payload. + * + * @param array $claims + * @param \Tymon\JWTAuth\Validators\PayloadValidator $validator + * @param bool $refreshFlow + */ + public function __construct(array $claims, PayloadValidator $validator, $refreshFlow = false) + { + $this->claims = $claims; + + $validator->setRefreshFlow($refreshFlow)->check($this->toArray()); + } + + /** + * Get the array of claim instances. + * + * @return \Tymon\JWTAuth\Claims\Claim[] + */ + public function getClaims() + { + return $this->claims; + } + + /** + * Get the array of claims. + * + * @return array + */ + public function toArray() + { + $results = []; + foreach ($this->claims as $claim) { + $results[$claim->getName()] = $claim->getValue(); + } + + return $results; + } + + /** + * Get the payload. + * + * @param string $claim + * @return mixed + */ + public function get($claim = null) + { + if (! is_null($claim)) { + if (is_array($claim)) { + return array_map([$this, 'get'], $claim); + } + + return array_get($this->toArray(), $claim, false); + } + + return $this->toArray(); + } + + /** + * Determine whether the payload has the claim. + * + * @param \Tymon\JWTAuth\Claims\Claim $claim + * @return bool + */ + public function has(Claim $claim) + { + return in_array($claim, $this->claims); + } + + /** + * Get the payload as a string. + * + * @return string + */ + public function __toString() + { + return json_encode($this->toArray()); + } + + /** + * Determine if an item exists at an offset. + * + * @param mixed $key + * @return bool + */ + public function offsetExists($key) + { + return array_key_exists($key, $this->toArray()); + } + + /** + * Get an item at a given offset. + * + * @param mixed $key + * @return mixed + */ + public function offsetGet($key) + { + return array_get($this->toArray(), $key, []); + } + + /** + * Don't allow changing the payload as it should be immutable. + * + * @param mixed $key + * @param mixed $value + * @throws Exceptions\PayloadException + * @return void + */ + public function offsetSet($key, $value) + { + throw new PayloadException('The payload is immutable'); + } + + /** + * Don't allow changing the payload as it should be immutable. + * + * @param string $key + * @throws Exceptions\PayloadException + * @return void + */ + public function offsetUnset($key) + { + throw new PayloadException('The payload is immutable'); + } + + /** + * Magically get a claim value. + * + * @param string $method + * @param array $parameters + * @return mixed + * @throws \BadMethodCallException + */ + public function __call($method, $parameters) + { + if (! method_exists($this, $method) && starts_with($method, 'get')) { + $class = sprintf('Tymon\\JWTAuth\\Claims\\%s', substr($method, 3)); + + foreach ($this->claims as $claim) { + if (get_class($claim) === $class) { + return $claim->getValue(); + } + } + } + + throw new \BadMethodCallException(sprintf('The claim [%s] does not exist on the payload.', $method)); + } +} diff --git a/src/PayloadFactory.php b/src/PayloadFactory.php new file mode 100644 index 0000000..352e413 --- /dev/null +++ b/src/PayloadFactory.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth; + +use Illuminate\Support\Str; +use Illuminate\Http\Request; +use Tymon\JWTAuth\Claims\Factory; +use Tymon\JWTAuth\Validators\PayloadValidator; + +class PayloadFactory +{ + /** + * @var \Tymon\JWTAuth\Claims\Factory + */ + protected $claimFactory; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * @var \Tymon\JWTAuth\Validators\PayloadValidator + */ + protected $validator; + + /** + * @var int + */ + protected $ttl = 60; + + /** + * @var bool + */ + protected $refreshFlow = false; + + /** + * @var array + */ + protected $defaultClaims = ['iss', 'iat', 'exp', 'nbf', 'jti']; + + /** + * @var array + */ + protected $claims = []; + + /** + * @param \Tymon\JWTAuth\Claims\Factory $claimFactory + * @param \Illuminate\Http\Request $request + * @param \Tymon\JWTAuth\Validators\PayloadValidator $validator + */ + public function __construct(Factory $claimFactory, Request $request, PayloadValidator $validator) + { + $this->claimFactory = $claimFactory; + $this->request = $request; + $this->validator = $validator; + } + + /** + * Create the Payload instance. + * + * @param array $customClaims + * @return \Tymon\JWTAuth\Payload + */ + public function make(array $customClaims = []) + { + $claims = $this->buildClaims($customClaims)->resolveClaims(); + + return new Payload($claims, $this->validator, $this->refreshFlow); + } + + /** + * Add an array of claims to the Payload. + * + * @param array $claims + * @return $this + */ + public function addClaims(array $claims) + { + foreach ($claims as $name => $value) { + $this->addClaim($name, $value); + } + + return $this; + } + + /** + * Add a claim to the Payload. + * + * @param string $name + * @param mixed $value + * @return $this + */ + public function addClaim($name, $value) + { + $this->claims[$name] = $value; + + return $this; + } + + /** + * Build the default claims. + * + * @param array $customClaims + * @return $this + */ + protected function buildClaims(array $customClaims) + { + // add any custom claims first + $this->addClaims($customClaims); + + foreach ($this->defaultClaims as $claim) { + if (! array_key_exists($claim, $customClaims)) { + $this->addClaim($claim, $this->$claim()); + } + } + + return $this; + } + + /** + * Build out the Claim DTO's. + * + * @return array + */ + public function resolveClaims() + { + $resolved = []; + foreach ($this->claims as $name => $value) { + $resolved[] = $this->claimFactory->get($name, $value); + } + + return $resolved; + } + + /** + * Set the Issuer (iss) claim. + * + * @return string + */ + public function iss() + { + return $this->request->url(); + } + + /** + * Set the Issued At (iat) claim. + * + * @return int + */ + public function iat() + { + return Utils::now()->timestamp; + } + + /** + * Set the Expiration (exp) claim. + * + * @return int + */ + public function exp() + { + return Utils::now()->addMinutes($this->ttl)->timestamp; + } + + /** + * Set the Not Before (nbf) claim. + * + * @return int + */ + public function nbf() + { + return Utils::now()->timestamp; + } + + /** + * Set a unique id (jti) for the token. + * + * @return string + */ + protected function jti() + { + return Str::random(); + } + + /** + * Set the token ttl (in minutes). + * + * @param int $ttl + * @return $this + */ + public function setTTL($ttl) + { + $this->ttl = $ttl; + + return $this; + } + + /** + * Get the token ttl. + * + * @return int + */ + public function getTTL() + { + return $this->ttl; + } + + /** + * Set the refresh flow. + * + * @param bool $refreshFlow + * @return $this + */ + public function setRefreshFlow($refreshFlow = true) + { + $this->refreshFlow = $refreshFlow; + + return $this; + } + + /** + * Magically add a claim. + * + * @param string $method + * @param array $parameters + * @return PayloadFactory + * @throws \BadMethodCallException + */ + public function __call($method, $parameters) + { + $this->addClaim($method, $parameters[0]); + + return $this; + } +} diff --git a/src/Providers/Auth/AuthInterface.php b/src/Providers/Auth/AuthInterface.php new file mode 100644 index 0000000..f6f1dd7 --- /dev/null +++ b/src/Providers/Auth/AuthInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\Auth; + +interface AuthInterface +{ + /** + * Check a user's credentials. + * + * @param array $credentials + * @return bool + */ + public function byCredentials(array $credentials = []); + + /** + * Authenticate a user via the id. + * + * @param mixed $id + * @return bool + */ + public function byId($id); + + /** + * Get the currently authenticated user. + * + * @return mixed + */ + public function user(); +} diff --git a/src/Providers/Auth/IlluminateAuthAdapter.php b/src/Providers/Auth/IlluminateAuthAdapter.php new file mode 100644 index 0000000..4eaccbd --- /dev/null +++ b/src/Providers/Auth/IlluminateAuthAdapter.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\Auth; + +use Illuminate\Auth\AuthManager; + +class IlluminateAuthAdapter implements AuthInterface +{ + /** + * @var \Illuminate\Auth\AuthManager + */ + protected $auth; + + /** + * @param \Illuminate\Auth\AuthManager $auth + */ + public function __construct(AuthManager $auth) + { + $this->auth = $auth; + } + + /** + * Check a user's credentials. + * + * @param array $credentials + * @return bool + */ + public function byCredentials(array $credentials = []) + { + return $this->auth->once($credentials); + } + + /** + * Authenticate a user via the id. + * + * @param mixed $id + * @return bool + */ + public function byId($id) + { + return $this->auth->onceUsingId($id); + } + + /** + * Get the currently authenticated user. + * + * @return mixed + */ + public function user() + { + return $this->auth->user(); + } +} diff --git a/src/Providers/JWT/JWTInterface.php b/src/Providers/JWT/JWTInterface.php new file mode 100644 index 0000000..0852923 --- /dev/null +++ b/src/Providers/JWT/JWTInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\JWT; + +interface JWTInterface +{ + /** + * @param array $payload + * @return string + */ + public function encode(array $payload); + + /** + * @param string $token + * @return array + */ + public function decode($token); +} diff --git a/src/Providers/JWT/JWTProvider.php b/src/Providers/JWT/JWTProvider.php new file mode 100644 index 0000000..e15e848 --- /dev/null +++ b/src/Providers/JWT/JWTProvider.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\JWT; + +abstract class JWTProvider +{ + /** + * @var string + */ + protected $secret; + + /** + * @var string + */ + protected $algo; + + /** + * @param string $secret + * @param string $algo + */ + public function __construct($secret, $algo = 'HS256') + { + $this->secret = $secret; + $this->algo = $algo; + } + + /** + * Set the algorithm used to sign the token. + * + * @param string $algo + * @return self + */ + public function setAlgo($algo) + { + $this->algo = $algo; + + return $this; + } + + /** + * Get the algorithm used to sign the token. + * + * @return string + */ + public function getAlgo() + { + return $this->algo; + } + + /** + * Set the secret used to sign the token. + * + * @param string $secret + * + * @return $this + */ + public function setSecret($secret) + { + $this->secret = $secret; + + return $this; + } + + /** + * Get the secret used to sign the token. + * + * @return string + */ + public function getSecret() + { + return $this->secret; + } +} diff --git a/src/Providers/JWT/NamshiAdapter.php b/src/Providers/JWT/NamshiAdapter.php new file mode 100644 index 0000000..e291474 --- /dev/null +++ b/src/Providers/JWT/NamshiAdapter.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\JWT; + +use Exception; +use Namshi\JOSE\JWS; +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; + +class NamshiAdapter extends JWTProvider implements JWTInterface +{ + /** + * @var \Namshi\JOSE\JWS + */ + protected $jws; + + /** + * @param string $secret + * @param string $algo + * @param null $driver + */ + public function __construct($secret, $algo, $driver = null) + { + parent::__construct($secret, $algo); + + $this->jws = $driver ?: new JWS(['typ' => 'JWT', 'alg' => $algo]); + } + + /** + * Create a JSON Web Token. + * + * @return string + * @throws \Tymon\JWTAuth\Exceptions\JWTException + */ + public function encode(array $payload) + { + try { + $this->jws->setPayload($payload)->sign($this->secret); + + return $this->jws->getTokenString(); + } catch (Exception $e) { + throw new JWTException('Could not create token: '.$e->getMessage()); + } + } + + /** + * Decode a JSON Web Token. + * + * @param string $token + * @return array + * @throws \Tymon\JWTAuth\Exceptions\JWTException + */ + public function decode($token) + { + try { + $jws = JWS::load($token); + } catch (Exception $e) { + throw new TokenInvalidException('Could not decode token: '.$e->getMessage()); + } + + if (! $jws->verify($this->secret, $this->algo)) { + throw new TokenInvalidException('Token Signature could not be verified.'); + } + + return $jws->getPayload(); + } +} diff --git a/src/Providers/JWTAuthServiceProvider.php b/src/Providers/JWTAuthServiceProvider.php new file mode 100644 index 0000000..6bb4365 --- /dev/null +++ b/src/Providers/JWTAuthServiceProvider.php @@ -0,0 +1,278 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers; + +use Tymon\JWTAuth\JWTAuth; +use Tymon\JWTAuth\Blacklist; +use Tymon\JWTAuth\JWTManager; +use Tymon\JWTAuth\Claims\Factory; +use Tymon\JWTAuth\PayloadFactory; +use Illuminate\Support\ServiceProvider; +use Tymon\JWTAuth\Commands\JWTGenerateCommand; +use Tymon\JWTAuth\Validators\PayloadValidator; + +class JWTAuthServiceProvider extends ServiceProvider +{ + /** + * Indicates if loading of the provider is deferred. + * + * @var bool + */ + protected $defer = false; + + /** + * Boot the service provider. + */ + public function boot() + { + $this->publishes([ + __DIR__.'/../config/config.php' => config_path('jwt.php'), + ], 'config'); + + $this->bootBindings(); + + $this->commands('tymon.jwt.generate'); + } + + /** + * Bind some Interfaces and implementations. + */ + protected function bootBindings() + { + $this->app->singleton('Tymon\JWTAuth\JWTAuth', function ($app) { + return $app['tymon.jwt.auth']; + }); + + $this->app->singleton('Tymon\JWTAuth\Providers\User\UserInterface', function ($app) { + return $app['tymon.jwt.provider.user']; + }); + + $this->app->singleton('Tymon\JWTAuth\Providers\JWT\JWTInterface', function ($app) { + return $app['tymon.jwt.provider.jwt']; + }); + + $this->app->singleton('Tymon\JWTAuth\Providers\Auth\AuthInterface', function ($app) { + return $app['tymon.jwt.provider.auth']; + }); + + $this->app->singleton('Tymon\JWTAuth\Providers\Storage\StorageInterface', function ($app) { + return $app['tymon.jwt.provider.storage']; + }); + + $this->app->singleton('Tymon\JWTAuth\JWTManager', function ($app) { + return $app['tymon.jwt.manager']; + }); + + $this->app->singleton('Tymon\JWTAuth\Blacklist', function ($app) { + return $app['tymon.jwt.blacklist']; + }); + + $this->app->singleton('Tymon\JWTAuth\PayloadFactory', function ($app) { + return $app['tymon.jwt.payload.factory']; + }); + + $this->app->singleton('Tymon\JWTAuth\Claims\Factory', function ($app) { + return $app['tymon.jwt.claim.factory']; + }); + + $this->app->singleton('Tymon\JWTAuth\Validators\PayloadValidator', function ($app) { + return $app['tymon.jwt.validators.payload']; + }); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + // register providers + $this->registerUserProvider(); + $this->registerJWTProvider(); + $this->registerAuthProvider(); + $this->registerStorageProvider(); + $this->registerJWTBlacklist(); + + $this->registerClaimFactory(); + $this->registerJWTManager(); + + $this->registerJWTAuth(); + $this->registerPayloadValidator(); + $this->registerPayloadFactory(); + $this->registerJWTCommand(); + + $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'jwt'); + } + + /** + * Register the bindings for the User provider. + */ + protected function registerUserProvider() + { + $this->app->singleton('tymon.jwt.provider.user', function ($app) { + $provider = $this->config('providers.user'); + $model = $app->make($this->config('user')); + + return new $provider($model); + }); + } + + /** + * Register the bindings for the JSON Web Token provider. + */ + protected function registerJWTProvider() + { + $this->app->singleton('tymon.jwt.provider.jwt', function ($app) { + $secret = $this->config('secret'); + $algo = $this->config('algo'); + $provider = $this->config('providers.jwt'); + + return new $provider($secret, $algo); + }); + } + + /** + * Register the bindings for the Auth provider. + */ + protected function registerAuthProvider() + { + $this->app->singleton('tymon.jwt.provider.auth', function ($app) { + return $this->getConfigInstance($this->config('providers.auth')); + }); + } + + /** + * Register the bindings for the Storage provider. + */ + protected function registerStorageProvider() + { + $this->app->singleton('tymon.jwt.provider.storage', function ($app) { + return $this->getConfigInstance($this->config('providers.storage')); + }); + } + + /** + * Register the bindings for the Payload Factory. + */ + protected function registerClaimFactory() + { + $this->app->singleton('tymon.jwt.claim.factory', function () { + return new Factory(); + }); + } + + /** + * Register the bindings for the JWT Manager. + */ + protected function registerJWTManager() + { + $this->app->singleton('tymon.jwt.manager', function ($app) { + $instance = new JWTManager( + $app['tymon.jwt.provider.jwt'], + $app['tymon.jwt.blacklist'], + $app['tymon.jwt.payload.factory'] + ); + + return $instance->setBlacklistEnabled((bool) $this->config('blacklist_enabled')); + }); + } + + /** + * Register the bindings for the main JWTAuth class. + */ + protected function registerJWTAuth() + { + $this->app->singleton('tymon.jwt.auth', function ($app) { + $auth = new JWTAuth( + $app['tymon.jwt.manager'], + $app['tymon.jwt.provider.user'], + $app['tymon.jwt.provider.auth'], + $app['request'] + ); + + return $auth->setIdentifier($this->config('identifier')); + }); + } + + /** + * Register the bindings for the main JWTAuth class. + */ + protected function registerJWTBlacklist() + { + $this->app->singleton('tymon.jwt.blacklist', function ($app) { + $instance = new Blacklist($app['tymon.jwt.provider.storage']); + + return $instance->setRefreshTTL($this->config('refresh_ttl')); + }); + } + + /** + * Register the bindings for the payload validator. + */ + protected function registerPayloadValidator() + { + $this->app->singleton('tymon.jwt.validators.payload', function () { + return with(new PayloadValidator())->setRefreshTTL($this->config('refresh_ttl'))->setRequiredClaims($this->config('required_claims')); + }); + } + + /** + * Register the bindings for the Payload Factory. + */ + protected function registerPayloadFactory() + { + $this->app->singleton('tymon.jwt.payload.factory', function ($app) { + $factory = new PayloadFactory($app['tymon.jwt.claim.factory'], $app['request'], $app['tymon.jwt.validators.payload']); + + return $factory->setTTL($this->config('ttl')); + }); + } + + /** + * Register the Artisan command. + */ + protected function registerJWTCommand() + { + $this->app->singleton('tymon.jwt.generate', function () { + return new JWTGenerateCommand(); + }); + } + + /** + * Helper to get the config values. + * + * @param string $key + * @return string + */ + protected function config($key, $default = null) + { + return config("jwt.$key", $default); + } + + /** + * Get an instantiable configuration instance. Pinched from dingo/api :). + * + * @param mixed $instance + * @return object + */ + protected function getConfigInstance($instance) + { + if (is_callable($instance)) { + return call_user_func($instance, $this->app); + } elseif (is_string($instance)) { + return $this->app->make($instance); + } + + return $instance; + } +} diff --git a/src/Providers/Storage/IlluminateCacheAdapter.php b/src/Providers/Storage/IlluminateCacheAdapter.php new file mode 100644 index 0000000..3abdee5 --- /dev/null +++ b/src/Providers/Storage/IlluminateCacheAdapter.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\Storage; + +use Illuminate\Cache\CacheManager; + +class IlluminateCacheAdapter implements StorageInterface +{ + /** + * @var \Illuminate\Cache\CacheManager + */ + protected $cache; + + /** + * @var string + */ + protected $tag = 'tymon.jwt'; + + /** + * @param \Illuminate\Cache\CacheManager $cache + */ + public function __construct(CacheManager $cache) + { + $this->cache = $cache; + } + + /** + * Add a new item into storage. + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes) + { + $this->cache()->put($key, $value, $minutes); + } + + /** + * Check whether a key exists in storage. + * + * @param string $key + * @return bool + */ + public function has($key) + { + return $this->cache()->has($key); + } + + /** + * Remove an item from storage. + * + * @param string $key + * @return bool + */ + public function destroy($key) + { + return $this->cache()->forget($key); + } + + /** + * Remove all items associated with the tag. + * + * @return void + */ + public function flush() + { + $this->cache()->flush(); + } + + /** + * Return the cache instance with tags attached. + * + * @return \Illuminate\Cache\CacheManager + */ + protected function cache() + { + if (! method_exists($this->cache, 'tags')) { + return $this->cache; + } + + return $this->cache->tags($this->tag); + } +} diff --git a/src/Providers/Storage/StorageInterface.php b/src/Providers/Storage/StorageInterface.php new file mode 100644 index 0000000..7468ed7 --- /dev/null +++ b/src/Providers/Storage/StorageInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\Storage; + +interface StorageInterface +{ + /** + * @param string $key + * @param int $minutes + * @return void + */ + public function add($key, $value, $minutes); + + /** + * @param string $key + * @return bool + */ + public function has($key); + + /** + * @param string $key + * @return bool + */ + public function destroy($key); + + /** + * @return void + */ + public function flush(); +} diff --git a/src/Providers/User/EloquentUserAdapter.php b/src/Providers/User/EloquentUserAdapter.php new file mode 100644 index 0000000..63e80cc --- /dev/null +++ b/src/Providers/User/EloquentUserAdapter.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\User; + +use Illuminate\Database\Eloquent\Model; + +class EloquentUserAdapter implements UserInterface +{ + /** + * @var \Illuminate\Database\Eloquent\Model + */ + protected $user; + + /** + * Create a new User instance. + * + * @param \Illuminate\Database\Eloquent\Model $user + */ + public function __construct(Model $user) + { + $this->user = $user; + } + + /** + * Get the user by the given key, value. + * + * @param mixed $key + * @param mixed $value + * @return Illuminate\Database\Eloquent\Model + */ + public function getBy($key, $value) + { + return $this->user->where($key, $value)->first(); + } +} diff --git a/src/Providers/User/UserInterface.php b/src/Providers/User/UserInterface.php new file mode 100644 index 0000000..6a73e3f --- /dev/null +++ b/src/Providers/User/UserInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Providers\User; + +interface UserInterface +{ + /** + * Get the user by the given key, value. + * + * @param string $key + * @param mixed $value + * @return Illuminate\Database\Eloquent\Model|null + */ + public function getBy($key, $value); +} diff --git a/src/Token.php b/src/Token.php new file mode 100644 index 0000000..e451aad --- /dev/null +++ b/src/Token.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth; + +use Tymon\JWTAuth\Validators\TokenValidator; + +class Token +{ + /** + * @var string + */ + private $value; + + /** + * Create a new JSON Web Token. + * + * @param string $value + */ + public function __construct($value) + { + with(new TokenValidator)->check($value); + + $this->value = $value; + } + + /** + * Get the token. + * + * @return string + */ + public function get() + { + return $this->value; + } + + /** + * Get the token when casting to string. + * + * @return string + */ + public function __toString() + { + return (string) $this->value; + } +} diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..9469781 --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth; + +use Carbon\Carbon; + +class Utils +{ + /** + * Get the Carbon instance for the current time. + * + * @return \Carbon\Carbon + */ + public static function now() + { + return Carbon::now(); + } + + /** + * Get the Carbon instance for the timestamp. + * + * @param int $timestamp + * @return \Carbon\Carbon + */ + public static function timestamp($timestamp) + { + return Carbon::createFromTimeStampUTC($timestamp); + } +} diff --git a/src/Validators/AbstractValidator.php b/src/Validators/AbstractValidator.php new file mode 100644 index 0000000..5a3ad28 --- /dev/null +++ b/src/Validators/AbstractValidator.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Validators; + +use Tymon\JWTAuth\Exceptions\JWTException; + +abstract class AbstractValidator implements ValidatorInterface +{ + /** + * @var bool + */ + protected $refreshFlow = false; + + /** + * Helper function to return a boolean. + * + * @param array $value + * @return bool + */ + public function isValid($value) + { + try { + $this->check($value); + } catch (JWTException $e) { + return false; + } + + return true; + } + + /** + * Set the refresh flow flag. + * + * @param bool $refreshFlow + * @return $this + */ + public function setRefreshFlow($refreshFlow = true) + { + $this->refreshFlow = $refreshFlow; + + return $this; + } +} diff --git a/src/Validators/PayloadValidator.php b/src/Validators/PayloadValidator.php new file mode 100644 index 0000000..80e1a86 --- /dev/null +++ b/src/Validators/PayloadValidator.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Validators; + +use Tymon\JWTAuth\Utils; +use Tymon\JWTAuth\Exceptions\TokenExpiredException; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; + +class PayloadValidator extends AbstractValidator +{ + /** + * @var array + */ + protected $requiredClaims = ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti']; + + /** + * @var int + */ + protected $refreshTTL = 20160; + + /** + * Run the validations on the payload array. + * + * @param array $value + * @return void + */ + public function check($value) + { + $this->validateStructure($value); + + if (! $this->refreshFlow) { + $this->validateTimestamps($value); + } else { + $this->validateRefresh($value); + } + } + + /** + * Ensure the payload contains the required claims and + * the claims have the relevant type. + * + * @param array $payload + * @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException + * @return bool + */ + protected function validateStructure(array $payload) + { + if (count(array_diff($this->requiredClaims, array_keys($payload))) !== 0) { + throw new TokenInvalidException('JWT payload does not contain the required claims'); + } + + return true; + } + + /** + * Validate the payload timestamps. + * + * @param array $payload + * @throws \Tymon\JWTAuth\Exceptions\TokenExpiredException + * @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException + * @return bool + */ + protected function validateTimestamps(array $payload) + { + if (isset($payload['nbf']) && Utils::timestamp($payload['nbf'])->isFuture()) { + throw new TokenInvalidException('Not Before (nbf) timestamp cannot be in the future', 400); + } + + if (isset($payload['iat']) && Utils::timestamp($payload['iat'])->isFuture()) { + throw new TokenInvalidException('Issued At (iat) timestamp cannot be in the future', 400); + } + + if (Utils::timestamp($payload['exp'])->isPast()) { + throw new TokenExpiredException('Token has expired'); + } + + return true; + } + + /** + * Check the token in the refresh flow context. + * + * @param $payload + * @return bool + */ + protected function validateRefresh(array $payload) + { + if (isset($payload['iat']) && Utils::timestamp($payload['iat'])->addMinutes($this->refreshTTL)->isPast()) { + throw new TokenExpiredException('Token has expired and can no longer be refreshed', 400); + } + + return true; + } + + /** + * Set the required claims. + * + * @param array $claims + */ + public function setRequiredClaims(array $claims) + { + $this->requiredClaims = $claims; + + return $this; + } + + /** + * Set the refresh ttl. + * + * @param int $ttl + */ + public function setRefreshTTL($ttl) + { + $this->refreshTTL = $ttl; + + return $this; + } +} diff --git a/src/Validators/TokenValidator.php b/src/Validators/TokenValidator.php new file mode 100644 index 0000000..f68e195 --- /dev/null +++ b/src/Validators/TokenValidator.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Validators; + +use Tymon\JWTAuth\Exceptions\TokenInvalidException; + +class TokenValidator extends AbstractValidator +{ + /** + * Check the structure of the token. + * + * @param string $value + * @return void + */ + public function check($value) + { + $this->validateStructure($value); + } + + /** + * @param string $token + * @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException + * @return bool + */ + protected function validateStructure($token) + { + if (count(explode('.', $token)) !== 3) { + throw new TokenInvalidException('Wrong number of segments'); + } + + return true; + } +} diff --git a/src/Validators/ValidatorInterface.php b/src/Validators/ValidatorInterface.php new file mode 100644 index 0000000..87d5dc0 --- /dev/null +++ b/src/Validators/ValidatorInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Validators; + +interface ValidatorInterface +{ + /** + * Perform some checks on the value. + * + * @param mixed $value + * @return void + */ + public function check($value); + + /** + * Helper function to return a boolean. + * + * @param array $value + * @return bool + */ + public function isValid($value); +} diff --git a/src/config/config.php b/src/config/config.php new file mode 100644 index 0000000..b12ac03 --- /dev/null +++ b/src/config/config.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + + /* + |-------------------------------------------------------------------------- + | JWT Authentication Secret + |-------------------------------------------------------------------------- + | + | Don't forget to set this, as it will be used to sign your tokens. + | A helper command is provided for this: `php artisan jwt:generate` + | + */ + + 'secret' => env('JWT_SECRET', 'changeme'), + + /* + |-------------------------------------------------------------------------- + | JWT time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token will be valid for. + | Defaults to 1 hour + | + */ + + 'ttl' => 60, + + /* + |-------------------------------------------------------------------------- + | Refresh time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token can be refreshed + | within. I.E. The user can refresh their token within a 2 week window of + | the original token being created until they must re-authenticate. + | Defaults to 2 weeks + | + */ + + 'refresh_ttl' => 20160, + + /* + |-------------------------------------------------------------------------- + | JWT hashing algorithm + |-------------------------------------------------------------------------- + | + | Specify the hashing algorithm that will be used to sign the token. + | + | See here: https://github.com/namshi/jose/tree/2.2.0/src/Namshi/JOSE/Signer + | for possible values + | + */ + + 'algo' => 'HS256', + + /* + |-------------------------------------------------------------------------- + | User Model namespace + |-------------------------------------------------------------------------- + | + | Specify the full namespace to your User model. + | e.g. 'Acme\Entities\User' + | + */ + + 'user' => 'App\User', + + /* + |-------------------------------------------------------------------------- + | User identifier + |-------------------------------------------------------------------------- + | + | Specify a unique property of the user that will be added as the 'sub' + | claim of the token payload. + | + */ + + 'identifier' => 'id', + + /* + |-------------------------------------------------------------------------- + | Required Claims + |-------------------------------------------------------------------------- + | + | Specify the required claims that must exist in any token. + | A TokenInvalidException will be thrown if any of these claims are not + | present in the payload. + | + */ + + 'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti'], + + /* + |-------------------------------------------------------------------------- + | Blacklist Enabled + |-------------------------------------------------------------------------- + | + | In order to invalidate tokens, you must have the blacklist enabled. + | If you do not want or need this functionality, then set this to false. + | + */ + + 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Providers + |-------------------------------------------------------------------------- + | + | Specify the various providers used throughout the package. + | + */ + + 'providers' => [ + + /* + |-------------------------------------------------------------------------- + | User Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to find the user based + | on the subject claim + | + */ + + 'user' => 'Tymon\JWTAuth\Providers\User\EloquentUserAdapter', + + /* + |-------------------------------------------------------------------------- + | JWT Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to create and decode the tokens. + | + */ + + 'jwt' => 'Tymon\JWTAuth\Providers\JWT\NamshiAdapter', + + /* + |-------------------------------------------------------------------------- + | Authentication Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to authenticate users. + | + */ + + 'auth' => 'Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter', + + /* + |-------------------------------------------------------------------------- + | Storage Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to store tokens in the blacklist + | + */ + + 'storage' => 'Tymon\JWTAuth\Providers\Storage\IlluminateCacheAdapter', + + ], + +]; diff --git a/tests/BlacklistTest.php b/tests/BlacklistTest.php new file mode 100644 index 0000000..0f00881 --- /dev/null +++ b/tests/BlacklistTest.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\JWT; + +use Mockery; +use Carbon\Carbon; +use Tymon\JWTAuth\Payload; +use Tymon\JWTAuth\Blacklist; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Expiration; + +class BlacklistTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + Carbon::setTestNow(Carbon::createFromTimeStampUTC(123)); + + $this->storage = Mockery::mock('Tymon\JWTAuth\Providers\Storage\StorageInterface'); + $this->blacklist = new Blacklist($this->storage); + $this->blacklist->setRefreshTTL(20160); + + $this->validator = Mockery::mock('Tymon\JWTAuth\Validators\PayloadValidator'); + $this->validator->shouldReceive('setRefreshFlow->check'); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_add_a_valid_token_to_the_blacklist() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(100 + 3600), + new NotBefore(100), + new IssuedAt(100), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator); + + $this->storage->shouldReceive('add')->once()->with('foo', [], 20160); + $this->assertTrue($this->blacklist->add($payload)); + } + + /** @test */ + public function it_should_return_true_when_adding_a_refreshable_expired_token_to_the_blacklist() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(101), + new NotBefore(100), + new IssuedAt(100), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator, true); + + $this->storage->shouldReceive('add')->once()->with('foo', [], 20160); + $this->assertTrue($this->blacklist->add($payload)); + } + + /** @test */ + public function it_should_return_false_when_adding_an_unrefreshable_token_to_the_blacklist() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(100), // default refresh_ttl + new NotBefore(100), + new IssuedAt(100 - 20160 * 60), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator, true); + + $this->storage->shouldReceive('add')->never(); + $this->assertFalse($this->blacklist->add($payload)); + } + + /** @test */ + public function it_should_return_false_when_adding_a_unrefreshable_token_after_modifying_refresh_ttl() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(101), + new NotBefore(100), + new IssuedAt(100), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator, true); + + $this->storage->shouldReceive('add')->never(); + $this->blacklist->setRefreshTTL(0); + $this->assertFalse($this->blacklist->add($payload)); + } + + /** @test */ + public function it_should_check_whether_a_token_has_been_blacklisted() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 + 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foobar'), + ]; + $payload = new Payload($claims, $this->validator); + + $this->storage->shouldReceive('has')->once()->with('foobar')->andReturn(true); + $this->assertTrue($this->blacklist->has($payload)); + } + + /** @test */ + public function it_should_remove_a_token_from_the_blacklist() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 + 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foobar'), + ]; + $payload = new Payload($claims, $this->validator); + + $this->storage->shouldReceive('destroy')->once()->with('foobar')->andReturn(true); + $this->assertTrue($this->blacklist->remove($payload)); + } + + /** @test */ + public function it_should_empty_the_blacklist() + { + $this->storage->shouldReceive('flush')->once(); + $this->assertTrue($this->blacklist->clear()); + } +} diff --git a/tests/Commands/JWTGenerateCommandTest.php b/tests/Commands/JWTGenerateCommandTest.php new file mode 100644 index 0000000..a4a11ac --- /dev/null +++ b/tests/Commands/JWTGenerateCommandTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test; + +use Illuminate\Foundation\Application; +use Tymon\JWTAuth\Commands\JWTGenerateCommand; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Tester\CommandTester; + +class JWTGenerateCommandTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->command = new JWTGenerateCommand(); + $this->tester = new CommandTester($this->command); + } + + /** @test */ + public function it_shoud_generate_random_key() + { + // $app = new Application(); + + // $app['path.base'] = ''; + + // $this->command->setLaravel($app); + + // $this->runCommand($this->command); + } + + protected function runCommand($command, $input = []) + { + return $command->run(new ArrayInput($input), new NullOutput); + } +} diff --git a/tests/JWTAuthTest.php b/tests/JWTAuthTest.php new file mode 100644 index 0000000..3f910d4 --- /dev/null +++ b/tests/JWTAuthTest.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test; + +use Mockery; +use Tymon\JWTAuth\Token; +use Tymon\JWTAuth\JWTAuth; +use Illuminate\Http\Request; + +class JWTAuthTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->user = Mockery::mock('Tymon\JWTAuth\Providers\User\UserInterface'); + $this->manager = Mockery::mock('Tymon\JWTAuth\JWTManager'); + $this->auth = Mockery::mock('Tymon\JWTAuth\Providers\Auth\AuthInterface'); + + $this->jwtAuth = new JWTAuth($this->manager, $this->user, $this->auth, Request::create('/foo', 'GET')); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_return_a_user_when_passing_a_token_containing_a_valid_subject_claim() + { + $payload = Mockery::mock('Tymon\JWTAuth\Payload'); + $payload->shouldReceive('offsetGet')->once()->andReturn(1); + + $this->manager->shouldReceive('decode')->once()->andReturn($payload); + $this->user->shouldReceive('getBy')->once()->andReturn((object) ['id' => 1]); + + $user = $this->jwtAuth->toUser('foo.bar.baz'); + + $this->assertEquals(1, $user->id); + } + + /** @test */ + public function it_should_return_false_when_passing_a_token_containing_an_invalid_subject_claim() + { + $payload = Mockery::mock('Tymon\JWTAuth\Payload'); + $payload->shouldReceive('offsetGet')->once()->andReturn(1); + + $this->manager->shouldReceive('decode')->once()->andReturn($payload); + $this->user->shouldReceive('getBy')->once()->andReturn(false); + + $user = $this->jwtAuth->toUser('foo.bar.baz'); + + $this->assertFalse($user); + } + + /** @test */ + public function it_should_return_a_token_when_passing_a_user() + { + $this->manager->shouldReceive('getPayloadFactory->make')->once()->andReturn(Mockery::mock('Tymon\JWTAuth\Payload')); + $this->manager->shouldReceive('encode->get')->once()->andReturn('foo.bar.baz'); + + $token = $this->jwtAuth->fromUser((object) ['id' => 1]); + + $this->assertEquals($token, 'foo.bar.baz'); + } + + /** @test */ + public function it_should_return_a_token_when_passing_valid_credentials_to_attempt_method() + { + $this->manager->shouldReceive('getPayloadFactory->make')->once()->andReturn(Mockery::mock('Tymon\JWTAuth\Payload')); + $this->manager->shouldReceive('encode->get')->once()->andReturn('foo.bar.baz'); + + $this->auth->shouldReceive('byCredentials')->once()->andReturn(true); + $this->auth->shouldReceive('user')->once()->andReturn((object) ['id' => 1]); + + $token = $this->jwtAuth->attempt(); + + $this->assertEquals($token, 'foo.bar.baz'); + } + + /** @test */ + public function it_should_return_false_when_passing_invalid_credentials_to_attempt_method() + { + $this->manager->shouldReceive('encode->get')->never(); + $this->auth->shouldReceive('byCredentials')->once()->andReturn(false); + $this->auth->shouldReceive('user')->never(); + + $token = $this->jwtAuth->attempt(); + + $this->assertFalse($token); + } + + /** @test */ + public function it_should_throw_an_exception_when_not_providing_a_token() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\JWTException'); + + $this->jwtAuth->toUser(); + } + + /** @test */ + public function it_should_return_the_owning_user_from_a_token_containing_an_existing_user() + { + $payload = Mockery::mock('Tymon\JWTAuth\Payload'); + $payload->shouldReceive('get')->once()->with('sub')->andReturn(1); + + $this->manager->shouldReceive('decode')->once()->andReturn($payload); + + $this->auth->shouldReceive('byId')->once()->with(1)->andReturn(true); + $this->auth->shouldReceive('user')->once()->andReturn((object) ['id' => 1]); + + $user = $this->jwtAuth->authenticate('foo.bar.baz'); + + $this->assertEquals($user->id, 1); + } + + /** @test */ + public function it_should_return_false_when_passing_a_token_not_containing_an_existing_user() + { + $payload = Mockery::mock('Tymon\JWTAuth\Payload'); + $payload->shouldReceive('get')->once()->with('sub')->andReturn(1); + + $this->manager->shouldReceive('decode')->once()->andReturn($payload); + + $this->auth->shouldReceive('byId')->once()->with(1)->andReturn(false); + $this->auth->shouldReceive('user')->never(); + + $user = $this->jwtAuth->authenticate('foo.bar.baz'); + + $this->assertFalse($user); + } + + /** @test */ + public function it_should_refresh_a_token() + { + $newToken = Mockery::mock('Tymon\JWTAuth\Token'); + $newToken->shouldReceive('get')->once()->andReturn('baz.bar.foo'); + + $this->manager->shouldReceive('refresh')->once()->andReturn($newToken); + + $result = $this->jwtAuth->setToken('foo.bar.baz')->refresh(); + + $this->assertEquals($result, 'baz.bar.foo'); + } + + /** @test */ + public function it_should_invalidate_a_token() + { + $this->manager->shouldReceive('invalidate')->once()->andReturn(true); + + $result = $this->jwtAuth->invalidate('foo.bar.baz'); + + $this->assertTrue($result); + } + + /** @test */ + public function it_should_retrieve_the_token_from_the_auth_header() + { + $request = Request::create('/foo', 'GET'); + $request->headers->set('authorization', 'Bearer foo.bar.baz'); + $jwtAuth = new JWTAuth($this->manager, $this->user, $this->auth, $request); + + $this->assertInstanceOf('Tymon\JWTAuth\Token', $jwtAuth->parseToken()->getToken()); + $this->assertEquals($jwtAuth->getToken(), 'foo.bar.baz'); + } + + /** @test */ + public function it_should_retrieve_the_token_from_the_query_string() + { + $request = Request::create('/foo', 'GET', ['token' => 'foo.bar.baz']); + $jwtAuth = new JWTAuth($this->manager, $this->user, $this->auth, $request); + + $this->assertInstanceOf('Tymon\JWTAuth\Token', $jwtAuth->parseToken()->getToken()); + $this->assertEquals($jwtAuth->getToken(), 'foo.bar.baz'); + } + + /** @test */ + public function it_should_throw_an_exception_when_token_not_present_in_request() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\JWTException'); + + $request = Request::create('/foo', 'GET'); + $jwtAuth = new JWTAuth($this->manager, $this->user, $this->auth, $request); + + $jwtAuth->parseToken(); + } + + /** @test */ + public function it_should_return_false_when_no_token_is_set() + { + $this->assertFalse($this->jwtAuth->getToken()); + } + + /** @test */ + public function it_should_set_the_identifier() + { + $this->jwtAuth->setIdentifier('foo'); + + $this->assertEquals($this->jwtAuth->getIdentifier(), 'foo'); + } + + /** @test */ + public function it_should_magically_call_the_manager() + { + $this->manager->shouldReceive('getBlacklist')->andReturn(new \StdClass); + + $blacklist = $this->jwtAuth->getBlacklist(); + + $this->assertInstanceOf('StdClass', $blacklist); + } + + /** @test */ + public function it_should_set_the_request() + { + $request = Request::create('/foo', 'GET', ['token' => 'some.random.token']); + + $token = $this->jwtAuth->setRequest($request)->getToken(); + + $this->assertEquals('some.random.token', $token); + } + + /** @test */ + public function it_should_get_the_manager_instance() + { + $manager = $this->jwtAuth->manager(); + $this->assertInstanceOf('Tymon\JWTAuth\JWTManager', $manager); + } +} diff --git a/tests/JWTManagerTest.php b/tests/JWTManagerTest.php new file mode 100644 index 0000000..e3ad97d --- /dev/null +++ b/tests/JWTManagerTest.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\JWT; + +use Mockery; +use Tymon\JWTAuth\Token; +use Tymon\JWTAuth\Payload; +use Tymon\JWTAuth\JWTManager; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Expiration; + +class JWTManagerTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->jwt = Mockery::mock('Tymon\JWTAuth\Providers\JWT\JWTInterface'); + $this->blacklist = Mockery::mock('Tymon\JWTAuth\Blacklist'); + $this->factory = Mockery::mock('Tymon\JWTAuth\PayloadFactory'); + $this->manager = new JWTManager($this->jwt, $this->blacklist, $this->factory); + + $this->validator = Mockery::mock('Tymon\JWTAuth\Validators\PayloadValidator'); + $this->validator->shouldReceive('setRefreshFlow->check'); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_encode_a_payload() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 + 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator); + + $this->jwt->shouldReceive('encode')->with($payload->toArray())->andReturn('foo.bar.baz'); + + $token = $this->manager->encode($payload); + + $this->assertEquals($token, 'foo.bar.baz'); + } + + /** @test */ + public function it_should_decode_a_token() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 + 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator); + $token = new Token('foo.bar.baz'); + + $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); + $this->factory->shouldReceive('setRefreshFlow->make')->with($payload->toArray())->andReturn($payload); + $this->blacklist->shouldReceive('has')->with($payload)->andReturn(false); + + $payload = $this->manager->decode($token); + + $this->assertInstanceOf('Tymon\JWTAuth\Payload', $payload); + } + + /** @test */ + public function it_should_throw_exception_when_token_is_blacklisted() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenBlacklistedException'); + + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 + 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator); + $token = new Token('foo.bar.baz'); + + $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); + $this->factory->shouldReceive('setRefreshFlow->make')->with($payload->toArray())->andReturn($payload); + $this->blacklist->shouldReceive('has')->with($payload)->andReturn(true); + + $this->manager->decode($token); + } + + /** @test */ + public function it_should_refresh_a_token() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 - 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator, true); + $token = new Token('foo.bar.baz'); + + $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); + $this->jwt->shouldReceive('encode')->with($payload->toArray())->andReturn('baz.bar.foo'); + + $this->factory->shouldReceive('setRefreshFlow')->andReturn($this->factory); + $this->factory->shouldReceive('make')->andReturn($payload); + + $this->blacklist->shouldReceive('has')->with($payload)->andReturn(false); + $this->blacklist->shouldReceive('add')->once()->with($payload); + + $token = $this->manager->refresh($token); + + $this->assertInstanceOf('Tymon\JWTAuth\Token', $token); + $this->assertEquals('baz.bar.foo', $token); + } + + /** @test */ + public function it_should_invalidate_a_token() + { + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 + 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foo'), + ]; + $payload = new Payload($claims, $this->validator); + $token = new Token('foo.bar.baz'); + + $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); + $this->factory->shouldReceive('setRefreshFlow->make')->with($payload->toArray())->andReturn($payload); + $this->blacklist->shouldReceive('has')->with($payload)->andReturn(false); + + $this->blacklist->shouldReceive('add')->with($payload)->andReturn(true); + + $this->manager->invalidate($token); + } + + /** @test */ + public function it_should_throw_an_exception_when_enable_blacklist_is_set_to_false() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\JWTException'); + + $token = new Token('foo.bar.baz'); + + $this->manager->setBlacklistEnabled(false)->invalidate($token); + } + + /** @test */ + public function it_should_get_the_payload_factory() + { + $this->assertInstanceOf('Tymon\JWTAuth\PayloadFactory', $this->manager->getPayloadFactory()); + } + + /** @test */ + public function it_should_get_the_jwt_provider() + { + $this->assertInstanceOf('Tymon\JWTAuth\Providers\JWT\JWTInterface', $this->manager->getJWTProvider()); + } + + /** @test */ + public function it_should_get_the_blacklist() + { + $this->assertInstanceOf('Tymon\JWTAuth\Blacklist', $this->manager->getBlacklist()); + } +} diff --git a/tests/Middleware/GetUserFromTokenTest.php b/tests/Middleware/GetUserFromTokenTest.php new file mode 100644 index 0000000..afb18ea --- /dev/null +++ b/tests/Middleware/GetUserFromTokenTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test; + +use Mockery; +use Tymon\JWTAuth\Middleware\GetUserFromToken; +use Tymon\JWTAuth\Exceptions\TokenExpiredException; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; + +class GetUserFromTokenTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher'); + $this->auth = Mockery::mock('Tymon\JWTAuth\JWTAuth'); + + $this->request = Mockery::mock('Illuminate\Http\Request'); + $this->response = Mockery::mock('Illuminate\Contracts\Routing\ResponseFactory'); + + $this->middleware = new GetUserFromToken($this->response, $this->events, $this->auth); + + $this->auth->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_fire_an_event_when_no_token_is_available() + { + $this->auth->shouldReceive('getToken')->once()->andReturn(false); + + $this->events->shouldReceive('fire')->once()->with('tymon.jwt.absent', [], true); + $this->response->shouldReceive('json')->with(['error' => 'token_not_provided'], 400); + + $this->middleware->handle($this->request, function () { + }); + } + + /** @test */ + public function it_should_fire_an_event_when_the_token_has_expired() + { + $exception = new TokenExpiredException; + + $this->auth->shouldReceive('getToken')->once()->andReturn('foo'); + $this->auth->shouldReceive('authenticate')->once()->with('foo')->andThrow($exception); + + $this->events->shouldReceive('fire')->once()->with('tymon.jwt.expired', [$exception], true); + $this->response->shouldReceive('json')->with(['error' => 'token_expired'], 401); + + $this->middleware->handle($this->request, function () { + }); + } + + /** @test */ + public function it_should_fire_an_event_when_the_token_is_invalid() + { + $exception = new TokenInvalidException; + + $this->auth->shouldReceive('getToken')->once()->andReturn('foo'); + $this->auth->shouldReceive('authenticate')->once()->with('foo')->andThrow($exception); + + $this->events->shouldReceive('fire')->once()->with('tymon.jwt.invalid', [$exception], true); + $this->response->shouldReceive('json')->with(['error' => 'token_invalid'], 400); + + $this->middleware->handle($this->request, function () { + }); + } + + /** @test */ + public function it_should_fire_an_event_when_no_user_is_found() + { + $this->auth->shouldReceive('getToken')->once()->andReturn('foo'); + $this->auth->shouldReceive('authenticate')->once()->with('foo')->andReturn(false); + + $this->events->shouldReceive('fire')->once()->with('tymon.jwt.user_not_found', [], true); + $this->response->shouldReceive('json')->with(['error' => 'user_not_found'], 404); + + $this->middleware->handle($this->request, function () { + }); + } + + /** @test */ + public function it_should_fire_an_event_when_the_token_has_been_decoded_and_user_is_found() + { + $user = (object) ['id' => 1]; + + $this->auth->shouldReceive('getToken')->once()->andReturn('foo'); + $this->auth->shouldReceive('authenticate')->once()->with('foo')->andReturn($user); + + $this->events->shouldReceive('fire')->once()->with('tymon.jwt.valid', $user); + $this->response->shouldReceive('json')->never(); + + $this->middleware->handle($this->request, function () { + }); + } +} diff --git a/tests/PayloadFactoryTest.php b/tests/PayloadFactoryTest.php new file mode 100644 index 0000000..f25d6cc --- /dev/null +++ b/tests/PayloadFactoryTest.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\JWT; + +use Mockery; +use Carbon\Carbon; +use Illuminate\Http\Request; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\Custom; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\PayloadFactory; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Expiration; + +class PayloadFactoryTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + Carbon::setTestNow(Carbon::createFromTimeStampUTC(123)); + + $this->claimFactory = Mockery::mock('Tymon\JWTAuth\Claims\Factory'); + $this->validator = Mockery::mock('Tymon\JWTAuth\Validators\PayloadValidator'); + $this->factory = new PayloadFactory($this->claimFactory, Request::create('/foo', 'GET'), $this->validator); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_return_a_payload_when_passing_an_array_of_claims_to_make_method() + { + $this->validator->shouldReceive('setRefreshFlow->check'); + + $expTime = 123 + 3600; + + $this->claimFactory->shouldReceive('get')->once()->with('sub', 1)->andReturn(new Subject(1)); + $this->claimFactory->shouldReceive('get')->once()->with('iss', Mockery::any())->andReturn(new Issuer('/foo')); + $this->claimFactory->shouldReceive('get')->once()->with('iat', 123)->andReturn(new IssuedAt(123)); + $this->claimFactory->shouldReceive('get')->once()->with('jti', 'foo')->andReturn(new JwtId('foo')); + $this->claimFactory->shouldReceive('get')->once()->with('nbf', 123)->andReturn(new NotBefore(123)); + $this->claimFactory->shouldReceive('get')->once()->with('exp', $expTime)->andReturn(new Expiration($expTime)); + + $payload = $this->factory->make(['sub' => 1, 'jti' => 'foo', 'iat' => 123]); + + $this->assertEquals($payload->get('sub'), 1); + $this->assertEquals($payload->get('iat'), 123); + $this->assertEquals($payload['exp'], $expTime); + + $this->assertInstanceOf('Tymon\JWTAuth\Payload', $payload); + } + + /** @test **/ + public function it_should_check_custom_claim_keys_accurately_and_accept_numeric_claims() + { + $this->validator->shouldReceive('setRefreshFlow->check'); + + $this->claimFactory->shouldReceive('get')->once()->with('iss', Mockery::any())->andReturn(new Issuer('/foo')); + $this->claimFactory->shouldReceive('get')->once()->with('exp', 123 + 3600)->andReturn(new Expiration(123 + 3600)); + $this->claimFactory->shouldReceive('get')->once()->with('iat', 123)->andReturn(new IssuedAt(123)); + $this->claimFactory->shouldReceive('get')->once()->with('jti', Mockery::any())->andReturn(new JwtId('foo')); + $this->claimFactory->shouldReceive('get')->once()->with('nbf', 123)->andReturn(new NotBefore(123)); + $this->claimFactory->shouldReceive('get')->once()->with(1, 'claim one')->andReturn(new Custom(1, 'claim one')); + + $payload = $this->factory->make([1 => 'claim one']); + + // if the checker doesn't compare defaults properly, numeric-keyed claims might be ignored + $this->assertEquals('claim one', $payload->get(1)); + // iat is $defaultClaims[1], so verify it wasn't skipped due to a bad k-v comparison + $this->assertEquals(123, $payload->get('iat')); + } + + /** @test */ + public function it_should_return_a_payload_when_chaining_claim_methods() + { + $this->validator->shouldReceive('setRefreshFlow->check'); + + $this->claimFactory->shouldReceive('get')->once()->with('sub', 1)->andReturn(new Subject(1)); + $this->claimFactory->shouldReceive('get')->once()->with('iss', Mockery::any())->andReturn(new Issuer('/foo')); + $this->claimFactory->shouldReceive('get')->once()->with('exp', 123 + 3600)->andReturn(new Expiration(123 + 3600)); + $this->claimFactory->shouldReceive('get')->once()->with('iat', 123)->andReturn(new IssuedAt(123)); + $this->claimFactory->shouldReceive('get')->once()->with('jti', Mockery::any())->andReturn(new JwtId('foo')); + $this->claimFactory->shouldReceive('get')->once()->with('nbf', 123)->andReturn(new NotBefore(123)); + $this->claimFactory->shouldReceive('get')->once()->with('foo', 'baz')->andReturn(new Custom('foo', 'baz')); + + $payload = $this->factory->sub(1)->foo('baz')->make(); + + $this->assertEquals($payload['sub'], 1); + $this->assertEquals($payload->get('jti'), 'foo'); + $this->assertEquals($payload->get('foo'), 'baz'); + + $this->assertInstanceOf('Tymon\JWTAuth\Payload', $payload); + } + + /** @test */ + public function it_should_return_a_payload_when_passing_miltidimensional_claims() + { + $this->validator->shouldReceive('setRefreshFlow->check'); + $userObject = ['name' => 'example']; + + $this->claimFactory->shouldReceive('get')->once()->with('sub', $userObject)->andReturn(new Subject($userObject)); + $this->claimFactory->shouldReceive('get')->once()->with('iss', Mockery::any())->andReturn(new Issuer('/foo')); + $this->claimFactory->shouldReceive('get')->once()->with('exp', Mockery::any())->andReturn(new Expiration(123 + 3600)); + $this->claimFactory->shouldReceive('get')->once()->with('iat', Mockery::any())->andReturn(new IssuedAt(123)); + $this->claimFactory->shouldReceive('get')->once()->with('jti', Mockery::any())->andReturn(new JwtId('foo')); + $this->claimFactory->shouldReceive('get')->once()->with('nbf', Mockery::any())->andReturn(new NotBefore(123)); + $this->claimFactory->shouldReceive('get')->once()->with('foo', ['bar' => [0, 0, 0]])->andReturn(new Custom('foo', ['bar' => [0, 0, 0]])); + + $payload = $this->factory->sub($userObject)->foo(['bar' => [0, 0, 0]])->make(); + + $this->assertEquals($payload->get('sub'), $userObject); + $this->assertEquals($payload->get('foo'), ['bar' => [0, 0, 0]]); + + $this->assertInstanceOf('Tymon\JWTAuth\Payload', $payload); + } + + /** @test */ + public function it_should_set_the_ttl() + { + $this->factory->setTTL(12345); + + $this->assertEquals($this->factory->getTTL(), 12345); + } +} diff --git a/tests/PayloadTest.php b/tests/PayloadTest.php new file mode 100644 index 0000000..42ae69b --- /dev/null +++ b/tests/PayloadTest.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\JWT; + +use Mockery; +use Carbon\Carbon; +use Tymon\JWTAuth\Payload; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Claims\Audience; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Expiration; + +class PayloadTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + Carbon::setTestNow(Carbon::createFromTimeStampUTC(123)); + + $claims = [ + new Subject(1), + new Issuer('http://example.com'), + new Expiration(123 + 3600), + new NotBefore(123), + new IssuedAt(123), + new JwtId('foo'), + ]; + + $this->validator = Mockery::mock('Tymon\JWTAuth\Validators\PayloadValidator'); + $this->validator->shouldReceive('setRefreshFlow->check'); + + $this->payload = new Payload($claims, $this->validator); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_throws_an_exception_when_trying_to_add_to_the_payload() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\PayloadException'); + + $this->payload['foo'] = 'bar'; + } + + /** @test */ + public function it_throws_an_exception_when_trying_to_remove_a_key_from_the_payload() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\PayloadException'); + + unset($this->payload['foo']); + } + + /** @test */ + public function it_should_cast_the_payload_to_a_string_as_json() + { + $this->assertEquals((string) $this->payload, json_encode($this->payload->get())); + $this->assertJsonStringEqualsJsonString((string) $this->payload, json_encode($this->payload->get())); + } + + /** @test */ + public function it_should_allow_array_access_on_the_payload() + { + $this->assertTrue(isset($this->payload['iat'])); + $this->assertEquals($this->payload['sub'], 1); + $this->assertArrayHasKey('exp', $this->payload); + } + + /** @test */ + public function it_should_get_properties_of_payload_via_get_method() + { + $this->assertInternalType('array', $this->payload->get()); + $this->assertEquals($this->payload->get('sub'), 1); + } + + /** @test */ + public function it_should_get_multiple_properties_when_passing_an_array_to_the_get_method() + { + $values = $this->payload->get(['sub', 'jti']); + + list($sub, $jti) = $values; + + $this->assertInternalType('array', $values); + $this->assertEquals($sub, 1); + $this->assertEquals($jti, 'foo'); + } + + /** @test */ + public function it_should_determine_whether_the_payload_has_a_claim() + { + $this->assertTrue($this->payload->has(new Subject(1))); + $this->assertFalse($this->payload->has(new Audience(1))); + } + + /** @test */ + public function it_should_magically_get_a_property() + { + $sub = $this->payload->getSubject(); + $jti = $this->payload->getJwtId(); + $iss = $this->payload->getIssuer(); + + $this->assertEquals($sub, 1); + $this->assertEquals($jti, 'foo'); + $this->assertEquals($iss, 'http://example.com'); + } + + /** @test */ + public function it_should_throw_an_exception_when_magically_getting_a_property_that_does_not_exist() + { + $this->setExpectedException('\BadMethodCallException'); + + $this->payload->getFoo(); + } + + /** @test */ + public function it_should_get_the_claims() + { + $claims = $this->payload->getClaims(); + + $this->assertInstanceOf('Tymon\JWTAuth\Claims\Expiration', $claims[2]); + $this->assertInstanceOf('Tymon\JWTAuth\Claims\JwtId', $claims[5]); + } +} diff --git a/tests/Providers/Auth/IlluminateAuthAdapterTest.php b/tests/Providers/Auth/IlluminateAuthAdapterTest.php new file mode 100644 index 0000000..3230dcc --- /dev/null +++ b/tests/Providers/Auth/IlluminateAuthAdapterTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\Auth; + +use Mockery; +use Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter; + +class IlluminateAuthAdapterTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->authManager = Mockery::mock('Illuminate\Auth\AuthManager'); + $this->auth = new IlluminateAuthAdapter($this->authManager); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_return_true_if_credentials_are_valid() + { + $this->authManager->shouldReceive('once')->once()->with(['email' => 'foo@bar.com', 'password' => 'foobar'])->andReturn(true); + $this->assertTrue($this->auth->byCredentials(['email' => 'foo@bar.com', 'password' => 'foobar'])); + } + + /** @test */ + public function it_should_return_true_if_user_is_found() + { + $this->authManager->shouldReceive('onceUsingId')->once()->with(123)->andReturn(true); + $this->assertTrue($this->auth->byId(123)); + } + + /** @test */ + public function it_should_return_false_if_user_is_not_found() + { + $this->authManager->shouldReceive('onceUsingId')->once()->with(123)->andReturn(false); + $this->assertFalse($this->auth->byId(123)); + } + + /** @test */ + public function it_should_bubble_exceptions_from_auth() + { + $this->authManager->shouldReceive('onceUsingId')->once()->with(123)->andThrow(new \Exception('Some auth failure')); + $this->setExpectedException('Exception', 'Some auth failure'); + $this->auth->byId(123); + } + + /** @test */ + public function it_should_return_the_currently_authenticated_user() + { + $this->authManager->shouldReceive('user')->once()->andReturn((object) ['id' => 1]); + $this->assertEquals($this->auth->user()->id, 1); + } +} diff --git a/tests/Providers/JWT/JWTProviderTest.php b/tests/Providers/JWT/JWTProviderTest.php new file mode 100644 index 0000000..227afdb --- /dev/null +++ b/tests/Providers/JWT/JWTProviderTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\JWT; + +use Mockery; +use Tymon\JWTAuth\Test\Stubs\JWTProviderStub; + +class JWTProviderTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->provider = new JWTProviderStub('secret', 'HS256'); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_set_the_algo() + { + $this->provider->setAlgo('HS512'); + + $this->assertEquals('HS512', $this->provider->getAlgo()); + } +} diff --git a/tests/Providers/JWT/NamshiAdapterTest.php b/tests/Providers/JWT/NamshiAdapterTest.php new file mode 100644 index 0000000..2f4be91 --- /dev/null +++ b/tests/Providers/JWT/NamshiAdapterTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\JWT; + +use Mockery; +use Carbon\Carbon; +use Tymon\JWTAuth\Providers\JWT\NamshiAdapter; + +class NamshiAdapterTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + Carbon::setTestNow(Carbon::createFromTimeStampUTC(123)); + + $this->jws = Mockery::mock('Namshi\JOSE\JWS'); + $this->provider = new NamshiAdapter('secret', 'HS256', $this->jws); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_return_the_token_when_passing_a_valid_subject_to_encode() + { + $payload = ['sub' => 1, 'exp' => 123, 'iat' => 123, 'iss' => '/foo']; + + $this->jws->shouldReceive('setPayload')->once()->with($payload)->andReturn(Mockery::self()); + $this->jws->shouldReceive('sign')->once()->with('secret')->andReturn(Mockery::self()); + $this->jws->shouldReceive('getTokenString')->once()->andReturn('foo.bar.baz'); + + $token = $this->provider->encode($payload); + + $this->assertEquals('foo.bar.baz', $token); + } + + /** @test */ + public function it_should_throw_an_invalid_exception_when_the_payload_could_not_be_encoded() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\JWTException'); + + $this->jws->shouldReceive('sign')->andThrow(new \Exception); + + $payload = ['sub' => 1, 'exp' => 123, 'iat' => 123, 'iss' => '/foo']; + $this->provider->encode($payload); + } + + /** @test */ + // public function it_should_return_the_payload_when_passing_a_valid_token_to_decode() + // { + // $this->jws->shouldReceive('load')->once()->with('foo.bar.baz')->andReturn(true); + // $this->jws->shouldReceive('verify')->andReturn(true); + + // $payload = $this->provider->decode('foo.bar.baz'); + + // } + + /** @test */ + public function it_should_throw_a_token_invalid_exception_when_the_token_could_not_be_decoded() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenInvalidException'); + + $this->jws->shouldReceive('verify')->andReturn(false); + + $token = $this->provider->decode('foo'); + } +} diff --git a/tests/Providers/Storage/IlluminateCacheAdapterTest.php b/tests/Providers/Storage/IlluminateCacheAdapterTest.php new file mode 100644 index 0000000..26bc40f --- /dev/null +++ b/tests/Providers/Storage/IlluminateCacheAdapterTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\Storage; + +use Mockery; +use Tymon\JWTAuth\Providers\Storage\IlluminateCacheAdapter; + +class IlluminateCacheAdapterTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->cache = Mockery::mock('Illuminate\Cache\CacheManager'); + $this->storage = new IlluminateCacheAdapter($this->cache); + + $this->cache->shouldReceive('tags')->andReturn(Mockery::self()); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_add_the_item_to_storage() + { + $this->cache->shouldReceive('tags->put')->with('foo', 'bar', 10); + + $this->storage->add('foo', 'bar', 10); + } + + /** @test */ + public function it_should_check_if_the_item_exists_in_storage() + { + $this->cache->shouldReceive('tags->has')->with('foo')->andReturn(true); + + $this->assertTrue($this->storage->has('foo')); + } + + /** @test */ + public function it_should_remove_the_item_from_storage() + { + $this->cache->shouldReceive('tags->forget')->with('foo')->andReturn(true); + + $this->assertTrue($this->storage->destroy('foo')); + } + + /** @test */ + public function it_should_remove_all_items_from_storage() + { + $this->cache->shouldReceive('tags->flush')->withNoArgs(); + + $this->storage->flush(); + } +} diff --git a/tests/Providers/User/EloquentUserAdapterTest.php b/tests/Providers/User/EloquentUserAdapterTest.php new file mode 100644 index 0000000..233a377 --- /dev/null +++ b/tests/Providers/User/EloquentUserAdapterTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\User; + +use Mockery; +use Tymon\JWTAuth\Providers\User\EloquentUserAdapter; + +class EloquentUserAdapterTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->builder = Mockery::mock('Illuminate\Database\Query\Builder'); + $this->model = Mockery::mock('Illuminate\Database\Eloquent\Model'); + $this->user = new EloquentUserAdapter($this->model); + } + + public function tearDown() + { + Mockery::close(); + } + + /** @test */ + public function it_should_return_the_user_if_found() + { + $this->builder->shouldReceive('first')->once()->withNoArgs()->andReturn((object) ['id' => 1]); + $this->model->shouldReceive('where')->once()->with('foo', 'bar')->andReturn($this->builder); + + $user = $this->user->getBy('foo', 'bar'); + + $this->assertEquals(1, $user->id); + } +} diff --git a/tests/Stubs/JWTProviderStub.php b/tests/Stubs/JWTProviderStub.php new file mode 100644 index 0000000..f05ec18 --- /dev/null +++ b/tests/Stubs/JWTProviderStub.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Stubs; + +use Tymon\JWTAuth\Providers\JWT\JWTProvider; + +class JWTProviderStub extends JWTProvider +{ +} diff --git a/tests/TokenTest.php b/tests/TokenTest.php new file mode 100644 index 0000000..c105dff --- /dev/null +++ b/tests/TokenTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test\Providers\JWT; + +use Tymon\JWTAuth\Token; + +class TokenTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->token = new Token('foo.bar.baz'); + } + + /** @test */ + public function it_should_return_the_token_when_casting_to_a_string() + { + $this->assertEquals((string) $this->token, $this->token); + } + + /** @test */ + public function it_should_return_the_token_when_calling_get_method() + { + $this->assertInternalType('string', $this->token->get()); + } +} diff --git a/tests/Validators/PayloadValidatorTest.php b/tests/Validators/PayloadValidatorTest.php new file mode 100644 index 0000000..a0cb7df --- /dev/null +++ b/tests/Validators/PayloadValidatorTest.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test; + +use Carbon\Carbon; +use Tymon\JWTAuth\Validators\PayloadValidator; + +class PayloadValidatorTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + Carbon::setTestNow(Carbon::createFromTimeStampUTC(123)); + $this->validator = new PayloadValidator(); + } + + /** @test */ + public function it_should_return_true_when_providing_a_valid_payload() + { + $payload = [ + 'iss' => 'http://example.com', + 'iat' => 100, + 'nbf' => 100, + 'exp' => 100 + 3600, + 'sub' => 1, + 'jti' => 'foo', + ]; + + $this->assertTrue($this->validator->isValid($payload)); + } + + /** @test */ + public function it_should_throw_an_exception_when_providing_an_expired_payload() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenExpiredException'); + + $payload = [ + 'iss' => 'http://example.com', + 'iat' => 20, + 'nbf' => 20, + 'exp' => 120, + 'sub' => 1, + 'jti' => 'foo', + ]; + + $this->validator->check($payload); + } + + /** @test */ + public function it_should_throw_an_exception_when_providing_an_invalid_nbf_claim() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenInvalidException'); + + $payload = [ + 'iss' => 'http://example.com', + 'iat' => 100, + 'nbf' => 150, + 'exp' => 150 + 3600, + 'sub' => 1, + 'jti' => 'foo', + ]; + + $this->validator->check($payload); + } + + /** @test */ + public function it_should_throw_an_exception_when_providing_an_invalid_iat_claim() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenInvalidException'); + + $payload = [ + 'iss' => 'http://example.com', + 'iat' => 150, + 'nbf' => 100, + 'exp' => 150 + 3600, + 'sub' => 1, + 'jti' => 'foo', + ]; + + $this->validator->check($payload); + } + + /** @test */ + public function it_should_throw_an_exception_when_providing_an_invalid_payload() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenInvalidException'); + + $payload = [ + 'iss' => 'http://example.com', + 'sub' => 1, + ]; + + $this->validator->check($payload); + } + + /** @test */ + public function it_should_throw_an_exception_when_providing_an_invalid_expiry() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenInvalidException'); + + $payload = [ + 'iss' => 'http://example.com', + 'iat' => 100, + 'exp' => 'foo', + 'sub' => 1, + 'jti' => 'foo', + ]; + + $this->validator->check($payload); + } + + /** @test **/ + public function it_should_throw_an_exception_when_required_claims_are_missing() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenInvalidException'); + + $payload = [ + 'iss' => 'http://example.com', + 'foo' => 'bar', + // these are inserted to check for regression to a previous bug + // where the check would only compare keys of autoindexed name arrays + // (There are enough to account for all of the required claims' indices) + 'autoindexed', + 'autoindexed', + 'autoindexed', + 'autoindexed', + 'autoindexed', + 'autoindexed', + 'autoindexed', + ]; + + $this->validator->check($payload); + } +} diff --git a/tests/Validators/TokenValidatorTest.php b/tests/Validators/TokenValidatorTest.php new file mode 100644 index 0000000..eb18ebb --- /dev/null +++ b/tests/Validators/TokenValidatorTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Test; + +use Tymon\JWTAuth\Validators\TokenValidator; + +class TokenValidatorTest extends \PHPUnit_Framework_TestCase +{ + public function setUp() + { + $this->validator = new TokenValidator(); + } + + /** @test */ + public function it_should_return_true_when_providing_a_well_formed_token() + { + $this->assertTrue($this->validator->isValid('one.two.three')); + } + + /** @test */ + public function it_should_return_false_when_providing_a_malformed_token() + { + $this->assertFalse($this->validator->isValid('one.two.three.four.five')); + } + + /** @test */ + public function it_should_throw_an_axception_when_providing_a_malformed_token() + { + $this->setExpectedException('Tymon\JWTAuth\Exceptions\TokenInvalidException'); + + $this->validator->check('one.two.three.four.five'); + } +}