├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config └── jwt.php ├── phpunit.xml ├── src ├── Checks │ ├── CheckContract.php │ ├── Expired.php │ ├── Signature.php │ └── Structure.php ├── Exceptions │ ├── JWTAlgorithmNotSupported.php │ ├── JWTAuthorizationHeaderMissing.php │ ├── JWTCheckNotValid.php │ ├── JWTExpired.php │ ├── JWTHeaderNotValid.php │ ├── JWTMethodNotSupported.php │ ├── JWTNoExpiredClaim.php │ ├── JWTNoSubjectClaim.php │ ├── JWTNotValid.php │ ├── JWTPayloadNotValid.php │ └── JWTSignatureNotValid.php ├── Facades │ └── Token.php ├── JwtGuard.php ├── Managers │ ├── Generator.php │ ├── Parser.php │ └── Validator.php ├── Providers │ └── JWTServiceProvider.php ├── Sections │ ├── Header.php │ ├── Payload.php │ ├── Section.php │ └── Signature.php ├── Token.php └── Traits │ ├── AlgorithmCheck.php │ ├── Detokenize.php │ ├── Encoder.php │ └── TokenFromRequest.php └── tests ├── BaseTest.php ├── Feature └── JwtGuardTest.php ├── Providers └── TestServiceProvider.php ├── Stubs └── TestUserProvider.php ├── Traits ├── CapturesOutputBuffer.php ├── ConfiguresJWT.php └── FakesTime.php ├── Unit ├── Checks │ ├── ExpiredTest.php │ ├── SignatureTest.php │ └── StructureTest.php ├── Managers │ ├── GeneratorTest.php │ ├── ParserTest.php │ └── ValidatorTest.php ├── Sections │ ├── HeaderTest.php │ ├── PayloadTest.php │ └── SignatureTest.php └── TokenTest.php └── routes.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | 5 | # Laravel 4 specific 6 | bootstrap/compiled.php 7 | app/storage/ 8 | 9 | # Laravel 5 & Lumen specific 10 | public/storage 11 | public/hot 12 | storage/*.key 13 | .env.*.php 14 | .env.php 15 | .env 16 | Homestead.yaml 17 | Homestead.json 18 | 19 | # Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer 20 | .rocketeer/ 21 | 22 | # PHP storm files 23 | .idea/* 24 | 25 | # PHPUnit reports 26 | .phpunit.result.cache 27 | coverage.xml 28 | 29 | 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | 6 | before_script: 7 | - travis_retry composer self-update 8 | - travis_retry composer install --no-interaction --prefer-source --dev 9 | 10 | script: 11 | - vendor/bin/phpunit 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dejan Babić 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT for Laravel 2 | 3 | >This is a Laravel package which provides all the means for a super easy JWT implementation 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/babicaja/jwt-4laravel.svg?style=flat-square)](https://packagist.org/packages/babicaja/jwt-4laravel) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/babicaja/jwt-4laravel.svg?style=flat-square)](https://packagist.org/packages/babicaja/jwt-4laravel) 7 | [![Build Status](https://img.shields.io/travis/babicaja/jwt-4laravel.svg?style=flat-square)](https://travis-ci.org/babicaja/jwt-4laravel.svg) 8 | [![Coverage](https://codecov.io/gh/babicaja/jwt-4laravel/branch/master/graph/badge.svg)](https://codecov.io/gh/babicaja/jwt-4laravel) 9 | [![Licence](https://img.shields.io/github/license/babicaja/jwt-4laravel.svg?style=flat-square)](https://github.com/babicaja/jwt-4laravel) 10 | 11 | - [Installation](#installation) 12 | - [Getting started](#getting-started) 13 | - [Configuration](#configuration) 14 | - [Usage](#usage) 15 | - [Checks](#checks) 16 | - [Contributing](#contributing) 17 | 18 | ## Installation 19 | 20 | Make sure you include this package as part of your Laravel project. You can do so by running the composer command from below. 21 | 22 | ```bash 23 | composer require babicaja/jwt-4laravel 24 | ``` 25 | 26 | ## Getting started 27 | 28 | First register the service provider with the `artisan vendor:publish` command. This will ensure all the bindings are in place and it will copy the default configuration file to your app's config folder. 29 | 30 | ```bash 31 | php artisan vendor:publish // Choose JWT4L\Providers\JWTServiceProvider from the list 32 | ``` 33 | 34 | Inspect the newly created `config/jwt.php` file. For now, you can leave it as it is (don't forget to change the `secret` key for production). Details about the configuration are covered in this [section](#configuration). 35 | 36 | Now everything is in place and you can start using the JWT for Laravel's functionality. Easiest way to see it in actions is using the `Token` Facade provided by the package, and through `artisan tinker`. If you are able to see a similar output as the one below, you are all set. 37 | 38 | ```bash 39 | php artisan tinker 40 | Psy Shell v0.9.9 (PHP 7.3.6-1+ubuntu18.04.1+deb.sury.org+1 — cli) by Justin Hileman 41 | >>> Token::create() 42 | => "eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYifQ==.eyJpYXQiOiIyMDE5LTA3LTAyVDE1OjAyOjQ5LjkzMTQwMloiLCJleHAiOiIyMDE5LTA3LTAyVDE1OjE3OjQ5LjkzMTQ2M1oifQ==.fa0f19c3a2a444d72bb58feb54227677e52c65e35f3db21b31673520ddb16c86" 43 | ``` 44 | 45 | ## Configuration 46 | 47 | Out of the box the configuration file comes with default values which you can use as they are. You should probably change the `secret` for any production code. You can set the values directly in the `config/jwt.php` file or preferably by setting the appropriate `.env` values. 48 | 49 | >config/jwt.php 50 | ```php 51 | env('JWT_ALG', 'sha256'), //hash_hmac_algos() 55 | 'expires' => env('JWT_EXP', 15), // minutes 56 | 'secret' => env('JWT_KEY', 'secret'), 57 | 'checks' => [ 58 | \JWT4L\Checks\Structure::class, 59 | \JWT4L\Checks\Signature::class, 60 | \JWT4L\Checks\Expired::class 61 | ] 62 | ]; 63 | ``` 64 | 65 | - Algorithm 66 | 67 | This value can be any of the algorithms from PHP's `hash_hmac_algos()`. The algorithm defined here is used for signing and comparing the JWT signature. 68 | 69 | - Expires 70 | 71 | When creating a new Token, by default an `exp` claim is added to the `Payload` section. The amount of minutes defined here will set the expire date from the moment of creation. 72 | 73 | - Secret 74 | 75 | This value is used in conjunction with the defined `algorithm` for signing and comparing the JWT signature. 76 | 77 | - Checks 78 | 79 | You should add your custom [checks](#checks) here or choose which one of the defaults you want to use. It's strongly recommended to use the default ones, but you are free to do as you wish. 80 | 81 | ## Usage 82 | 83 | There are a few ways to use the JWT for Laravel package: 84 | 85 | - Token Facade 86 | 87 | Combines the functionality of the `\JWT4L\Token\Generator`, `JWT4L\Token\Validator` and `\JWT4L\Token\Parser` Token Managers. This allows the user to create, validate and parse the JWT through one interface anywhere in the application. 88 | 89 | > Example of Token Facade usage 90 | ```php 91 | $token = Token::create() // create a JWT 92 | $payload = Token::payload($token) // retrieve the Payload section from the JWT 93 | ``` 94 | - Token Managers 95 | 96 | There are three Token Managers: 97 | - `\JWT4L\Token\Generator` is responsible for JWT creation 98 | - `JWT4L\Token\Validator` validates the provided JWT against the `Checks` 99 | - `\JWT4L\Token\Parser` does the parsing of JWT 100 | 101 | All of the Token Managers are bound to Laravel's Service Container which allows you to inject them into constructors or method calls. 102 | 103 | > Example of Token Manager usage 104 | ```php 105 | create()); 114 | } 115 | } 116 | ``` 117 | - JWTGuard 118 | 119 | The package provides a custom Guard called `JWTGuard`. Laravel's `Auth` will be automatically extended with this Guard but you need to manually configure it in the `config/auth.php` 120 | 121 | > config/auth.php 122 | ```php 123 | 'guards' => [ 124 | 'web' => [ 125 | 'driver' => 'session', 126 | 'provider' => 'users', 127 | ], 128 | 129 | 'api' => [ 130 | 'driver' => 'token', 131 | 'provider' => 'users', 132 | 'hash' => false, 133 | ], 134 | 135 | 'jwt' => [ 136 | 'driver' => 'jwt', 137 | 'provider' => 'users', 138 | ], 139 | ], 140 | ``` 141 | 142 | Now you can assign this guard to any route using the `auth:jwt` middleware. 143 | 144 | > Example of a route protected by the JWTGuard 145 | ```php 146 | Route::middleware('auth:jwt')->post('/user', function (Request $request) { 147 | return $request->user(); 148 | }); 149 | ``` 150 | 151 | One important note regarding the `JWTGuard` implementation. The Guard will try to extract the user identifier from the `sub` (payload) claim of the provided token, so you should make sure that the logic responsible for token creation sets this claim as in the example bellow. 152 | 153 | > Example of setting a custom claim 154 | ```php 155 | Token::withPayload(['sub'=>'user-identifier'])->create(); 156 | ``` 157 | 158 | ## Checks 159 | 160 | The package provides three JWT checks out of the box: 161 | 162 | - `\JWT4L\Checks\Structure` verifies the expected `header.payload.signature` structure 163 | - `\JWT4L\Checks\Signature` validates the hashed signature 164 | - `\JWT4L\Checks\Expired` checks has the token expired 165 | 166 | You can define your custom check by creating a class which implements the `\JWT4L\Checks\CheckContract` interface and include it in the `config/jwt.php`file. 167 | 168 | > app/Checks/Friday.php 169 | ```php 170 | isFriday()) throw new \Exception("It's not Friday"); 189 | } 190 | } 191 | ``` 192 | 193 | > config/jwt.php 194 | ```php 195 | env('JWT_ALG', 'sha256'), //hash_hmac_algos() 199 | 'expires' => env('JWT_EXP', 15), // minutes 200 | 'secret' => env('JWT_KEY', 'secret'), 201 | 'checks' => [ 202 | \JWT4L\Checks\Structure::class, 203 | \JWT4L\Checks\Signature::class, 204 | \JWT4L\Checks\Expired::class, 205 | \App\Checks\Friday::class 206 | ] 207 | ]; 208 | ``` 209 | 210 | ## Contributing 211 | 212 | Contributors: 213 | 214 | - [babicaja](https://github.com/babicaja) 215 | 216 | You are more than welcome to contribute to this project. The main goal is to keep it simple because there are more than enough libraries with advance features. To take your work into consideration please create a Pull Request along the following guidelines: 217 | 218 | ``` 219 | # What's the purpose of this PR? 220 | (Insert the description of the purpose of this change here) 221 | # Impact Analysis 222 | (What will this possibly affect?) 223 | # Where should the tester start? 224 | (Hints tips or tricks regarding how to test this, things to watch out for, etc) 225 | # Any background context you want to provide? 226 | (e.g. was this driven from expirience ) 227 | # What are the relevant tickets? 228 | (Is this related to a ticket/bug at the moment?) 229 | ``` 230 | 231 | Don't forget to write unit tests! All contributors will be listed. 232 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babicaja/jwt-4laravel", 3 | "description": "This is a Laravel package which provides all the means for a super easy JWT implementation", 4 | "license": "MIT", 5 | "keywords": ["laravel", "package", "laravel-5-package", "jwt", "jwt-auth-guard"], 6 | "authors": [ 7 | { 8 | "name": "Dejan Babić", 9 | "email": "babicaja@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.3", 14 | "illuminate/support": "^5.8", 15 | "ext-json": "*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^8.1", 19 | "nunomaduro/phpinsights": "^1.2", 20 | "orchestra/testbench": "^3.5" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "JWT4L\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Tests\\JWT4L\\": "tests/" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "JWT4L\\Providers\\JWTServiceProvider" 36 | ], 37 | "aliases": { 38 | "Token": "JWT4L\\Facades\\Token" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/jwt.php: -------------------------------------------------------------------------------- 1 | env('JWT_ALG', 'sha256'), // hash_hmac_algos() 5 | 'expires' => env('JWT_EXP', 15), // minutes 6 | 'secret' => env('JWT_KEY', 'secret'), 7 | 'checks' => [ 8 | \JWT4L\Checks\Structure::class, 9 | \JWT4L\Checks\Signature::class, 10 | \JWT4L\Checks\Expired::class 11 | ] 12 | ]; -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ./src 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Checks/CheckContract.php: -------------------------------------------------------------------------------- 1 | payloadFromToken($token); 24 | 25 | if (!$payload->exp) throw new JWTNoExpiredClaim(); 26 | if (!Carbon::now()->isBefore(Carbon::create($payload->exp))) throw new JWTExpired(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Checks/Signature.php: -------------------------------------------------------------------------------- 1 | signature = $signature; 22 | } 23 | 24 | /** 25 | * Do necessary checks and throw a specific exception if conditions are not met. 26 | * 27 | * @param string $token 28 | * @return void 29 | * @throws mixed 30 | */ 31 | public function validate(string $token) 32 | { 33 | $calculatedHash = $this->signature->sign($this->headerFromToken($token), $this->payloadFromToken($token)); 34 | $providedHash = $this->signatureFromToken($token); 35 | 36 | if (!hash_equals($calculatedHash, $providedHash)) throw new JWTSignatureNotValid(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Checks/Structure.php: -------------------------------------------------------------------------------- 1 | userProvider = $userProvider; 47 | $this->jwtValidator = $jwtValidator; 48 | $this->jwtParser = $jwtParser; 49 | } 50 | 51 | /** 52 | * Determine if the current user is authenticated. 53 | * 54 | * @return bool 55 | * @throws BindingResolutionException 56 | * @throws Exceptions\JWTAuthorizationHeaderMissing 57 | * @throws Exceptions\JWTCheckNotValid 58 | * @throws JWTHeaderNotValid 59 | * @throws JWTNoSubjectClaim 60 | * @throws JWTPayloadNotValid 61 | */ 62 | public function check() 63 | { 64 | return ! is_null($this->user()); 65 | } 66 | 67 | /** 68 | * Determine if the current user is a guest. 69 | * 70 | * @return bool 71 | * @throws BindingResolutionException 72 | * @throws Exceptions\JWTAuthorizationHeaderMissing 73 | * @throws Exceptions\JWTCheckNotValid 74 | * @throws JWTHeaderNotValid 75 | * @throws JWTNoSubjectClaim 76 | * @throws JWTPayloadNotValid 77 | */ 78 | public function guest() 79 | { 80 | return !$this->check(); 81 | } 82 | 83 | /** 84 | * Get the currently authenticated user. 85 | * 86 | * @return Authenticatable|null 87 | * @throws BindingResolutionException 88 | * @throws Exceptions\JWTAuthorizationHeaderMissing 89 | * @throws Exceptions\JWTCheckNotValid 90 | * @throws JWTHeaderNotValid 91 | * @throws JWTNoSubjectClaim 92 | * @throws JWTPayloadNotValid 93 | */ 94 | public function user() 95 | { 96 | $this->jwtValidator->validate(); 97 | $sub = $this->jwtParser->payload()->sub; 98 | 99 | // if(!$sub) { 100 | // echo "there is no sub."; 101 | // } else { 102 | // echo "we have a sub"; 103 | // } 104 | 105 | if(!$sub) throw new JWTNoSubjectClaim(); 106 | // else echo "here"; 107 | $user = $this->userProvider->retrieveById($sub); 108 | 109 | $this->setUser($user); 110 | 111 | return $user; 112 | } 113 | 114 | /** 115 | * Get the ID for the currently authenticated user. 116 | * 117 | * @return int|null 118 | * @throws BindingResolutionException 119 | * @throws Exceptions\JWTAuthorizationHeaderMissing 120 | * @throws Exceptions\JWTCheckNotValid 121 | * @throws JWTHeaderNotValid 122 | * @throws JWTNoSubjectClaim 123 | * @throws JWTPayloadNotValid 124 | */ 125 | public function id() 126 | { 127 | return $this->user()->getAuthIdentifier(); 128 | } 129 | 130 | /** 131 | * Validate a user's credentials. 132 | * 133 | * @param array $credentials 134 | * @return bool 135 | */ 136 | public function validate(array $credentials = []) 137 | { 138 | $this->user = $this->userProvider->retrieveByCredentials($credentials); 139 | 140 | return $this->user instanceof Authenticatable; 141 | 142 | } 143 | 144 | /** 145 | * Set the current user. 146 | * 147 | * @param Authenticatable $user 148 | * @return void 149 | */ 150 | public function setUser(Authenticatable $user) 151 | { 152 | $this->user = $user; 153 | } 154 | } -------------------------------------------------------------------------------- /src/Managers/Generator.php: -------------------------------------------------------------------------------- 1 | header = $header; 36 | $this->payload = $payload; 37 | $this->signature = $signature; 38 | } 39 | 40 | /** 41 | * Append or replace the default header claims. 42 | * 43 | * @param array $claims 44 | * @param bool $replace 45 | * @return $this 46 | */ 47 | public function withHeader(array $claims, bool $replace = false) 48 | { 49 | $this->header->with($claims, $replace); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Append or replace the default payload claims. 56 | * 57 | * @param array $claims 58 | * @param bool $replace 59 | * @return $this 60 | */ 61 | public function withPayload(array $claims, bool $replace = false) 62 | { 63 | $this->payload->with($claims, $replace); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Create the token. 70 | * 71 | * @return string 72 | */ 73 | public function create() 74 | { 75 | $encodedHeader = $this->header->make(); 76 | $encodedPayload = $this->payload->make(); 77 | $hashedSignature = $this->signature->sign($this->header, $this->payload); 78 | 79 | return implode('.', [$encodedHeader, $encodedPayload, $hashedSignature]); 80 | } 81 | } -------------------------------------------------------------------------------- /src/Managers/Parser.php: -------------------------------------------------------------------------------- 1 | payloadFromToken($this->token($token)); 27 | } 28 | 29 | /** 30 | * Extract the header section from the token. 31 | * 32 | * @param string|null $token 33 | * @return mixed 34 | * @throws Exceptions\JWTHeaderNotValid 35 | * @throws Exceptions\JWTPayloadNotValid 36 | * @throws BindingResolutionException 37 | * @throws Exceptions\JWTAuthorizationHeaderMissing 38 | */ 39 | public function header(string $token = null) 40 | { 41 | return $this->headerFromToken($this->token($token)); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Managers/Validator.php: -------------------------------------------------------------------------------- 1 | checks = $checks; 28 | } 29 | 30 | /** 31 | * Validate the token with configured checks. If the token is not provided it 32 | * will attempt to extract it from the Authorization Bearer header. 33 | * 34 | * @param string|null $token 35 | * @return boolean 36 | * @throws BindingResolutionException 37 | * @throws JWTAuthorizationHeaderMissing 38 | * @throws JWTCheckNotValid 39 | */ 40 | public function validate(string $token = null) 41 | { 42 | foreach ($this->checks as $check) 43 | { 44 | $this->makeCheck($check)->validate($this->token($token)); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | /** 51 | * Check is the defined $check instance of CheckContract, and resolve it out of the container. 52 | * 53 | * @param $check 54 | * @return CheckContract 55 | * @throws BindingResolutionException 56 | * @throws JWTCheckNotValid 57 | */ 58 | private function makeCheck($check) 59 | { 60 | $instance = app()->make($check); 61 | 62 | if (! $instance instanceof CheckContract) throw new JWTCheckNotValid($check); 63 | 64 | return $instance; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Providers/JWTServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(Header::class, function () { 26 | 27 | return new Header(config('jwt.algorithm', 'sha256')); 28 | }); 29 | 30 | $this->app->bind(Payload::class, function () { 31 | 32 | return new Payload(config('jwt.expires', 15)); 33 | }); 34 | 35 | $this->app->bind(Signature::class, function () { 36 | 37 | return new Signature(config('jwt.algorithm', 'sha256'), config('jwt.secret', 'secret')); 38 | }); 39 | 40 | $this->app->bind(Validator::class, function () { 41 | 42 | return new Validator(config('jwt.checks', [])); 43 | }); 44 | 45 | $this->app->bind('jwt-token', function ($app) { 46 | 47 | return new Token(resolve(Managers\Generator::class), resolve(Validator::class), resolve(Parser::class)); 48 | }); 49 | 50 | Auth::extend('jwt', function($app, $name, array $config){ 51 | 52 | return new JwtGuard(Auth::createUserProvider($config['provider']), resolve(Validator::class), resolve(Parser::class)); 53 | }); 54 | } 55 | 56 | /** 57 | * Bootstrap services. 58 | * 59 | * @return void 60 | */ 61 | public function boot() 62 | { 63 | $this->publishes([ 64 | __DIR__ . '/../../config/jwt.php' => config_path('jwt.php'), 65 | ]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Sections/Header.php: -------------------------------------------------------------------------------- 1 | isSupported($algorithm); 18 | 19 | $this->claims = [ 20 | "typ" => "JWT", 21 | "alg" => $algorithm 22 | ]; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Sections/Payload.php: -------------------------------------------------------------------------------- 1 | claims = [ 17 | "iat" => Carbon::now(), 18 | "exp" => Carbon::now()->addMinutes($minutes), 19 | ]; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Sections/Section.php: -------------------------------------------------------------------------------- 1 | claims; 28 | $this->claims = array_merge($default, $claims); 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Encode the claims. 35 | * 36 | * @return string 37 | */ 38 | public function make() 39 | { 40 | return $this->encode($this->claims); 41 | } 42 | 43 | /** 44 | * Access the claim parameters. 45 | * 46 | * @param $name 47 | * @return mixed|null 48 | */ 49 | public function __get($name) 50 | { 51 | return isset($this->claims[$name]) ? $this->claims[$name] : null; 52 | } 53 | 54 | /** 55 | * Expose claims on json encoding. 56 | * 57 | * @return array|mixed 58 | */ 59 | public function jsonSerialize() 60 | { 61 | return $this->claims; 62 | } 63 | 64 | /** 65 | * Expose claims on dump. 66 | * 67 | * @return array 68 | */ 69 | public function __debugInfo() 70 | { 71 | return $this->claims; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Sections/Signature.php: -------------------------------------------------------------------------------- 1 | isSupported($algorithm); 28 | 29 | $this->secret = $secret; 30 | } 31 | 32 | /** 33 | * Sign the claims. 34 | * 35 | * @param Header $header 36 | * @param Payload $payload 37 | * @return string 38 | */ 39 | public function sign(Header $header, Payload $payload) 40 | { 41 | $data = implode('.', [ 42 | $header->make(), 43 | $payload->make() 44 | ]); 45 | 46 | return $this->hash($this->algorithm, $data, $this->secret); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Token.php: -------------------------------------------------------------------------------- 1 | tokenManagers = [$generator, $validator, $parser]; 34 | } 35 | 36 | /** 37 | * @param $name 38 | * @param $arguments 39 | * @return mixed 40 | * @throws Exception 41 | */ 42 | public function __call($name, $arguments) 43 | { 44 | reset($this->tokenManagers); 45 | 46 | do { 47 | try { 48 | return call_user_func_array([current($this->tokenManagers), $name], $arguments); 49 | } catch (ErrorException $exception){} 50 | } while (next($this->tokenManagers)); 51 | 52 | /** @noinspection PhpUnreachableStatementInspection */ 53 | throw new JWTMethodNotSupported("The {$name} method is not supported"); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Traits/AlgorithmCheck.php: -------------------------------------------------------------------------------- 1 | algorithm = $algorithm; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Traits/Detokenize.php: -------------------------------------------------------------------------------- 1 | extractClaims($token, 0, JWTHeaderNotValid::class); 27 | 28 | return app()->make(Header::class)->with($claims, true); 29 | } 30 | 31 | /** 32 | * Creates a Payload object from the provided token. 33 | * 34 | * @param string $token 35 | * @return mixed 36 | * @throws JWTHeaderNotValid 37 | * @throws JWTPayloadNotValid 38 | * @throws BindingResolutionException 39 | */ 40 | protected function payloadFromToken(string $token) 41 | { 42 | $claims = $this->extractClaims($token, 1, JWTPayloadNotValid::class); 43 | 44 | return app()->make(Payload::class)->with($claims, true); 45 | } 46 | 47 | /** 48 | * Extracts the signature portion from the token. 49 | * 50 | * @param string $token 51 | * @return string 52 | * @throws JWTNotValid 53 | */ 54 | protected function signatureFromToken(string $token) 55 | { 56 | return $this->rawSection($token, 2); 57 | } 58 | 59 | 60 | /** 61 | * @param string $token 62 | * @param int $section 63 | * @param string $exceptionClass 64 | * @return array 65 | * @throws JWTHeaderNotValid|JWTPayloadNotValid 66 | */ 67 | private function extractClaims(string $token, int $section, string $exceptionClass) 68 | { 69 | try 70 | { 71 | return (array)($this->decodeFromRaw($this->rawSection($token, $section))); 72 | } 73 | catch (JWTNotValid $exception) 74 | { 75 | /** @var JWTHeaderNotValid|JWTPayloadNotValid $exceptionClass */ 76 | throw new $exceptionClass($exception); 77 | } 78 | } 79 | 80 | /** 81 | * @param string $token 82 | * @param int $section 83 | * @return string 84 | * @throws JWTNotValid 85 | */ 86 | private function rawSection(string $token, int $section) 87 | { 88 | $sections = explode('.', $token); 89 | 90 | if (!isset($sections[$section])) throw new JWTNotValid(); 91 | 92 | return $sections[$section]; 93 | } 94 | 95 | /** 96 | * @param string $raw 97 | * @return mixed 98 | * @throws JWTNotValid 99 | */ 100 | private function decodeFromRaw(string $raw) 101 | { 102 | $decoded = json_decode(base64_decode($raw)); 103 | 104 | if (!$decoded instanceof stdClass && !is_array($decoded)) throw new JWTNotValid(); 105 | 106 | return $decoded; 107 | } 108 | } -------------------------------------------------------------------------------- /src/Traits/Encoder.php: -------------------------------------------------------------------------------- 1 | bearerToken()) throw new JWTAuthorizationHeaderMissing(); 18 | return $token; 19 | } 20 | 21 | /** 22 | * Fetch the token from the Authorization header if the provided token is not set. 23 | * 24 | * @param string $token 25 | * @return string|null 26 | * @throws Exceptions\JWTAuthorizationHeaderMissing 27 | */ 28 | public function token(string $token = null) 29 | { 30 | return $token ?: $this->tokenFromRequest(); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/BaseTest.php: -------------------------------------------------------------------------------- 1 | configure(); 20 | $this->setTime(); 21 | } 22 | 23 | protected function getPackageProviders($app) 24 | { 25 | return [ 26 | JWTServiceProvider::class, 27 | TestServiceProvider::class 28 | ]; 29 | } 30 | } -------------------------------------------------------------------------------- /tests/Feature/JwtGuardTest.php: -------------------------------------------------------------------------------- 1 | generator = $this->app->make(Generator::class); 24 | } 25 | 26 | /** @test **/ 27 | public function it_will_respond_with_token_not_found_without_an_authorization_header() 28 | { 29 | $response = $this->get('/test-jwt-route'); 30 | $this->assertEquals('Authorization Bearer token not found', $response->exception->getMessage()); 31 | } 32 | 33 | /** @test **/ 34 | public function it_will_run_the_configured_checks() 35 | { 36 | $response = $this->withHeader('Authorization', 'Bearer a.b.c')->get('/test-jwt-route'); 37 | // since it should pass the structure check, the signature should check the header and fail there 38 | $this->assertInstanceOf(JWTHeaderNotValid::class, $response->exception); 39 | } 40 | 41 | /** @test **/ 42 | public function it_will_authenticate_a_valid_token() 43 | { 44 | $token = $this->generator->withPayload(['sub' => 1])->create(); 45 | 46 | $response = $this->withHeader('Authorization', "Bearer {$token}")->get('/test-jwt-route'); 47 | 48 | $this->assertEquals("JWT SUCCESS", $response->getContent()); 49 | } 50 | 51 | /** @test */ 52 | public function it_will_provide_user_info_through_guard_commands() 53 | { 54 | $token = $this->generator->withPayload(['sub' => 1])->create(); 55 | 56 | $this->withHeader('Authorization', "Bearer {$token}")->get('/test-jwt-route'); 57 | 58 | $this->assertInstanceOf(Authenticatable::class, Auth::user()); 59 | $this->assertEquals(1, Auth::id()); 60 | $this->assertEquals(false, Auth::guest()); 61 | $this->assertEquals(true, Auth::validate(['email' => 'example@example.com', 'password' => 'test'])); 62 | } 63 | 64 | /** @test **/ 65 | public function it_will_throw_a_proper_exception_if_the_sub_claim_is_missing() 66 | { 67 | $token = $this->generator->create(); 68 | $response = $this->withHeader('Authorization', "Bearer {$token}")->get('/test-jwt-route'); 69 | 70 | $this->assertInstanceOf(JWTNoSubjectClaim::class, $response->exception); 71 | } 72 | } -------------------------------------------------------------------------------- /tests/Providers/TestServiceProvider.php: -------------------------------------------------------------------------------- 1 | make(TestUserProvider::class); 18 | }); 19 | } 20 | 21 | /** 22 | * Bootstrap the routes. 23 | */ 24 | public function boot() 25 | { 26 | $this->loadRoutesFrom(__DIR__ . '/../routes.php'); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /tests/Stubs/TestUserProvider.php: -------------------------------------------------------------------------------- 1 | user = \Mockery::mock(Authenticatable::class); 18 | $this->user->shouldReceive("getAuthIdentifier")->andReturn(1); 19 | } 20 | 21 | /** 22 | * Retrieve a user by their unique identifier. 23 | * 24 | * @param mixed $identifier 25 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 26 | */ 27 | public function retrieveById($identifier) 28 | { 29 | return $this->user; 30 | } 31 | 32 | /** 33 | * Retrieve a user by their unique identifier and "remember me" token. 34 | * 35 | * @param mixed $identifier 36 | * @param string $token 37 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 38 | */ 39 | public function retrieveByToken($identifier, $token) 40 | { 41 | return $this->user; 42 | } 43 | 44 | /** 45 | * Update the "remember me" token for the given user in storage. 46 | * 47 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 48 | * @param string $token 49 | * @return void 50 | */ 51 | public function updateRememberToken(Authenticatable $user, $token) 52 | { 53 | } 54 | 55 | /** 56 | * Retrieve a user by the given credentials. 57 | * 58 | * @param array $credentials 59 | * @return \Illuminate\Contracts\Auth\Authenticatable|null 60 | */ 61 | public function retrieveByCredentials(array $credentials) 62 | { 63 | return $this->user; 64 | } 65 | 66 | /** 67 | * Validate a user against the given credentials. 68 | * 69 | * @param \Illuminate\Contracts\Auth\Authenticatable $user 70 | * @param array $credentials 71 | * @return bool 72 | */ 73 | public function validateCredentials(Authenticatable $user, array $credentials) 74 | { 75 | return true; 76 | } 77 | } -------------------------------------------------------------------------------- /tests/Traits/CapturesOutputBuffer.php: -------------------------------------------------------------------------------- 1 | 'sha256']); 18 | config(['jwt.secret' => 'test-secret']); 19 | config(['jwt.expires' => 15]); 20 | config(['jwt.checks' => [ 21 | Structure::class, 22 | Signature::class, 23 | Expired::class 24 | ]]); 25 | 26 | // setup custom guard 27 | config(['auth.guards.jwt' => [ 28 | 'driver' => 'jwt', 29 | 'provider' => 'users' 30 | ]]); 31 | 32 | // setup custom user provider 33 | config(['auth.providers.users' => [ 34 | 'driver' => 'test' 35 | ]]); 36 | } 37 | 38 | /** 39 | * Override any configuration value. 40 | * 41 | * @param array $config 42 | */ 43 | public function overrideConfiguration(array $config) 44 | { 45 | config($config); 46 | } 47 | } -------------------------------------------------------------------------------- /tests/Traits/FakesTime.php: -------------------------------------------------------------------------------- 1 | testNow = Carbon::create(2012, 12, 21, 12, 0, 0); 20 | 21 | Carbon::setTestNow($this->testNow); 22 | } 23 | 24 | /** 25 | * Move the test time forth in minutes. 26 | * 27 | * @param int $minutes 28 | */ 29 | public function moveTime(int $minutes) 30 | { 31 | $this->setTime(); 32 | 33 | Carbon::setTestNow($this->testNow->addMinutes($minutes)); 34 | } 35 | } -------------------------------------------------------------------------------- /tests/Unit/Checks/ExpiredTest.php: -------------------------------------------------------------------------------- 1 | overrideConfiguration(['jwt.expires' => $this->expiresIn]); 38 | 39 | $this->generator = $this->app->make(Generator::class); 40 | $this->check = $this->app->make(Expired::class); 41 | 42 | $this->token = $this->generator->create(); 43 | } 44 | 45 | /** @test */ 46 | public function it_will_throw_a_proper_exception_if_the_token_has_expired() 47 | { 48 | $this->moveTime($this->expiresIn + 1); 49 | $this->expectException(JWTExpired::class); 50 | 51 | $this->check->validate($this->token); 52 | } 53 | 54 | /** @test */ 55 | public function it_will_finish_silently_if_the_token_has_not_expired() 56 | { 57 | $this->moveTime($this->expiresIn - 1); 58 | /** @noinspection PhpVoidFunctionResultUsedInspection */ 59 | $this->assertNull($this->check->validate($this->token)); 60 | } 61 | 62 | /** @test **/ 63 | public function it_will_throw_a_proper_exception_if_the_exp_claim_is_not_set() 64 | { 65 | $this->expectException(JWTNoExpiredClaim::class); 66 | $this->check->validate($this->generator->withPayload([], true)->create()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/Checks/SignatureTest.php: -------------------------------------------------------------------------------- 1 | check = $this->app->make(Signature::class); 32 | $this->validToken = $this->app->make(Generator::class)->create(); 33 | } 34 | 35 | /** @test */ 36 | public function it_will_throw_a_proper_exception_if_the_token_signatures_are_not_equal() 37 | { 38 | $this->overrideConfiguration(['jwt.secret' => 'not-signature-secret']); 39 | 40 | $token = $this->app->make(Generator::class)->create(); 41 | 42 | $this->expectException(JWTSignatureNotValid::class); 43 | 44 | $this->check->validate($token); 45 | } 46 | 47 | /** @test */ 48 | public function it_will_finish_silently_if_the_token_signatures_are_equal() 49 | { 50 | /** @noinspection PhpVoidFunctionResultUsedInspection */ 51 | $this->assertNull($this->check->validate($this->validToken)); 52 | } 53 | 54 | /** @test */ 55 | public function it_will_throw_a_proper_exception_if_the_header_claims_were_manipulated() 56 | { 57 | $manipulatedHeader = $this->encode(['typ' => 'manipulated', 'alg' => 'very-bad']); 58 | $manipulatedToken = $this->replaceSectionInToken($this->validToken, $manipulatedHeader, 0); 59 | 60 | $this->expectException(JWTSignatureNotValid::class); 61 | 62 | $this->check->validate($manipulatedToken); 63 | } 64 | 65 | /** @test */ 66 | public function it_will_throw_a_proper_exception_if_the_payload_claims_were_manipulated() 67 | { 68 | $manipulatedPayload = $this->encode(['exp' => "2012-12-21"]); 69 | $manipulatedToken = $this->replaceSectionInToken($this->validToken, $manipulatedPayload, 1); 70 | 71 | $this->expectException(JWTSignatureNotValid::class); 72 | 73 | $this->check->validate($manipulatedToken); 74 | } 75 | 76 | /** @test */ 77 | public function it_will_throw_a_proper_exception_if_the_header_claims_are_invalid() 78 | { 79 | $manipulatedToken = $this->replaceSectionInToken($this->validToken, "bad-header", 0); 80 | 81 | $this->expectException(JWTHeaderNotValid::class); 82 | 83 | $this->check->validate($manipulatedToken); 84 | } 85 | 86 | /** @test */ 87 | public function it_will_throw_a_proper_exception_if_the_payload_claims_are_invalid() 88 | { 89 | $manipulatedToken = $this->replaceSectionInToken($this->validToken, "bad-payload", 1); 90 | 91 | $this->expectException(JWTPayloadNotValid::class); 92 | 93 | $this->check->validate($manipulatedToken); 94 | } 95 | 96 | /** 97 | * Replace a section of a token with provided string. 98 | * 99 | * @param string $validToken 100 | * @param string $manipulatedSection 101 | * @param int $sectionPosition 102 | * @return string 103 | */ 104 | private function replaceSectionInToken(string $validToken, string $manipulatedSection, int $sectionPosition) 105 | { 106 | $sections = explode('.', $validToken); 107 | $sections[$sectionPosition] = $manipulatedSection; 108 | 109 | return implode('.', $sections); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/Unit/Checks/StructureTest.php: -------------------------------------------------------------------------------- 1 | app->make(Structure::class); 15 | 16 | $this->expectException(JWTNotValid::class); 17 | 18 | $check->validate('one.two'); 19 | } 20 | 21 | /** @test */ 22 | public function it_will_finish_silently_if_a_three_part_token_is_provided() 23 | { 24 | $check = $this->app->make(Structure::class); 25 | 26 | $this->assertNull($check->validate('one.two.three')); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Unit/Managers/GeneratorTest.php: -------------------------------------------------------------------------------- 1 | generator = $this->app->make(Generator::class); 20 | } 21 | 22 | /** @test **/ 23 | public function it_can_generate_a_token() 24 | { 25 | $this->assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYifQ==.eyJpYXQiOiIyMDEyLTEyLTIxVDEyOjAwOjAwLjAwMDAwMFoiLCJleHAiOiIyMDEyLTEyLTIxVDEyOjE1OjAwLjAwMDAwMFoifQ==.d4d513d6449050229d8ef73325c88d01a3707a49c5fc7c86bad5e741657c0c7b", $this->generator->create()); 26 | } 27 | 28 | /** @test **/ 29 | public function it_can_append_to_the_header_and_generate_a_token() 30 | { 31 | $this->generator->withHeader(['test' => 'claim']); 32 | $this->assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYiLCJ0ZXN0IjoiY2xhaW0ifQ==.eyJpYXQiOiIyMDEyLTEyLTIxVDEyOjAwOjAwLjAwMDAwMFoiLCJleHAiOiIyMDEyLTEyLTIxVDEyOjE1OjAwLjAwMDAwMFoifQ==.020d0c1ed9840675f4a68d01f32357f866ec82fcebf62edaa6a2dcc65bad9e63", $this->generator->create()); 33 | } 34 | 35 | /** @test **/ 36 | public function it_can_replace_the_header_and_generate_a_token() 37 | { 38 | $this->generator->withHeader(['test' => 'claim'], true); 39 | $this->assertEquals("eyJ0ZXN0IjoiY2xhaW0ifQ==.eyJpYXQiOiIyMDEyLTEyLTIxVDEyOjAwOjAwLjAwMDAwMFoiLCJleHAiOiIyMDEyLTEyLTIxVDEyOjE1OjAwLjAwMDAwMFoifQ==.92a31e9c7ccb737c96bf127d3cb05ed79b6f1574c6422d82ad387d2546cc849b", $this->generator->create()); 40 | } 41 | 42 | /** @test **/ 43 | public function it_can_append_to_the_payload_and_generate_a_token() 44 | { 45 | $this->generator->withPayload(['test' => 'claim']); 46 | $this->assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYifQ==.eyJpYXQiOiIyMDEyLTEyLTIxVDEyOjAwOjAwLjAwMDAwMFoiLCJleHAiOiIyMDEyLTEyLTIxVDEyOjE1OjAwLjAwMDAwMFoiLCJ0ZXN0IjoiY2xhaW0ifQ==.7c62872412b4a0a699f8d02e7015929224a569ae8b90c5a7af4bebf3c7d44ba7", $this->generator->create()); 47 | } 48 | 49 | /** @test **/ 50 | public function it_can_replace_the_payload_and_generate_a_token() 51 | { 52 | $this->generator->withPayload(['test' => 'claim'], true); 53 | $this->assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYifQ==.eyJ0ZXN0IjoiY2xhaW0ifQ==.e2d23f571b2bbc41b42da723b3945c052a2e29feae1f9baa5ee43bab1727603d", $this->generator->create()); 54 | } 55 | } -------------------------------------------------------------------------------- /tests/Unit/Managers/ParserTest.php: -------------------------------------------------------------------------------- 1 | overrideConfiguration(['jwt.checks' => [Structure::class]]); 33 | 34 | $this->validToken = $this->app->make(Generator::class)->create(); 35 | $this->parser = $this->app->make(Parser::class); 36 | } 37 | 38 | /** 39 | * @test 40 | * @throws mixed 41 | */ 42 | public function it_can_extract_the_payload_from_a_valid_token() 43 | { 44 | $this->assertInstanceOf(Payload::class, $this->parser->payload($this->validToken)); 45 | } 46 | 47 | /** 48 | * @test 49 | * @throws mixed 50 | */ 51 | public function it_can_extract_the_header_from_a_valid_token() 52 | { 53 | $this->assertInstanceOf(Header::class, $this->parser->header($this->validToken)); 54 | } 55 | 56 | /** 57 | * @test 58 | * @throws BindingResolutionException 59 | * @throws JWTAuthorizationHeaderMissing 60 | * @throws JWTHeaderNotValid 61 | * @throws JWTPayloadNotValid 62 | */ 63 | public function it_will_use_the_bearer_token_if_the_token_is_not_provided() 64 | { 65 | request()->headers->add(["Authorization" => "Bearer {$this->validToken}"]); 66 | $this->assertInstanceOf(Payload::class, $this->parser->payload()); 67 | } 68 | 69 | /** 70 | * @test 71 | * @throws BindingResolutionException 72 | * @throws JWTAuthorizationHeaderMissing 73 | * @throws JWTHeaderNotValid 74 | * @throws JWTPayloadNotValid 75 | */ 76 | public function it_will_throw_a_proper_exception_if_the_token_is_not_provided_or_not_in_the_header() 77 | { 78 | $this->expectException(JWTAuthorizationHeaderMissing::class); 79 | $this->parser->payload(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Unit/Managers/ValidatorTest.php: -------------------------------------------------------------------------------- 1 | setChecks([Structure::class]); 28 | } 29 | 30 | /** 31 | * @test 32 | * @throws BindingResolutionException 33 | * @throws JWTAuthorizationHeaderMissing 34 | * @throws JWTCheckNotValid 35 | */ 36 | public function it_will_only_run_the_configured_checks_on_validation() 37 | { 38 | $this->assertTrue($this->validator->validate("one.two.three")); 39 | } 40 | 41 | /** 42 | * @test 43 | * @throws BindingResolutionException 44 | * @throws JWTAuthorizationHeaderMissing 45 | * @throws JWTCheckNotValid 46 | */ 47 | public function it_will_throw_a_proper_exception_if_validation_fails() 48 | { 49 | $this->expectException(JWTNotValid::class); 50 | $this->validator->validate("one.two"); 51 | } 52 | 53 | /** 54 | * @test 55 | * @throws BindingResolutionException 56 | * @throws JWTAuthorizationHeaderMissing 57 | * @throws JWTCheckNotValid 58 | */ 59 | public function it_will_throw_a_proper_exception_if_a_check_is_not_valid() 60 | { 61 | $this->setChecks([stdClass::class]); 62 | $this->expectException(JWTCheckNotValid::class); 63 | $this->validator->validate("one.two.three"); 64 | } 65 | 66 | /** 67 | * @test 68 | * @throws BindingResolutionException 69 | * @throws JWTAuthorizationHeaderMissing 70 | * @throws JWTCheckNotValid 71 | */ 72 | public function it_will_use_the_bearer_token_if_the_token_is_not_provided() 73 | { 74 | $this->configure(); 75 | $token = resolve(Generator::class)->create(); 76 | request()->headers->add(["Authorization" => "Bearer {$token}"]); 77 | $this->assertTrue($this->validator->validate()); 78 | } 79 | 80 | /** 81 | * @test 82 | * @throws BindingResolutionException 83 | * @throws JWTAuthorizationHeaderMissing 84 | * @throws JWTCheckNotValid 85 | */ 86 | public function it_will_throw_a_proper_exception_if_the_token_is_not_provided_or_not_in_the_header() 87 | { 88 | $this->expectException(JWTAuthorizationHeaderMissing::class); 89 | $this->validator->validate(); 90 | } 91 | 92 | public function setChecks(array $checks = []): void 93 | { 94 | $this->overrideConfiguration(['jwt.checks' => $checks]); 95 | 96 | $this->validator = $this->app->make(Validator::class); 97 | } 98 | } -------------------------------------------------------------------------------- /tests/Unit/Sections/HeaderTest.php: -------------------------------------------------------------------------------- 1 | header = $this->app->make(Header::class); 24 | } 25 | 26 | /** @test **/ 27 | public function it_can_encode_the_header_with_default_values() 28 | { 29 | $this->assertEquals('eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYifQ==', $this->header->make()); 30 | } 31 | 32 | /** @test **/ 33 | public function it_can_encode_the_header_with_appended_values() 34 | { 35 | $this->header->with(['test'=>123]); 36 | 37 | $this->assertEquals('eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYiLCJ0ZXN0IjoxMjN9', $this->header->make()); 38 | } 39 | 40 | /** @test **/ 41 | public function it_can_encode_the_header_with_replaced_values() 42 | { 43 | $this->header->with(['test'=>123], true); 44 | 45 | $this->assertEquals('eyJ0ZXN0IjoxMjN9', $this->header->make()); 46 | } 47 | 48 | /** @test */ 49 | public function it_will_throw_if_the_algorithm_is_unsupported() 50 | { 51 | // set the algorithm type in the config 52 | $this->overrideConfiguration(['jwt.algorithm' => 'test']); 53 | $this->expectException(JWTAlgorithmNotSupported::class); 54 | $this->app->make(Header::class); 55 | } 56 | 57 | /** @test **/ 58 | public function it_can_access_claims_through_magic_get() 59 | { 60 | $this->assertEquals('JWT', $this->header->typ); 61 | } 62 | 63 | /** @test **/ 64 | public function it_can_be_json_encoded() 65 | { 66 | $this->assertJsonStringEqualsJsonString('{"typ":"JWT","alg":"sha256"}', json_encode($this->header)); 67 | } 68 | 69 | /** @test **/ 70 | public function it_will_expose_claims_on_dump() 71 | { 72 | $dump = $this->capture("var_dump", $this->header); 73 | 74 | $this->assertStringContainsString(Header::class, $dump); 75 | $this->assertStringContainsString("JWT", $dump); 76 | $this->assertStringContainsString("sha256", $dump); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Unit/Sections/PayloadTest.php: -------------------------------------------------------------------------------- 1 | payload = $this->app->make(Payload::class); 23 | } 24 | 25 | /** @test **/ 26 | public function it_can_encode_the_payload_with_default_values() 27 | { 28 | $this->assertEquals('eyJpYXQiOiIyMDEyLTEyLTIxVDEyOjAwOjAwLjAwMDAwMFoiLCJleHAiOiIyMDEyLTEyLTIxVDEyOjE1OjAwLjAwMDAwMFoifQ==', $this->payload->make()); 29 | } 30 | 31 | /** @test **/ 32 | public function it_can_encode_the_payload_with_appended_values() 33 | { 34 | $this->payload->with(['test'=>123]); 35 | 36 | $this->assertEquals('eyJpYXQiOiIyMDEyLTEyLTIxVDEyOjAwOjAwLjAwMDAwMFoiLCJleHAiOiIyMDEyLTEyLTIxVDEyOjE1OjAwLjAwMDAwMFoiLCJ0ZXN0IjoxMjN9', $this->payload->make()); 37 | } 38 | 39 | /** @test **/ 40 | public function it_can_encode_the_payload_with_replaced_values() 41 | { 42 | $this->payload->with(['test'=>123], true); 43 | 44 | $this->assertEquals('eyJ0ZXN0IjoxMjN9', $this->payload->make()); 45 | } 46 | 47 | /** @test **/ 48 | public function it_can_access_claims_through_magic_get() 49 | { 50 | $this->assertEquals('2012-12-21T12:00:00.000000Z', $this->payload->iat->toISOString()); 51 | } 52 | 53 | /** @test **/ 54 | public function it_will_set_the_default_issued_at_and_expiration_times() 55 | { 56 | $expireIn = 5; 57 | $this->overrideConfiguration(['jwt.expires' => $expireIn]); 58 | 59 | $payload = $this->app->make(Payload::class); 60 | 61 | $this->assertEquals($expireIn, $payload->iat->diffInMinutes($payload->exp)); 62 | } 63 | 64 | /** @test **/ 65 | public function it_can_be_json_encoded() 66 | { 67 | $this->assertJsonStringEqualsJsonString('{"iat":"2012-12-21T12:00:00.000000Z","exp":"2012-12-21T12:15:00.000000Z"}', json_encode($this->payload)); 68 | } 69 | 70 | /** @test **/ 71 | public function it_will_expose_claims_on_dump() 72 | { 73 | $dump = $this->capture("var_dump", $this->payload); 74 | 75 | $this->assertStringContainsString(Payload::class, $dump); 76 | $this->assertStringContainsString("iat", $dump); 77 | $this->assertStringContainsString("exp", $dump); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Unit/Sections/SignatureTest.php: -------------------------------------------------------------------------------- 1 | expectException(JWTAlgorithmNotSupported::class); 17 | $this->makeSignatureWithAlgorithm('test'); 18 | } 19 | 20 | /** 21 | * @test 22 | * @param string $algorithm 23 | * @dataProvider supportedAlgorithms 24 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 25 | */ 26 | public function it_can_sign_with_all_supported_algorithms(string $algorithm) 27 | { 28 | /** @var Signature $signature */ 29 | $signature = $this->makeSignatureWithAlgorithm($algorithm); 30 | 31 | /** @var Header $header */ 32 | $header = app()->make(Header::class); 33 | /** @var Payload $payload */ 34 | $payload = app()->make(Payload::class); 35 | 36 | $hashedSignature = $signature->sign($header, $payload); 37 | $manualSignature = hash_hmac($algorithm, $header->make(). '.' . $payload->make(), config('jwt.secret')); 38 | 39 | $this->assertTrue(hash_equals($manualSignature, $hashedSignature)); 40 | } 41 | 42 | /** 43 | * All the supported hash algorithms. 44 | * 45 | * @return array 46 | */ 47 | public function supportedAlgorithms() 48 | { 49 | return array_map(function($value){ 50 | return [$value]; 51 | }, hash_hmac_algos()); 52 | } 53 | 54 | /** 55 | * Resolve Signature with provided algorithm. 56 | * 57 | * @param string $algorithm 58 | * @return mixed 59 | */ 60 | private function makeSignatureWithAlgorithm(string $algorithm) 61 | { 62 | // set the algorithm type in the config 63 | $this->overrideConfiguration(['jwt.algorithm' => $algorithm]); 64 | 65 | return $this->app->make(Signature::class); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Unit/TokenTest.php: -------------------------------------------------------------------------------- 1 | token = $this->app->make(Token::class); 23 | } 24 | 25 | /** @test **/ 26 | public function it_will_magically_call_generator_methods() 27 | { 28 | $this->assertIsString($this->token->create()); 29 | $this->assertIsString($this->token->withPayload(['test'=>'payload'])->create()); 30 | $this->assertIsString($this->token->withHeader(['test'=>'header'])->create()); 31 | } 32 | 33 | /** @test **/ 34 | public function it_will_magically_call_parser_methods() 35 | { 36 | $token = $this->token->create(); 37 | 38 | $this->assertInstanceOf(Payload::class, $this->token->payload($token)); 39 | $this->assertInstanceOf(Header::class, $this->token->header($token)); 40 | } 41 | 42 | /** @test **/ 43 | public function it_will_magically_call_validator_methods() 44 | { 45 | $token = $this->token->create(); 46 | 47 | $this->assertTrue($this->token->validate($token)); 48 | } 49 | 50 | /** @test */ 51 | public function it_should_throw_an_exception_on_invalid_method_call() 52 | { 53 | $this->expectException(JWTMethodNotSupported::class); 54 | $this->token->invalid(); 55 | } 56 | 57 | /** @test **/ 58 | public function it_can_recover_from_failed_magic_calls() 59 | { 60 | $this->expectException(JWTMethodNotSupported::class); 61 | $this->token->invalid(); 62 | $this->assertIsString($this->token->create()); 63 | } 64 | } -------------------------------------------------------------------------------- /tests/routes.php: -------------------------------------------------------------------------------- 1 | get('test-jwt-route', function() { 4 | return 'JWT SUCCESS'; 5 | }); --------------------------------------------------------------------------------