diff --git a/.gitattributes b/.gitattributes index ebcadc98d..8f6a4572a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,13 @@ * text=auto /tests export-ignore +/docs export-ignore /.codecov.yml export-ignore /.editorconfig export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.styleci.yml export-ignore /.travis.yml export-ignore -/CODE_OF_CONDUCT.md export-ignore -/CONTRIBUTING.md export-ignore +/.github export-ignore /phpunit.xml.dist export-ignore /README.md export-ignore diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 000000000..228097987 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,44 @@ +name: Run tests + +on: + push: + +jobs: + php-tests: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + php: [7.4] + laravel: [6.*, 7.*] + os: [ubuntu-latest, windows-latest] + coverage: ['none', 'xdebug'] + + name: '[P${{ matrix.php }}] [L${{ matrix.laravel }}] [${{ matrix.os }}] [${{ matrix.coverage }}]' + + steps: + - name: Checkout code + uses: actions/checkout@v1 + + - name: Setup PHP + uses: shivammathur/setup-php@v1 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, gmp, exif, iconv, imagick + coverage: ${{ matrix.coverage }} + + - name: Install dependencies + run: | + composer require "illuminate/contracts:${{ matrix.laravel }}" --no-interaction --no-update + composer update --prefer-stable --prefer-dist --no-interaction --no-suggest + + - name: Execute tests + run: composer test:ci + + - name: Upload coverage + uses: codecov/codecov-action@v1 + if: matrix.coverage == 'xdebug' + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + yml: ./codecov.yml diff --git a/.gitignore b/.gitignore index 8830b4835..7335f3b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ composer.lock build coverage.xml +/.phpunit.* diff --git a/.styleci.yml b/.styleci.yml index 8d6af11ab..3377c4db4 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -6,5 +6,4 @@ enabled: - phpdoc_order - phpdoc_separation - unalign_double_arrow - -linting: true + - php_unit_no_expectation_annotation diff --git a/.travis.yml b/.travis.yml index c200bd9d9..69aeaf7f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,64 +15,14 @@ env: matrix: include: - - php: hhvm-3.18 - sudo: required - dist: trusty - env: LARAVEL='5.4.*' XDEBUG=1 - group: edge - - php: 5.5.9 - env: LARAVEL='5.1.*' - - php: 5.5.9 - env: LARAVEL='5.2.*' - - php: 5.5 - env: LARAVEL='5.1.*' - - php: 5.5 - env: LARAVEL='5.2.*' - - php: 5.6 - env: LARAVEL='5.1.*' - - php: 5.6 - env: LARAVEL='5.2.*' - - php: 5.6 - env: LARAVEL='5.3.*' - - php: 5.6 - env: LARAVEL='5.4.*' - - php: 7.0 - env: LARAVEL='5.1.*' - - php: 7.0 - env: LARAVEL='5.2.*' - - php: 7.0 - env: LARAVEL='5.3.*' - - php: 7.0 - env: LARAVEL='5.4.*' - - php: 7.0 - env: LARAVEL='5.5.*' - - php: 7.1 - env: LARAVEL='5.1.*' - - php: 7.1 - env: LARAVEL='5.2.*' - - php: 7.1 - env: LARAVEL='5.3.*' - - php: 7.1 - env: LARAVEL='5.4.*' - - php: 7.1 - env: LARAVEL='5.5.*' - - php: 7.1 - env: LARAVEL='5.6.*' - - php: 7.2 - env: LARAVEL='5.1.*' XDEBUG=1 - - php: 7.2 - env: LARAVEL='5.2.*' XDEBUG=1 - - php: 7.2 - env: LARAVEL='5.3.*' XDEBUG=1 - - php: 7.2 - env: LARAVEL='5.4.*' XDEBUG=1 - - php: 7.2 - env: LARAVEL='5.5.*' XDEBUG=1 - - php: 7.2 - env: COVERAGE=1 LARAVEL='5.6.*' XDEBUG=1 + - php: 7.4 + env: LARAVEL='5.7.*' XDEBUG=1 + - php: 7.4 + env: LARAVEL='5.8.*' XDEBUG=1 + - php: 7.4 + env: COVERAGE=1 LARAVEL='6.*' XDEBUG=1 fast_finish: true - before_install: - if [[ $XDEBUG = 0 ]]; then phpenv config-rm xdebug.ini; fi - travis_retry composer self-update @@ -82,8 +32,7 @@ install: - travis_retry composer install --prefer-dist --no-interaction --no-suggest script: - - if [[ $LARAVEL = '5.1.*' ]]; then composer test:ci -- --exclude-group laravel-5.2; fi - - if [[ $LARAVEL != '5.1.*' ]]; then composer test:ci; fi + - composer test:ci after_success: - if [[ $COVERAGE = 1 ]]; then bash <(curl -s https://codecov.io/bash); fi diff --git a/LICENSE b/LICENSE index ebd746db8..90712983c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2018 Sean Tymon +Copyright (c) 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 diff --git a/README.md b/README.md index 776e31529..14539f734 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ ![jwt-auth-banner](https://cloud.githubusercontent.com/assets/1801923/9915273/119b9350-5cae-11e5-850b-c941cac60b32.png) -[![Build Status](http://img.shields.io/travis/tymondesigns/jwt-auth/master.svg?style=flat-square)](https://travis-ci.org/tymondesigns/jwt-auth) -[![Codecov branch](https://img.shields.io/codecov/c/github/tymondesigns/jwt-auth/develop.svg?style=flat-square)](https://codecov.io/github/tymondesigns/jwt-auth) -[![StyleCI](https://styleci.io/repos/23680678/shield?style=flat-square)](https://styleci.io/repos/23680678) -[![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) -[![Dependency Status](https://www.versioneye.com/php/tymon:jwt-auth/dev-develop/badge?style=flat-square)](https://www.versioneye.com/php/tymon:jwt-auth/dev-develop) -[![PHP-Eye](https://php-eye.com/badge/tymon/jwt-auth/tested.svg?style=flat-square)](https://php-eye.com/package/tymon/jwt-auth) +[![Build Status](http://img.shields.io/travis/tymondesigns/jwt-auth/master.svg?style=flat-square&logo=travis)](https://travis-ci.org/tymondesigns/jwt-auth) +[![Codecov branch](https://img.shields.io/codecov/c/github/tymondesigns/jwt-auth/develop.svg?style=flat-square&logo=codecov)](https://codecov.io/github/tymondesigns/jwt-auth) +[![StyleCI](https://styleci.io/repos/23680678/shield?style=flat-square&logo=styleci)](https://styleci.io/repos/23680678) +[![Latest Version](http://img.shields.io/packagist/v/tymon/jwt-auth.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/tymon/jwt-auth) +[![Latest Dev Version](https://img.shields.io/packagist/vpre/tymon/jwt-auth.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/tymon/jwt-auth#dev-develop) +[![Monthly Downloads](https://img.shields.io/packagist/dm/tymon/jwt-auth.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/tymon/jwt-auth) ## Documentation diff --git a/composer.json b/composer.json index f40982825..3ae104869 100644 --- a/composer.json +++ b/composer.json @@ -23,24 +23,32 @@ } ], "require": { - "php": "^5.5.9 || ^7.0", - "illuminate/auth": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.*", - "illuminate/contracts": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.*", - "illuminate/http": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.*", - "illuminate/support": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.*", + "php": "^7.4|^8.0.0", + "illuminate/contracts": "^6|^7", + "illuminate/http": "^6|^7", + "illuminate/support": "^6|^7", "lcobucci/jwt": "^3.2", - "namshi/jose": "^7.0", - "nesbot/carbon": "^1.0" + "nesbot/carbon": "^2.0", + "web-token/jwt-checker": "^2.1", + "web-token/jwt-core": "^2.1", + "web-token/jwt-key-mgmt": "^2.1", + "web-token/jwt-signature": "^2.1", + "web-token/jwt-signature-algorithm-ecdsa": "^2.1", + "web-token/jwt-signature-algorithm-hmac": "^2.1", + "web-token/jwt-signature-algorithm-rsa": "^2.1" }, "require-dev": { - "cartalyst/sentinel": "2.0.*", - "illuminate/console": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.*", - "illuminate/database": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.*", - "illuminate/routing": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.* || 5.6.*", - "mockery/mockery": ">=0.9.9", - "phpunit/phpunit": "~4.8 || ~6.0" + "illuminate/auth": "^6|^7", + "illuminate/console": "^6|^7", + "illuminate/database": "^6|^7", + "illuminate/routing": "^6|^7", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.0" }, "autoload": { + "files": [ + "src/Support/helpers.php" + ], "psr-4": { "Tymon\\JWTAuth\\": "src/" } @@ -52,12 +60,12 @@ }, "extra": { "branch-alias": { - "dev-develop": "1.0-dev" + "2.0": "2.0-dev" }, "laravel": { "aliases": { - "JWTAuth": "Tymon\\JWTAuth\\Facades\\JWTAuth", - "JWTFactory": "Tymon\\JWTAuth\\Facades\\JWTFactory" + "JWTManager": "Tymon\\JWTAuth\\Facades\\JWTManager", + "JWTProvider": "Tymon\\JWTAuth\\Facades\\JWTProvider" }, "providers": [ "Tymon\\JWTAuth\\Providers\\LaravelServiceProvider" diff --git a/config/config.php b/config/config.php index c4ea92db1..6525071b4 100644 --- a/config/config.php +++ b/config/config.php @@ -9,6 +9,8 @@ * file that was distributed with this source code. */ +use Tymon\JWTAuth\Claims; + return [ /* @@ -91,7 +93,7 @@ |-------------------------------------------------------------------------- | | Specify the length of time (in minutes) that the token will be valid for. - | Defaults to 1 hour. + | Defaults to 30 minutes. | | You can also set this to null, to yield a never expiring token. | Some people may want this behaviour for e.g. a mobile app. @@ -100,26 +102,21 @@ | */ - 'ttl' => env('JWT_TTL', 60), + 'ttl' => env('JWT_TTL', 30), /* |-------------------------------------------------------------------------- - | Refresh time to live + | Max refresh period |-------------------------------------------------------------------------- | - | 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. + | Specify the length of time (in minutes) that the token will be + | refreshable for. | - | You can also set this to null, to yield an infinite refresh time. - | Some may want this instead of never expiring tokens for e.g. a mobile app. - | This is not particularly recommended, so make sure you have appropriate - | systems in place to revoke the token if necessary. + | Defaults to null, which will allow tokens to be refreshable forever. | */ - 'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), + 'max_refresh_period' => env('JWT_MAX_REFRESH_PERIOD'), /* |-------------------------------------------------------------------------- @@ -128,8 +125,11 @@ | | Specify the hashing algorithm that will be used to sign the token. | - | See here: https://github.com/namshi/jose/tree/master/src/Namshi/JOSE/Signer/OpenSSL - | for possible values. + | Possible values: + | + | 'HS256', 'HS384', 'HS512', + | 'RS256', 'RS384', 'RS512', + | 'ES256', 'ES384', 'ES512' | */ @@ -147,30 +147,11 @@ */ 'required_claims' => [ - 'iss', - 'iat', - 'exp', - 'nbf', - 'sub', - 'jti', - ], - - /* - |-------------------------------------------------------------------------- - | Persistent Claims - |-------------------------------------------------------------------------- - | - | Specify the claim keys to be persisted when refreshing a token. - | `sub` and `iat` will automatically be persisted, in - | addition to the these claims. - | - | Note: If a claim does not exist then it will be ignored. - | - */ - - 'persistent_claims' => [ - // 'foo', - // 'bar', + Claims\Issuer::NAME, + Claims\IssuedAt::NAME, + Claims\Expiration::NAME, + Claims\Subject::NAME, + Claims\JwtId::NAME, ], /* @@ -178,11 +159,12 @@ | Lock Subject |-------------------------------------------------------------------------- | - | This will determine whether a `prv` claim is automatically added to - | the token. The purpose of this is to ensure that if you have multiple - | authentication models e.g. `App\User` & `App\OtherPerson`, then we - | should prevent one authentication request from impersonating another, - | if 2 tokens happen to have the same id across the 2 different models. + | This will determine whether a HashedSubject (hsu) claim is automatically + | added to the token. The purpose of this is to ensure that if you have + | multiple authentication models e.g. `App\User` & `App\OtherPerson`, + | then we should prevent one authentication request from impersonating + | another, if 2 tokens happen to have the same id across the 2 different + | models. | | Under specific circumstances, you may want to disable this behaviour | e.g. if you only have one authentication model, then you would save @@ -276,17 +258,6 @@ 'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class, - /* - |-------------------------------------------------------------------------- - | Authentication Provider - |-------------------------------------------------------------------------- - | - | Specify the provider that is used to authenticate users. - | - */ - - 'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class, - /* |-------------------------------------------------------------------------- | Storage Provider diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 000000000..c97a55358 --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,11 @@ +# jwt-auth + +> JSON Web Token Authentication for Laravel & Lumen + +- Simple and lightweight (~21kB gzipped) +- No statically built html files +- Multiple themes + +[Get Started](laravel-installation.md) +[GitHub](https://github.com/tymondesigns/jwt-auth) + diff --git a/docs/_navbar.md b/docs/_navbar.md new file mode 100644 index 000000000..911b87045 --- /dev/null +++ b/docs/_navbar.md @@ -0,0 +1,9 @@ +- **Getting started** + - [Laravel Installation](laravel-installation.md) + - [Lumen Installation](lumen-installation.md) + - [Quick Start](quick-start.md) + - [Configuration](configuration.md) + - [Exception handling](exception-handling.md) + +- **Versions** + - [1.*](#/1.0) diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 000000000..be8fd8941 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,15 @@ +- **Getting started** + - [Laravel Installation](laravel-installation.md) + - [Lumen Installation](lumen-installation.md) + - [Quick Start](quick-start.md) + - [Configuration](configuration.md) + +- **Usage** + - [Auth Guard](auth-guard.md) + - [Exception handling](exception-handling.md) + +- [Resources](resources.md) + +- **Links** + - [![Github](https://icongr.am/simple/github.svg?color=808080&size=16)Github](https://github.com/tymondesigns/jwt-auth) + - [![Twitter](https://icongr.am/simple/twitter.svg?colored&size=16)@tymondesigns](http://twitter.com/tymondesigns) diff --git a/docs/auth-guard.md b/docs/auth-guard.md index 98f0668dc..3f335bdb5 100644 --- a/docs/auth-guard.md +++ b/docs/auth-guard.md @@ -7,8 +7,12 @@ The following methods are available on the Auth guard instance. Attempt to authenticate a user via some credentials. ```php +$credentials = request(['email', 'password']); + // Generate a token for the user if the credentials are valid -$token = auth()->attempt($credentials); +if ($token = auth()->attempt($credentials)) { + // Credentials are good +} ``` This will return either a jwt or `null` @@ -102,9 +106,21 @@ $payload = auth()->payload(); // then you can access the claims directly e.g. $payload->get('sub'); // = 123 + +// array access to claims $payload['jti']; // = 'asfe4fq434asdf' + +// invokable access to claims $payload('exp') // = 123456 -$payload->toArray(); // = ['sub' => 123, 'exp' => 123456, 'jti' => 'asfe4fq434asdf'] etc + +$payload->toArray(); // = ['sub' => 123, 'exp' => 123456, 'jti' => 'asfe4...'] etc +``` + +?> Payloads are immutable and cannot be altered after they have been created. + +```php +// This will throw a \Tymon\JWTAuth\Exceptions\PayloadException +$payload['sub'] = 1; ``` ### validate() diff --git a/docs/configuration.md b/docs/configuration.md index d58e9a10b..510e9c988 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,3 +1,5 @@ +# Configuration + Let's review some of the options in the `config/jwt.php` file that we published earlier. I won't go through all of the options here since [the file itself](https://github.com/tymondesigns/jwt-auth/blob/1.0.0-beta.2/config/config.php) is pretty well documented. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..c7d9bedb2 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,72 @@ + + + + + JSON Web Token Authentication for Laravel & Lumen + + + + + + + + + +
+ + + + + + + + + + diff --git a/docs/index.md b/docs/index.md index 9486484f4..48f65ad6b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,4 @@ -JSON Web Token Authentication for Laravel & Lumen + + ![jwt-auth-banner](https://cloud.githubusercontent.com/assets/1801923/9915273/119b9350-5cae-11e5-850b-c941cac60b32.png) diff --git a/docs/laravel-installation.md b/docs/laravel-installation.md index 2d473cdbe..4b6ba1e6e 100644 --- a/docs/laravel-installation.md +++ b/docs/laravel-installation.md @@ -1,4 +1,6 @@ -### Install via composer +# Laravel Installation + +## Install via composer Run the following command to pull in the latest version: @@ -8,13 +10,12 @@ composer require tymon/jwt-auth ------------------------------------------------------------------------------- -### Add service provider ( Laravel 5.4 or below ) +## Add service provider ( Laravel 5.4 or below ) Add the service provider to the `providers` array in the `config/app.php` config file as follows: ```php 'providers' => [ - ... Tymon\JWTAuth\Providers\LaravelServiceProvider::class, @@ -23,7 +24,7 @@ Add the service provider to the `providers` array in the `config/app.php` config ------------------------------------------------------------------------------- -### Publish the config +## Publish the config Run the following command to publish the package config file: @@ -35,7 +36,7 @@ You should now have a `config/jwt.php` file that allows you to configure the bas ------------------------------------------------------------------------------- -### Generate secret key +## Generate secret key I have included a helper command to generate a key for you: diff --git a/docs/lumen-installation.md b/docs/lumen-installation.md index 039abc521..b41f5758c 100644 --- a/docs/lumen-installation.md +++ b/docs/lumen-installation.md @@ -1,4 +1,6 @@ -### Install via composer +# Lumen Installation + +## Install via composer Run the following command to pull in the latest version: @@ -8,7 +10,7 @@ composer require tymon/jwt-auth ------------------------------------------------------------------------------- -### Bootstrap file changes. +## Bootstrap file changes. Add the following snippet to the `bootstrap/app.php` file under the providers section as follows: @@ -30,7 +32,7 @@ $app->routeMiddleware([ ------------------------------------------------------------------------------- -### Generate secret key +## Generate secret key I have included a helper command to generate a key for you: diff --git a/docs/quick-start.md b/docs/quick-start.md index 9a7b3f11e..056c5f81b 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -1,7 +1,9 @@ +# Quick Start + Before continuing, make sure you have installed the package as per the installation instructions for [Laravel](laravel-installation) or [Lumen](lumen-installation). -### Update your User model +## Update your User model Firstly you need to implement the `Tymon\JWTAuth\Contracts\JWTSubject` contract on your User model, which requires that you implement the 2 methods `getJWTIdentifier()` and `getJWTCustomClaims()`. @@ -46,9 +48,9 @@ class User extends Authenticatable implements JWTSubject } ``` -### Configure Auth guard +## Configure Auth guard -*Note: This will only work if you are using Laravel 5.2 and above.* +!> This will only work if you are using Laravel 5.2 and above.* Inside the `config/auth.php` file you will need to make a few changes to configure Laravel to use the `jwt` guard to power your application authentication. @@ -76,27 +78,23 @@ as the default. We can now use Laravel's built in Auth system, with jwt-auth doing the work behind the scenes! -### Add some basic authentication routes +## Add some basic authentication routes First let's add some routes in `routes/api.php` as follows: ```php Route::group([ - 'middleware' => 'api', 'prefix' => 'auth' - ], function ($router) { - Route::post('login', 'AuthController@login'); Route::post('logout', 'AuthController@logout'); Route::post('refresh', 'AuthController@refresh'); Route::post('me', 'AuthController@me'); - }); ``` -### Create the AuthController +## Create the AuthController Then create the `AuthController`, either manually or by running the artisan command: @@ -123,7 +121,7 @@ class AuthController extends Controller */ public function __construct() { - $this->middleware('auth:api', ['except' => ['login']]); + $this->middleware('auth:api')->except('login'); } /** @@ -205,7 +203,7 @@ credentials and see a response like: This token can then be used to make authenticated requests to your application. -### Authenticated requests +## Authenticated requests There are a number of ways to send the token via http: diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 9cedaccd8..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,11 +0,0 @@ -site_name: jwt-auth -pages: - - Home: index.md - - Laravel Installation: laravel-installation.md - - Lumen Installation (incomplete): lumen-installation.md - - Quick start: quick-start.md - - Auth guard: auth-guard.md - - Configuration: configuration.md - - Exception Handling: exception-handling.md - - Resources: resources.md -theme: readthedocs diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ee9ce0773..01a0291ba 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -34,7 +34,7 @@ - + diff --git a/src/Blacklist.php b/src/Blacklist.php index 78bc273e4..f387702d5 100644 --- a/src/Blacklist.php +++ b/src/Blacklist.php @@ -1,5 +1,7 @@ hasKey('exp')) { - return $this->addForever($payload); + if (! $payload->hasKey(Expiration::NAME)) { + $this->addForever($payload); + + return; + } + + // if we have already added this token to the blacklist + if (! empty($this->storage->get($this->getKey($payload)))) { + return; } $this->storage->add( $this->getKey($payload), - ['valid_until' => $this->getGraceTimestamp()], + [static::VALID_UNTIL => $this->getGraceTimestamp()], $this->getMinutesUntilExpired($payload) ); - - return true; } /** * Get the number of minutes until the token expiry. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return int */ - protected function getMinutesUntilExpired(Payload $payload) + protected function getMinutesUntilExpired(Payload $payload): int { - $exp = Utils::timestamp($payload['exp']); - $iat = Utils::timestamp($payload['iat']); + $exp = timestamp($payload[Expiration::NAME]); - // get the latter of the two expiration dates and find - // the number of minutes until the expiration date, + // find the number of minutes until the expiration date, // plus 1 minute to avoid overlap - return $exp->max($iat->addMinutes($this->refreshTTL))->addMinute()->diffInMinutes(); + return now() + ->subMinute() + ->diffInRealMinutes($exp); } /** * Add the token (jti claim) to the blacklist indefinitely. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return bool */ - public function addForever(Payload $payload) + public function addForever(Payload $payload): void { - $this->storage->forever($this->getKey($payload), 'forever'); - - return true; + $this->storage->forever($this->getKey($payload), static::FOREVER); } /** * Determine whether the token has been blacklisted. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return bool */ - public function has(Payload $payload) + public function has(Payload $payload): bool { $val = $this->storage->get($this->getKey($payload)); // exit early if the token was blacklisted forever, - if ($val === 'forever') { + if ($val === static::FOREVER) { return true; } // check whether the expiry + grace has past - return ! empty($val) && ! Utils::isFuture($val['valid_until']); + return ! empty($val) && ! is_future($val[static::VALID_UNTIL]); } /** - * Remove the token (jti claim) from the blacklist. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return bool + * Remove the token from the blacklist. */ - public function remove(Payload $payload) + public function remove(Payload $payload): void { - return $this->storage->destroy($this->getKey($payload)); + $this->storage->destroy($this->getKey($payload)); } /** * Remove all tokens from the blacklist. - * - * @return bool */ - public function clear() + public function clear(): void { $this->storage->flush(); - - return true; } /** * Get the timestamp when the blacklist comes into effect * This defaults to immediate (0 seconds). - * - * @return int */ - protected function getGraceTimestamp() + protected function getGraceTimestamp(): int { - return Utils::now()->addSeconds($this->gracePeriod)->getTimestamp(); + return now() + ->addSeconds($this->gracePeriod) + ->getTimestamp(); } /** * Set the grace period. - * - * @param int $gracePeriod - * - * @return $this */ - public function setGracePeriod($gracePeriod) + public function setGracePeriod(int $gracePeriod): self { - $this->gracePeriod = (int) $gracePeriod; + $this->gracePeriod = $gracePeriod; return $this; } /** * Get the grace period. - * - * @return int */ - public function getGracePeriod() + public function getGracePeriod(): int { return $this->gracePeriod; } /** * Get the unique key held within the blacklist. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return mixed */ - public function getKey(Payload $payload) + public function getKey(Payload $payload): string { - return $payload($this->key); + return (string) $payload[$this->key]; } /** * Set the unique key held within the blacklist. - * - * @param string $key - * - * @return $this */ - public function setKey($key) + public function setKey(string $key): self { - $this->key = value($key); + $this->key = $key; return $this; } - - /** - * Set the refresh time limit. - * - * @param int $ttl - * - * @return $this - */ - public function setRefreshTTL($ttl) - { - $this->refreshTTL = (int) $ttl; - - return $this; - } - - /** - * Get the refresh time limit. - * - * @return int - */ - public function getRefreshTTL() - { - return $this->refreshTTL; - } } diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 000000000..d54755fc6 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,283 @@ + + * + * 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 Illuminate\Support\Arr; +use Tymon\JWTAuth\Claims\Factory as ClaimFactory; +use Tymon\JWTAuth\Contracts\JWTSubject; +use function Tymon\JWTAuth\Support\now; +use function Tymon\JWTAuth\Support\timestamp; + +class Builder +{ + /** + * The request. + */ + protected Request $request; + + /** + * The TTL in minutes. + */ + protected int $ttl = 30; + + /** + * Lock the subject. + */ + protected bool $lockSubject = true; + + /** + * Time leeway in seconds. + */ + protected int $leeway = 0; + + /** + * Max refresh period in minutes. + */ + protected ?int $maxRefreshPeriod = null; + + /** + * The required claims. + */ + protected array $requiredClaims = []; + + /** + * The default claims to add. + */ + protected array $defaultClaims = [ + Claims\IssuedAt::NAME, + Claims\JwtId::NAME, + Claims\Issuer::NAME, + ]; + + /** + * Any custom validators. + */ + protected array $customValidators = []; + + /** + * Constructor. + */ + public function __construct(Request $request) + { + $this->request = $request; + } + + /** + * Create a Payload instance for a given array of claims. + */ + public function make($claims = []): Payload + { + return Factory::make($claims, $this->getOptions()); + } + + /** + * Create a Payload instance for a given subject. + */ + public function makeForSubject(JWTSubject $subject, array $claims = []): Payload + { + return $this->make(array_merge( + $this->getDefaultClaims(), + $this->getClaimsForSubject($subject), + $subject->getJWTCustomClaims(), // custom claims from JWTSubject method + $claims // custom claims from inline setter + )); + } + + /** + * Build the claims to go into the refreshed token. + */ + public function buildRefreshClaims(Payload $payload): array + { + return array_merge($payload->toArray(), [ + Claims\JwtId::NAME => ClaimFactory::get(Claims\JwtId::NAME), + Claims\Expiration::NAME => timestamp($payload[Claims\Expiration::NAME]) + ->addMinutes($this->getTTL()) + ->getTimestamp(), + ]); + } + + /** + * Get the builder options. + */ + public function getOptions(): Options + { + return new Options([ + Options::LEEWAY => $this->leeway, + Options::REQUIRED_CLAIMS => $this->requiredClaims, + Options::MAX_REFRESH_PERIOD => $this->maxRefreshPeriod, + Options::VALIDATORS => $this->customValidators, + ]); + } + + /** + * Get the default claims to add. + */ + protected function getDefaultClaims(): array + { + if ($key = array_search(Claims\Issuer::NAME, $this->defaultClaims)) { + $iss = Arr::pull($this->defaultClaims, $key); + } + + return array_merge( + $this->defaultClaims, + // only add the iss claim if it exists in the default claims. + isset($iss) ? [$this->issClaim()] : [], + // only add exp claim if the ttl is not null + $this->getTTL() !== null ? [$this->expClaim()] : [] + ); + } + + /** + * Get the issuer (iss) claim. + */ + protected function issClaim(): Claims\Issuer + { + return ClaimFactory::get( + Claims\Issuer::NAME, + $this->request->getHost(), + $this->getOptions() + ); + } + + /** + * Get the expiration (exp) claim. + */ + protected function expClaim(): Claims\Expiration + { + return ClaimFactory::get( + Claims\Expiration::NAME, + now()->addMinutes($this->getTTL())->getTimestamp(), + $this->getOptions() + ); + } + + /** + * Get the claims associated with a given subject. + */ + protected function getClaimsForSubject(JWTSubject $subject): array + { + return array_merge([ + Claims\Subject::NAME => $subject->getJWTIdentifier(), + ], $this->lockSubject ? [ + Claims\HashedSubject::NAME => $this->hashSubjectModel($subject), + ] : []); + } + + /** + * Hash the subject model and return it. + * + * @param string|object $model + */ + public function hashSubjectModel($model): string + { + return sha1(is_object($model) ? get_class($model) : $model); + } + + /** + * Set the request instance. + */ + public function setRequest(Request $request): self + { + $this->request = $request; + + return $this; + } + + /** + * Get the request instance. + */ + public function getRequest(): Request + { + return $this->request; + } + + /** + * Set whether the subject should be "locked". + */ + public function lockSubject(bool $lock): self + { + $this->lockSubject = $lock; + + return $this; + } + + /** + * Set the token ttl (in minutes). + */ + public function setTTL(?int $ttl): self + { + $this->ttl = $ttl; + + return $this; + } + + /** + * Get the token ttl. + */ + public function getTTL(): ?int + { + return $this->ttl; + } + + /** + * Set the default claims. + */ + public function setDefaultClaims(array $claims = []): self + { + $this->defaultClaims = $claims; + + return $this; + } + + /** + * Set the default claims. + */ + public function setRequiredClaims(array $claims = []): self + { + $this->requiredClaims = $claims; + + return $this; + } + + /** + * Set the leeway in seconds. + */ + public function setLeeway(int $leeway): self + { + $this->leeway = $leeway; + + return $this; + } + + /** + * Set the max refresh period in minutes. + */ + public function setMaxRefreshPeriod(?int $period): self + { + $this->maxRefreshPeriod = $period; + + return $this; + } + + /** + * Add a custom validator. + */ + public function setCustomValidator(string $key, callable $validator): self + { + $this->customValidators[$key] = $validator; + + return $this; + } +} diff --git a/src/Claims/Audience.php b/src/Claims/Audience.php index b34477558..d9fdc76ef 100644 --- a/src/Claims/Audience.php +++ b/src/Claims/Audience.php @@ -14,7 +14,7 @@ class Audience extends Claim { /** - * {@inheritdoc} + * @var string */ - protected $name = 'aud'; + const NAME = 'aud'; } diff --git a/src/Claims/Claim.php b/src/Claims/Claim.php index cb75f1d0d..514d8ec7a 100644 --- a/src/Claims/Claim.php +++ b/src/Claims/Claim.php @@ -1,5 +1,7 @@ value = $this->validateCreate($value); @@ -70,12 +66,8 @@ public function getValue() /** * Set the claim name. - * - * @param string $name - * - * @return $this */ - public function setName($name) + public function setName(string $name): ClaimContract { $this->name = $name; @@ -84,20 +76,18 @@ public function setName($name) /** * Get the claim name. - * - * @return string */ - public function getName() + public function getName(): string { - return $this->name; + return $this->name ?? static::NAME; } /** - * Validate the claim in a standalone Claim context. + * Validate the claim for creation. * * @param mixed $value * - * @return bool + * @return mixed */ public function validateCreate($value) { @@ -105,56 +95,53 @@ public function validateCreate($value) } /** - * Validate the Claim within a Payload context. - * - * @return bool + * Check the claim when verifying the validity of the payload. */ - public function validatePayload() + public function verify(): void { - return $this->getValue(); + // } /** - * Validate the Claim within a refresh context. - * - * @param int $refreshTTL - * - * @return bool + * Create an instance of the claim. */ - public function validateRefresh($refreshTTL) + public static function make($value = null): ClaimContract { - return $this->getValue(); + return new static($value); } /** * Checks if the value matches the claim. * * @param mixed $value - * @param bool $strict - * - * @return bool */ - public function matches($value, $strict = true) + public function matches($value, bool $strict = true): bool { - return $strict ? $this->value === $value : $this->value == $value; + return $strict + ? $this->value === $value + : $this->value == $value; + } + + /** + * Checks if the name matches the claim. + */ + public function matchesName(string $name): bool + { + return $this->getName() === $name; } /** * Convert the object into something JSON serializable. - * - * @return array */ - public function jsonSerialize() + public function jsonSerialize(): array { return $this->toArray(); } /** * Build a key value array comprising of the claim name and value. - * - * @return array */ - public function toArray() + public function toArray(): array { return [$this->getName() => $this->getValue()]; } @@ -163,20 +150,16 @@ public function toArray() * Get the claim as JSON. * * @param int $options - * - * @return string */ - public function toJson($options = JSON_UNESCAPED_SLASHES) + public function toJson($options = JSON_UNESCAPED_SLASHES): string { return json_encode($this->toArray(), $options); } /** * Get the payload as a string. - * - * @return string */ - public function __toString() + public function __toString(): string { return $this->toJson(); } diff --git a/src/Claims/Collection.php b/src/Claims/Collection.php index 7ec2da156..aee1c6c00 100644 --- a/src/Claims/Collection.php +++ b/src/Claims/Collection.php @@ -1,5 +1,7 @@ getArrayableItems($items)); - } - /** * Get a Claim instance by it's unique name. - * - * @param string $name - * @param callable $callback - * @param mixed $default - * - * @return \Tymon\JWTAuth\Claims\Claim */ - public function getByClaimName($name, callable $callback = null, $default = null) + public function getByClaimName(string $name, ...$args): ?ClaimContract { - return $this->filter(function (Claim $claim) use ($name) { - return $claim->getName() === $name; - })->first($callback, $default); + return $this->filter->matchesName($name) + ->first(...$args); } /** - * Validate each claim under a given context. - * - * @param string $context - * - * @return $this + * Verify the validity of each claim. */ - public function validate($context = 'payload') + public function verify(): self { - $args = func_get_args(); - array_shift($args); - - $this->each(function ($claim) use ($context, $args) { - call_user_func_array( - [$claim, 'validate'.Str::ucfirst($context)], - $args - ); - }); + $this->each->verify(); return $this; } @@ -70,42 +41,31 @@ public function validate($context = 'payload') * Determine if the Collection contains all of the given keys. * * @param mixed $claims - * - * @return bool */ - public function hasAllClaims($claims) + public function hasAllClaims($claims): bool { - return count($claims) && (new static($claims))->diff($this->keys())->isEmpty(); + if (empty($claims)) { + return true; + } + + return (new static($claims)) + ->diff($this->keys()) + ->isEmpty(); } /** * Get the claims as key/val array. - * - * @return array */ - public function toPlainArray() + public function toPlainArray(): array { - return $this->map(function (Claim $claim) { - return $claim->getValue(); - })->toArray(); + return $this->map->getValue() + ->toArray(); } /** * {@inheritdoc} */ - protected function getArrayableItems($items) - { - return $this->sanitizeClaims($items); - } - - /** - * Ensure that the given claims array is keyed by the claim name. - * - * @param mixed $items - * - * @return array - */ - private function sanitizeClaims($items) + protected function getArrayableItems($items): array { $claims = []; foreach ($items as $key => $value) { diff --git a/src/Claims/Custom.php b/src/Claims/Custom.php index 0e43de54f..15e0d42d8 100644 --- a/src/Claims/Custom.php +++ b/src/Claims/Custom.php @@ -1,5 +1,7 @@ setName($name); diff --git a/src/Claims/DatetimeTrait.php b/src/Claims/DatetimeTrait.php index dd2b77d5a..9617f5975 100644 --- a/src/Claims/DatetimeTrait.php +++ b/src/Claims/DatetimeTrait.php @@ -1,5 +1,7 @@ add($value); + $value = now()->add($value); } if ($value instanceof DateTimeInterface) { @@ -61,39 +70,71 @@ public function validateCreate($value) /** * Determine whether the value is in the future. - * - * @param mixed $value - * - * @return bool */ - protected function isFuture($value) + protected function isFuture(int $value): bool { - return Utils::isFuture($value, $this->leeway); + return is_future($value, $this->leeway); } /** * Determine whether the value is in the past. - * - * @param mixed $value - * - * @return bool */ - protected function isPast($value) + protected function isPast(int $value): bool { - return Utils::isPast($value, $this->leeway); + return is_past($value, $this->leeway); } /** * Set the leeway in seconds. - * - * @param int $leeway - * - * @return $this */ - public function setLeeway($leeway) + public function setLeeway(int $leeway): self { $this->leeway = $leeway; return $this; } + + /** + * Get the leeway. + */ + public function getLeeway(): int + { + return $this->leeway; + } + + /** + * Set the max refresh period in minutes. + */ + public function setMaxRefreshPeriod(?int $period): self + { + $this->maxRefreshPeriod = $period; + + return $this; + } + + /** + * Get the max refresh period. + */ + public function getMaxRefreshPeriod(): ?int + { + return $this->maxRefreshPeriod; + } + + /** + * Get the claim value as a Carbon instance. + */ + public function asCarbon(): Carbon + { + return timestamp($this->getValue()); + } + + /** + * Get the claim value as a CarbonInterval instance. + */ + public function asCarbonInterval(): CarbonInterval + { + return now() + ->diffAsCarbonInterval($this->asCarbon()->endOfSecond()) + ->microseconds(0); + } } diff --git a/src/Claims/Expiration.php b/src/Claims/Expiration.php index d15d4b24e..bbdac5f9b 100644 --- a/src/Claims/Expiration.php +++ b/src/Claims/Expiration.php @@ -18,14 +18,14 @@ class Expiration extends Claim use DatetimeTrait; /** - * {@inheritdoc} + * @var string */ - protected $name = 'exp'; + const NAME = 'exp'; /** * {@inheritdoc} */ - public function validatePayload() + public function verify(): void { if ($this->isPast($this->getValue())) { throw new TokenExpiredException('Token has expired'); diff --git a/src/Claims/Factory.php b/src/Claims/Factory.php index 084732b48..6dc292ab2 100644 --- a/src/Claims/Factory.php +++ b/src/Claims/Factory.php @@ -1,5 +1,7 @@ Audience::class, - 'exp' => Expiration::class, - 'iat' => IssuedAt::class, - 'iss' => Issuer::class, - 'jti' => JwtId::class, - 'nbf' => NotBefore::class, - 'sub' => Subject::class, + * The class map. + */ + private static array $classMap = [ + Audience::NAME => Audience::class, + Expiration::NAME => Expiration::class, + IssuedAt::NAME => IssuedAt::class, + Issuer::NAME => Issuer::class, + JwtId::NAME => JwtId::class, + NotBefore::NAME => NotBefore::class, + Subject::NAME => Subject::class, + HashedSubject::NAME => HashedSubject::class, ]; - /** - * Constructor. - * - * @param \Illuminate\Http\Request $request - * - * @return void - */ - public function __construct(Request $request) - { - $this->request = $request; - } - /** * 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) + public static function get(string $name, $value = null, ?Options $options = null): ClaimContract { - if ($this->has($name)) { - $claim = new $this->classMap[$name]($value); + $options ??= new Options(); - return method_exists($claim, 'setLeeway') ? - $claim->setLeeway($this->leeway) : - $claim; - } + $claim = static::has($name) + ? call_user_func([static::$classMap[$name], 'make'], $value) + : new Custom($name, $value); - return new Custom($name, $value); + return static::applyClaimMethods($claim, [ + 'setLeeway' => $options->leeway(), + 'setMaxRefreshPeriod' => $options->maxRefreshPeriod(), + ]); } /** * Check whether the claim exists. - * - * @param string $name - * - * @return bool - */ - public function has($name) - { - return array_key_exists($name, $this->classMap); - } - - /** - * Generate the initial value and return the Claim instance. - * - * @param string $name - * - * @return \Tymon\JWTAuth\Claims\Claim - */ - public function make($name) - { - return $this->get($name, $this->$name()); - } - - /** - * Get the Issuer (iss) claim. - * - * @return string */ - public function iss() + public static function has(string $name): bool { - return $this->request->url(); + return array_key_exists($name, static::$classMap); } /** - * Get the Issued At (iat) claim. - * - * @return int + * Apply a multiple methods to the given claim if they exist. */ - public function iat() + protected static function applyClaimMethods(ClaimContract $claim, array $data): ClaimContract { - return Utils::now()->getTimestamp(); - } - - /** - * Get the Expiration (exp) claim. - * - * @return int - */ - public function exp() - { - return Utils::now()->addMinutes($this->ttl)->getTimestamp(); - } - - /** - * Get the Not Before (nbf) claim. - * - * @return int - */ - public function nbf() - { - return Utils::now()->getTimestamp(); - } - - /** - * Get the JWT Id (jti) claim. - * - * @return string - */ - public function jti() - { - return Str::random(); - } - - /** - * Add a new claim mapping. - * - * @param string $name - * @param string $classPath - * - * @return $this - */ - public function extend($name, $classPath) - { - $this->classMap[$name] = $classPath; - - return $this; - } - - /** - * Set the request instance. - * - * @param \Illuminate\Http\Request $request - * - * @return $this - */ - public function setRequest(Request $request) - { - $this->request = $request; - - return $this; - } - - /** - * 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 leeway in seconds. - * - * @param int $leeway - * - * @return $this - */ - public function setLeeway($leeway) - { - $this->leeway = $leeway; + foreach ($data as $method => $value) { + $claim = method_exists($claim, $method) + ? $claim->{$method}($value) + : $claim; + } - return $this; + return $claim; } } diff --git a/src/Claims/HashedSubject.php b/src/Claims/HashedSubject.php new file mode 100644 index 000000000..6bc2e2046 --- /dev/null +++ b/src/Claims/HashedSubject.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\Claims; + +class HashedSubject extends Claim +{ + /** + * @var string + */ + const NAME = 'hsu'; +} diff --git a/src/Claims/IssuedAt.php b/src/Claims/IssuedAt.php index 6253fe88d..3453eb58b 100644 --- a/src/Claims/IssuedAt.php +++ b/src/Claims/IssuedAt.php @@ -11,9 +11,12 @@ namespace Tymon\JWTAuth\Claims; +use Tymon\JWTAuth\Contracts\Claim as ClaimContract; use Tymon\JWTAuth\Exceptions\InvalidClaimException; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Exceptions\TokenInvalidException; +use function Tymon\JWTAuth\Support\now; +use function Tymon\JWTAuth\Support\timestamp; class IssuedAt extends Claim { @@ -22,9 +25,9 @@ class IssuedAt extends Claim } /** - * {@inheritdoc} + * @var string */ - protected $name = 'iat'; + const NAME = 'iat'; /** * {@inheritdoc} @@ -43,20 +46,24 @@ public function validateCreate($value) /** * {@inheritdoc} */ - public function validatePayload() + public function verify(): void { if ($this->isFuture($this->getValue())) { throw new TokenInvalidException('Issued At (iat) timestamp cannot be in the future'); } + + if ($this->maxRefreshPeriod !== null) { + if (timestamp($this->getValue())->addMinutes($this->maxRefreshPeriod)->isFuture()) { + throw new TokenExpiredException('Token has expired'); + } + } } /** * {@inheritdoc} */ - public function validateRefresh($refreshTTL) + public static function make($value = null): ClaimContract { - if ($this->isPast($this->getValue() + $refreshTTL * 60)) { - throw new TokenExpiredException('Token has expired and can no longer be refreshed'); - } + return new static($value ?? now()->getTimestamp()); } } diff --git a/src/Claims/Issuer.php b/src/Claims/Issuer.php index d1d68cde4..8451f7ec6 100644 --- a/src/Claims/Issuer.php +++ b/src/Claims/Issuer.php @@ -14,7 +14,7 @@ class Issuer extends Claim { /** - * {@inheritdoc} + * @var string */ - protected $name = 'iss'; + const NAME = 'iss'; } diff --git a/src/Claims/JwtId.php b/src/Claims/JwtId.php index e1b93fc9f..a63d0a2c4 100644 --- a/src/Claims/JwtId.php +++ b/src/Claims/JwtId.php @@ -11,10 +11,21 @@ namespace Tymon\JWTAuth\Claims; +use Illuminate\Support\Str; +use Tymon\JWTAuth\Contracts\Claim as ClaimContract; + class JwtId extends Claim { + /** + * @var string + */ + const NAME = 'jti'; + /** * {@inheritdoc} */ - protected $name = 'jti'; + public static function make($value = null): ClaimContract + { + return new static($value ?? Str::random(16)); + } } diff --git a/src/Claims/NotBefore.php b/src/Claims/NotBefore.php index 47fba3de7..d90b43107 100644 --- a/src/Claims/NotBefore.php +++ b/src/Claims/NotBefore.php @@ -11,41 +11,34 @@ namespace Tymon\JWTAuth\Claims; -use Tymon\JWTAuth\Exceptions\InvalidClaimException; +use Tymon\JWTAuth\Contracts\Claim as ClaimContract; use Tymon\JWTAuth\Exceptions\TokenInvalidException; +use function Tymon\JWTAuth\Support\now; class NotBefore extends Claim { - use DatetimeTrait { - validateCreate as commonValidateCreate; - } + use DatetimeTrait; /** - * {@inheritdoc} + * @var string */ - protected $name = 'nbf'; + const NAME = 'nbf'; /** * {@inheritdoc} */ - public function validateCreate($value) + public function verify(): void { - $this->commonValidateCreate($value); - - if ($this->isFuture($value)) { - throw new InvalidClaimException($this); + if ($this->isFuture($this->getValue())) { + throw new TokenInvalidException('Not Before (nbf) timestamp cannot be in the future'); } - - return $value; } /** * {@inheritdoc} */ - public function validatePayload() + public static function make($value = null): ClaimContract { - if ($this->isFuture($this->getValue())) { - throw new TokenInvalidException('Not Before (nbf) timestamp cannot be in the future'); - } + return new static($value ?? now()->getTimestamp()); } } diff --git a/src/Claims/Subject.php b/src/Claims/Subject.php index 8ecb93d86..0c21bd43e 100644 --- a/src/Claims/Subject.php +++ b/src/Claims/Subject.php @@ -14,7 +14,7 @@ class Subject extends Claim { /** - * {@inheritdoc} + * @var string */ - protected $name = 'sub'; + const NAME = 'sub'; } diff --git a/src/Console/JWTGenerateSecretCommand.php b/src/Console/JWTGenerateSecretCommand.php index 24dd7fe04..ba953be2b 100644 --- a/src/Console/JWTGenerateSecretCommand.php +++ b/src/Console/JWTGenerateSecretCommand.php @@ -11,8 +11,8 @@ namespace Tymon\JWTAuth\Console; -use Illuminate\Support\Str; use Illuminate\Console\Command; +use Illuminate\Support\Str; class JWTGenerateSecretCommand extends Command { @@ -30,7 +30,7 @@ class JWTGenerateSecretCommand extends Command * * @var string */ - protected $description = 'Set the JWTAuth secret key used to sign the tokens'; + protected $description = 'Set the secret key used to sign the tokens'; /** * Execute the console command. @@ -39,7 +39,7 @@ class JWTGenerateSecretCommand extends Command */ public function handle() { - $key = Str::random(32); + $key = Str::random(64); if ($this->option('show')) { $this->comment($key); @@ -47,7 +47,7 @@ public function handle() return; } - if (file_exists($path = $this->envPath()) === false) { + if (file_exists($path = $this->laravel->environmentFilePath()) === false) { return $this->displayKey($key); } @@ -73,12 +73,8 @@ public function handle() /** * Display the key. - * - * @param string $key - * - * @return void */ - protected function displayKey($key) + protected function displayKey(string $key): void { $this->laravel['config']['jwt.secret'] = $key; @@ -87,27 +83,11 @@ protected function displayKey($key) /** * Check if the modification is confirmed. - * - * @return bool */ - protected function isConfirmed() + protected function isConfirmed(): bool { return $this->option('force') ? true : $this->confirm( 'This will invalidate all existing tokens. Are you sure you want to override the secret key?' ); } - - /** - * Get the .env file path. - * - * @return string - */ - protected function envPath() - { - if (method_exists($this->laravel, 'environmentFilePath')) { - return $this->laravel->environmentFilePath(); - } - - return $this->laravel->basePath('.env'); - } } diff --git a/src/Contracts/Claim.php b/src/Contracts/Claim.php index 9d6e9a94e..15b3f657f 100644 --- a/src/Contracts/Claim.php +++ b/src/Contracts/Claim.php @@ -1,5 +1,7 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Contracts\Providers; - -interface Auth -{ - /** - * Check a user's credentials. - * - * @param array $credentials - * - * @return mixed - */ - public function byCredentials(array $credentials); - - /** - * Authenticate a user via the id. - * - * @param mixed $id - * - * @return mixed - */ - public function byId($id); - - /** - * Get the currently authenticated user. - * - * @return mixed - */ - public function user(); -} diff --git a/src/Contracts/Providers/JWT.php b/src/Contracts/Providers/JWT.php index 7065a8791..3ac18e1af 100644 --- a/src/Contracts/Providers/JWT.php +++ b/src/Contracts/Providers/JWT.php @@ -1,5 +1,7 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Events; + +class JWTAttempt extends JWTEvent +{ +} diff --git a/src/Events/JWTEvent.php b/src/Events/JWTEvent.php new file mode 100644 index 000000000..052f4cf67 --- /dev/null +++ b/src/Events/JWTEvent.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\Events; + +abstract class JWTEvent +{ + /** + * The authenticated user. + * + * @var \Illuminate\Contracts\Auth\Authenticatable + */ + public $user; + + /** + * @var \Tymon\JWTAuth\Token|string|null + */ + public $token; + + /** + * Constructor. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param \Tymon\JWTAuth\Token|string|null $token + * + * @return void + */ + public function __construct($user, $token = null) + { + $this->token = $token; + $this->user = $user; + } +} diff --git a/src/Events/JWTInvalidate.php b/src/Events/JWTInvalidate.php new file mode 100644 index 000000000..9a1ebd0f8 --- /dev/null +++ b/src/Events/JWTInvalidate.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Events; + +class JWTInvalidate extends JWTEvent +{ +} diff --git a/src/Events/JWTLogin.php b/src/Events/JWTLogin.php new file mode 100644 index 000000000..aae06a07c --- /dev/null +++ b/src/Events/JWTLogin.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Events; + +class JWTLogin extends JWTEvent +{ +} diff --git a/src/Events/JWTLogout.php b/src/Events/JWTLogout.php new file mode 100644 index 000000000..ed801827f --- /dev/null +++ b/src/Events/JWTLogout.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Events; + +class JWTLogout extends JWTEvent +{ +} diff --git a/src/Events/JWTRefresh.php b/src/Events/JWTRefresh.php new file mode 100644 index 000000000..09c6a3223 --- /dev/null +++ b/src/Events/JWTRefresh.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Events; + +class JWTRefresh extends JWTEvent +{ +} diff --git a/src/Exceptions/InvalidClaimException.php b/src/Exceptions/InvalidClaimException.php index 7a17feb72..f5580736f 100644 --- a/src/Exceptions/InvalidClaimException.php +++ b/src/Exceptions/InvalidClaimException.php @@ -18,14 +18,8 @@ class InvalidClaimException extends JWTException { /** * Constructor. - * - * @param \Tymon\JWTAuth\Claims\Claim $claim - * @param int $code - * @param \Exception|null $previous - * - * @return void */ - public function __construct(Claim $claim, $code = 0, Exception $previous = null) + public function __construct(Claim $claim, int $code = 0, ?Exception $previous = null) { parent::__construct('Invalid value provided for claim ['.$claim->getName().']', $code, $previous); } diff --git a/src/Exceptions/PayloadException.php b/src/Exceptions/PayloadException.php index dfcc69f24..18a6234fe 100644 --- a/src/Exceptions/PayloadException.php +++ b/src/Exceptions/PayloadException.php @@ -13,5 +13,8 @@ class PayloadException extends JWTException { - // + /** + * {@inheritdoc} + */ + protected $message = 'The payload is immutable'; } diff --git a/src/Exceptions/TokenBlacklistedException.php b/src/Exceptions/TokenBlacklistedException.php index f53de8a1c..f4a7ed61e 100644 --- a/src/Exceptions/TokenBlacklistedException.php +++ b/src/Exceptions/TokenBlacklistedException.php @@ -13,5 +13,8 @@ class TokenBlacklistedException extends TokenInvalidException { - // + /** + * {@inheritdoc} + */ + protected $message = 'The token has been blacklisted'; } diff --git a/src/Exceptions/TokenExpiredException.php b/src/Exceptions/TokenExpiredException.php index 99d466c06..8f243156c 100644 --- a/src/Exceptions/TokenExpiredException.php +++ b/src/Exceptions/TokenExpiredException.php @@ -13,5 +13,8 @@ class TokenExpiredException extends JWTException { - // + /** + * {@inheritdoc} + */ + protected $message = 'The token has expired'; } diff --git a/src/Exceptions/TokenInvalidException.php b/src/Exceptions/TokenInvalidException.php index 8c6ced478..816dd3316 100644 --- a/src/Exceptions/TokenInvalidException.php +++ b/src/Exceptions/TokenInvalidException.php @@ -13,5 +13,8 @@ class TokenInvalidException extends JWTException { - // + /** + * {@inheritdoc} + */ + protected $message = 'The token is invalid'; } diff --git a/src/Exceptions/UserNotDefinedException.php b/src/Exceptions/UserNotDefinedException.php index 658713c9b..965cf579b 100644 --- a/src/Exceptions/UserNotDefinedException.php +++ b/src/Exceptions/UserNotDefinedException.php @@ -13,5 +13,8 @@ class UserNotDefinedException extends JWTException { - // + /** + * {@inheritdoc} + */ + protected $message = 'User not defined'; } diff --git a/src/Facades/JWTFactory.php b/src/Facades/JWTFactory.php deleted file mode 100644 index f43ff4695..000000000 --- a/src/Facades/JWTFactory.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * 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/Facades/JWTAuth.php b/src/Facades/JWTManager.php similarity index 86% rename from src/Facades/JWTAuth.php rename to src/Facades/JWTManager.php index 419b5903f..a8d3d427f 100644 --- a/src/Facades/JWTAuth.php +++ b/src/Facades/JWTManager.php @@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Facade; -class JWTAuth extends Facade +class JWTManager extends Facade { /** * Get the registered name of the component. @@ -22,6 +22,6 @@ class JWTAuth extends Facade */ protected static function getFacadeAccessor() { - return 'tymon.jwt.auth'; + return 'tymon.jwt.manager'; } } diff --git a/src/Factory.php b/src/Factory.php index 5f5f90d47..ec546c655 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -1,5 +1,7 @@ claimFactory = $claimFactory; - $this->validator = $validator; - $this->claims = new Collection; - } - /** - * Create the Payload instance. - * - * @param bool $resetClaims - * - * @return \Tymon\JWTAuth\Payload + * Create a Payload instance. */ - public function make($resetClaims = false) + public static function make($claims = [], ?Options $options = null): Payload { - $payload = $this->withClaims($this->buildClaimsCollection()); + $claims = Collection::make($claims) + ->map(function ($value, $key) use ($options) { + if ($value instanceof Claim) { + return $value; + } - if ($resetClaims) { - $this->emptyClaims(); - } + if (! is_string($key)) { + $key = $value; + $value = null; + } - return $payload; - } - - /** - * Empty the claims collection. - * - * @return $this - */ - public function emptyClaims() - { - $this->claims = new Collection; - - return $this; - } - - /** - * Add an array of claims to the Payload. - * - * @param array $claims - * - * @return $this - */ - protected 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 - */ - protected function addClaim($name, $value) - { - $this->claims->put($name, $value); - - return $this; - } - - /** - * Build the default claims. - * - * @return $this - */ - protected function buildClaims() - { - // remove the exp claim if it exists and the ttl is null - if ($this->claimFactory->getTTL() === null && $key = array_search('exp', $this->defaultClaims)) { - unset($this->defaultClaims[$key]); - } - - // add the default claims - foreach ($this->defaultClaims as $claim) { - $this->addClaim($claim, $this->claimFactory->make($claim)); - } - - // add custom claims on top, allowing them to overwrite defaults - return $this->addClaims($this->getCustomClaims()); - } - - /** - * Build out the Claim DTO's. - * - * @return \Tymon\JWTAuth\Claims\Collection - */ - protected function resolveClaims() - { - return $this->claims->map(function ($value, $name) { - return $value instanceof Claim ? $value : $this->claimFactory->get($name, $value); - }); - } - - /** - * Build and get the Claims Collection. - * - * @return \Tymon\JWTAuth\Claims\Collection - */ - public function buildClaimsCollection() - { - return $this->buildClaims()->resolveClaims(); - } - - /** - * Get a Payload instance with a claims collection. - * - * @param \Tymon\JWTAuth\Claims\Collection $claims - * - * @return \Tymon\JWTAuth\Payload - */ - public function withClaims(Collection $claims) - { - return new Payload($claims, $this->validator, $this->refreshFlow); - } - - /** - * Set the default claims to be added to the Payload. - * - * @param array $claims - * - * @return $this - */ - public function setDefaultClaims(array $claims) - { - $this->defaultClaims = $claims; - - return $this; - } - - /** - * Helper to set the ttl. - * - * @param int $ttl - * - * @return $this - */ - public function setTTL($ttl) - { - $this->claimFactory->setTTL($ttl); - - return $this; - } - - /** - * Helper to get the ttl. - * - * @return int - */ - public function getTTL() - { - return $this->claimFactory->getTTL(); - } - - /** - * Get the default claims. - * - * @return array - */ - public function getDefaultClaims() - { - return $this->defaultClaims; - } - - /** - * Get the PayloadValidator instance. - * - * @return \Tymon\JWTAuth\Validators\PayloadValidator - */ - public function validator() - { - return $this->validator; - } - - /** - * Magically add a claim. - * - * @param string $method - * @param array $parameters - * - * @return $this - */ - public function __call($method, $parameters) - { - $this->addClaim($method, $parameters[0]); + return ClaimFactory::get($key, $value, $options); + }); - return $this; + // Validate the claims + return PayloadValidator::check($claims, $options); } } diff --git a/src/Http/Middleware/Authenticate.php b/src/Http/Middleware/Authenticate.php deleted file mode 100644 index cd3b99ae1..000000000 --- a/src/Http/Middleware/Authenticate.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Http\Middleware; - -use Closure; - -class Authenticate extends BaseMiddleware -{ - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * - * @return mixed - */ - public function handle($request, Closure $next) - { - $this->authenticate($request); - - return $next($request); - } -} diff --git a/src/Http/Middleware/AuthenticateAndRenew.php b/src/Http/Middleware/AuthenticateAndRenew.php deleted file mode 100644 index a40bba5ce..000000000 --- a/src/Http/Middleware/AuthenticateAndRenew.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Http\Middleware; - -use Closure; - -class AuthenticateAndRenew extends BaseMiddleware -{ - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * - * @return mixed - */ - public function handle($request, Closure $next) - { - $this->authenticate($request); - - $response = $next($request); - - // Send the refreshed token back to the client. - return $this->setAuthenticationHeader($response); - } -} diff --git a/src/Http/Middleware/BaseMiddleware.php b/src/Http/Middleware/BaseMiddleware.php deleted file mode 100644 index 6cd851b64..000000000 --- a/src/Http/Middleware/BaseMiddleware.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Http\Middleware; - -use Tymon\JWTAuth\JWTAuth; -use Illuminate\Http\Request; -use Tymon\JWTAuth\Exceptions\JWTException; -use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; - -abstract class BaseMiddleware -{ - /** - * The JWT Authenticator. - * - * @var \Tymon\JWTAuth\JWTAuth - */ - protected $auth; - - /** - * Create a new BaseMiddleware instance. - * - * @param \Tymon\JWTAuth\JWTAuth $auth - * - * @return void - */ - public function __construct(JWTAuth $auth) - { - $this->auth = $auth; - } - - /** - * Check the request for the presence of a token. - * - * @param \Illuminate\Http\Request $request - * - * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException - * - * @return void - */ - public function checkForToken(Request $request) - { - if (! $this->auth->parser()->setRequest($request)->hasToken()) { - throw new UnauthorizedHttpException('jwt-auth', 'Token not provided'); - } - } - - /** - * Attempt to authenticate a user via the token in the request. - * - * @param \Illuminate\Http\Request $request - * - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * - * @return void - */ - public function authenticate(Request $request) - { - $this->checkForToken($request); - - try { - if (! $this->auth->parseToken()->authenticate()) { - throw new UnauthorizedHttpException('jwt-auth', 'User not found'); - } - } catch (JWTException $e) { - throw new UnauthorizedHttpException('jwt-auth', $e->getMessage(), $e, $e->getCode()); - } - } - - /** - * Set the authentication header. - * - * @param \Illuminate\Http\Response|\Illuminate\Http\JsonResponse $response - * @param string|null $token - * - * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse - */ - protected function setAuthenticationHeader($response, $token = null) - { - $token = $token ?: $this->auth->refresh(); - $response->headers->set('Authorization', 'Bearer '.$token); - - return $response; - } -} diff --git a/src/Http/Middleware/Check.php b/src/Http/Middleware/Check.php deleted file mode 100644 index 38d2bfb5a..000000000 --- a/src/Http/Middleware/Check.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Http\Middleware; - -use Closure; -use Exception; - -class Check extends BaseMiddleware -{ - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * - * @return mixed - */ - public function handle($request, Closure $next) - { - if ($this->auth->parser()->setRequest($request)->hasToken()) { - try { - $this->auth->parseToken()->authenticate(); - } catch (Exception $e) { - // - } - } - - return $next($request); - } -} diff --git a/src/Http/Middleware/RefreshToken.php b/src/Http/Middleware/RefreshToken.php deleted file mode 100644 index c70007fe5..000000000 --- a/src/Http/Middleware/RefreshToken.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Http\Middleware; - -use Closure; -use Tymon\JWTAuth\Exceptions\JWTException; -use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; - -class RefreshToken extends BaseMiddleware -{ - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * - * @return mixed - */ - public function handle($request, Closure $next) - { - $this->checkForToken($request); - - try { - $token = $this->auth->parseToken()->refresh(); - } catch (JWTException $e) { - throw new UnauthorizedHttpException('jwt-auth', $e->getMessage(), $e, $e->getCode()); - } - - $response = $next($request); - - // Send the refreshed token back to the client. - return $this->setAuthenticationHeader($response, $token); - } -} diff --git a/src/Http/Parser/AuthHeaders.php b/src/Http/Parser/AuthHeaders.php index 53808f032..8206c39a1 100644 --- a/src/Http/Parser/AuthHeaders.php +++ b/src/Http/Parser/AuthHeaders.php @@ -18,54 +18,33 @@ class AuthHeaders implements ParserContract { /** * The header name. - * - * @var string */ - protected $header = 'authorization'; + protected string $header = 'authorization'; /** * The header prefix. - * - * @var string */ - protected $prefix = 'bearer'; - - /** - * Attempt to parse the token from some other possible headers. - * - * @param \Illuminate\Http\Request $request - * - * @return null|string - */ - protected function fromAltHeaders(Request $request) - { - return $request->server->get('HTTP_AUTHORIZATION') ?: $request->server->get('REDIRECT_HTTP_AUTHORIZATION'); - } + protected string $prefix = 'bearer'; /** * Try to parse the token from the request header. - * - * @param \Illuminate\Http\Request $request - * - * @return null|string */ - public function parse(Request $request) + public function parse(Request $request): ?string { - $header = $request->headers->get($this->header) ?: $this->fromAltHeaders($request); + $header = $request->headers->get($this->header) + ?: $this->fromAltHeaders($request); if ($header && preg_match('/'.$this->prefix.'\s*(\S+)\b/i', $header, $matches)) { return $matches[1]; } + + return null; } /** * Set the header name. - * - * @param string $headerName - * - * @return $this */ - public function setHeaderName($headerName) + public function setHeaderName(string $headerName): self { $this->header = $headerName; @@ -74,15 +53,20 @@ public function setHeaderName($headerName) /** * Set the header prefix. - * - * @param string $headerPrefix - * - * @return $this */ - public function setHeaderPrefix($headerPrefix) + public function setHeaderPrefix(string $headerPrefix): self { $this->prefix = $headerPrefix; return $this; } + + /** + * Attempt to parse the token from some other possible headers. + */ + protected function fromAltHeaders(Request $request): ?string + { + return $request->server->get('HTTP_AUTHORIZATION') + ?? $request->server->get('REDIRECT_HTTP_AUTHORIZATION'); + } } diff --git a/src/Http/Parser/Cookies.php b/src/Http/Parser/Cookies.php index ad3d5e211..3ebf78088 100644 --- a/src/Http/Parser/Cookies.php +++ b/src/Http/Parser/Cookies.php @@ -21,24 +21,21 @@ class Cookies implements ParserContract /** * Decrypt or not the cookie while parsing. - * - * @var bool */ - private $decrypt; + private bool $decrypt; - public function __construct($decrypt = true) + /** + * Constructor. + */ + public function __construct(bool $decrypt = true) { $this->decrypt = $decrypt; } /** * Try to parse the token from the request cookies. - * - * @param \Illuminate\Http\Request $request - * - * @return null|string */ - public function parse(Request $request) + public function parse(Request $request): ?string { if ($this->decrypt && $request->hasCookie($this->key)) { return Crypt::decrypt($request->cookie($this->key)); diff --git a/src/Http/Parser/InputSource.php b/src/Http/Parser/InputSource.php index d5692bf0c..df7faf5c3 100644 --- a/src/Http/Parser/InputSource.php +++ b/src/Http/Parser/InputSource.php @@ -20,12 +20,8 @@ class InputSource implements ParserContract /** * Try to parse the token from the request input source. - * - * @param \Illuminate\Http\Request $request - * - * @return null|string */ - public function parse(Request $request) + public function parse(Request $request): ?string { return $request->input($this->key); } diff --git a/src/Http/Parser/KeyTrait.php b/src/Http/Parser/KeyTrait.php index e65da38e6..f6dfd8ca1 100644 --- a/src/Http/Parser/KeyTrait.php +++ b/src/Http/Parser/KeyTrait.php @@ -15,19 +15,13 @@ trait KeyTrait { /** * The key. - * - * @var string */ - protected $key = 'token'; + protected string $key = 'token'; /** * Set the key. - * - * @param string $key - * - * @return $this */ - public function setKey($key) + public function setKey(string $key): self { $this->key = $key; @@ -36,10 +30,8 @@ public function setKey($key) /** * Get the key. - * - * @return string */ - public function getKey() + public function getKey(): string { return $this->key; } diff --git a/src/Http/Parser/LumenRouteParams.php b/src/Http/Parser/LumenRouteParams.php index d9511f891..c829f15b3 100644 --- a/src/Http/Parser/LumenRouteParams.php +++ b/src/Http/Parser/LumenRouteParams.php @@ -11,19 +11,15 @@ namespace Tymon\JWTAuth\Http\Parser; -use Illuminate\Support\Arr; use Illuminate\Http\Request; +use Illuminate\Support\Arr; class LumenRouteParams extends RouteParams { /** * Try to get the token from the route parameters. - * - * @param \Illuminate\Http\Request $request - * - * @return null|string */ - public function parse(Request $request) + public function parse(Request $request): ?string { // WARNING: Only use this parser if you know what you're doing! // It will only work with poorly-specified aspects of certain Lumen releases. diff --git a/src/Http/Parser/Parser.php b/src/Http/Parser/Parser.php index fff0d2d0a..7e03d535c 100644 --- a/src/Http/Parser/Parser.php +++ b/src/Http/Parser/Parser.php @@ -12,30 +12,23 @@ namespace Tymon\JWTAuth\Http\Parser; use Illuminate\Http\Request; +use Illuminate\Support\Arr; +use Tymon\JWTAuth\Contracts\Http\Parser as ParserContract; class Parser { /** - * The chain. - * - * @var array + * The request. */ - private $chain; + protected Request $request; /** - * The request. - * - * @var \Illuminate\Http\Request + * The chain. */ - protected $request; + private array $chain; /** * Constructor. - * - * @param \Illuminate\Http\Request $request - * @param array $chain - * - * @return void */ public function __construct(Request $request, array $chain = []) { @@ -45,22 +38,16 @@ public function __construct(Request $request, array $chain = []) /** * Get the parser chain. - * - * @return array */ - public function getChain() + public function getChain(): array { return $this->chain; } /** * Set the order of the parser chain. - * - * @param array $chain - * - * @return $this */ - public function setChain(array $chain) + public function setChain(array $chain): self { $this->chain = $chain; @@ -69,52 +56,58 @@ public function setChain(array $chain) /** * Alias for setting the order of the chain. - * - * @param array $chain - * - * @return $this */ - public function setChainOrder(array $chain) + public function setChainOrder(array $chain): self { return $this->setChain($chain); } + /** + * Get a parser by key. + */ + public function get(string $key): ?ParserContract + { + return Arr::get($this->chain, $key); + } + /** * Iterate through the parsers and attempt to retrieve * a value, otherwise return null. - * - * @return string|null */ - public function parseToken() + public function parseToken(): ?string { foreach ($this->chain as $parser) { - if ($response = $parser->parse($this->request)) { - return $response; + if ($token = $parser->parse($this->request)) { + return $token; } } + + return null; } /** * Check whether a token exists in the chain. - * - * @return bool */ - public function hasToken() + public function hasToken(): bool { return $this->parseToken() !== null; } /** * Set the request instance. - * - * @param \Illuminate\Http\Request $request - * - * @return $this */ - public function setRequest(Request $request) + public function setRequest(Request $request): self { $this->request = $request; return $this; } + + /** + * Get the request instance. + */ + public function getRequest(): Request + { + return $this->request; + } } diff --git a/src/Http/Parser/QueryString.php b/src/Http/Parser/QueryString.php index 68b1a3594..8b1a44a20 100644 --- a/src/Http/Parser/QueryString.php +++ b/src/Http/Parser/QueryString.php @@ -20,12 +20,8 @@ class QueryString implements ParserContract /** * Try to parse the token from the request query string. - * - * @param \Illuminate\Http\Request $request - * - * @return null|string */ - public function parse(Request $request) + public function parse(Request $request): ?string { return $request->query($this->key); } diff --git a/src/Http/Parser/RouteParams.php b/src/Http/Parser/RouteParams.php index b178b238a..ab70f07b1 100644 --- a/src/Http/Parser/RouteParams.php +++ b/src/Http/Parser/RouteParams.php @@ -20,12 +20,8 @@ class RouteParams implements ParserContract /** * Try to get the token from the route parameters. - * - * @param \Illuminate\Http\Request $request - * - * @return null|string */ - public function parse(Request $request) + public function parse(Request $request): ?string { $route = $request->route(); @@ -35,5 +31,7 @@ public function parse(Request $request) if (is_callable([$route, 'parameter'])) { return $route->parameter($this->key); } + + return null; } } diff --git a/src/Http/TokenResponse.php b/src/Http/TokenResponse.php new file mode 100644 index 000000000..2c2967c37 --- /dev/null +++ b/src/Http/TokenResponse.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\Http; + +use Illuminate\Contracts\Support\Responsable; +use Illuminate\Http\JsonResponse; +use Illuminate\Support\Traits\ForwardsCalls; +use Tymon\JWTAuth\Token; + +class TokenResponse implements Responsable +{ + use ForwardsCalls; + + /** + * The token itself. + */ + protected Token $token; + + /** + * The token ttl. + */ + protected int $ttl; + + /** + * The token type. + */ + protected string $type; + + /** + * Constructor. + */ + public function __construct(Token $token, int $ttl, string $type = 'bearer') + { + $this->token = $token; + $this->ttl = $ttl; + $this->type = $type; + } + + /** + * {@inheritdoc} + */ + public function toResponse($request) + { + return new JsonResponse([ + 'access_token' => $this->token->get(), + 'token_type' => $this->type, + 'expires_in' => $this->ttl * 60, + ]); + } + + /** + * Get the token when casting to string. + */ + public function __toString(): string + { + return $this->token->get(); + } + + /** + * Magically call the Token. + * + * @throws \BadMethodCallException + * + * @return mixed + */ + public function __call(string $method, array $parameters) + { + return $this->forwardCallTo($this->token, $method, $parameters); + } +} diff --git a/src/JWT.php b/src/JWT.php index 55d14e235..e6e4db461 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -1,5 +1,7 @@ builder = $builder; $this->manager = $manager; $this->parser = $parser; } /** * Generate a token for a given subject. - * - * @param \Tymon\JWTAuth\Contracts\JWTSubject $subject - * - * @return string */ - public function fromSubject(JWTSubject $subject) + public function fromSubject(JWTSubject $subject): Token { - $payload = $this->makePayload($subject); - - return $this->manager->encode($payload)->get(); + return $this->manager->tokenForSubject($subject, $this->customClaims); } /** * Alias to generate a token for a given user. - * - * @param \Tymon\JWTAuth\Contracts\JWTSubject $user - * - * @return string */ - public function fromUser(JWTSubject $user) + public function fromUser(JWTSubject $user): Token { return $this->fromSubject($user); } /** - * Refresh an expired token. - * - * @param bool $forceForever - * @param bool $resetClaims - * - * @return string + * Invalidate a token (add it to the blacklist). */ - public function refresh($forceForever = false, $resetClaims = false) + public function invalidate(): self { $this->requireToken(); - return $this->manager->customClaims($this->getCustomClaims()) - ->refresh($this->token, $forceForever, $resetClaims) - ->get(); + $this->manager->invalidate($this->token); + + return $this; } /** - * Invalidate a token (add it to the blacklist). - * - * @param bool $forceForever - * - * @return $this + * Refresh a token. */ - public function invalidate($forceForever = false) + public function refresh(): Token { $this->requireToken(); - $this->manager->invalidate($this->token, $forceForever); - - return $this; + return $this->manager->refresh($this->token); } /** @@ -128,22 +99,18 @@ public function invalidate($forceForever = false) * the token is valid i.e. not expired or blacklisted. * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return \Tymon\JWTAuth\Payload */ - public function checkOrFail() + public function checkOrFail(): Payload { - return $this->getPayload(); + return $this->payload(); } /** * Check that the token is valid. * - * @param bool $getPayload - * * @return \Tymon\JWTAuth\Payload|bool */ - public function check($getPayload = false) + public function check(bool $getPayload = false) { try { $payload = $this->checkOrFail(); @@ -156,12 +123,10 @@ public function check($getPayload = false) /** * Get the token. - * - * @return \Tymon\JWTAuth\Token|null */ - public function getToken() + public function getToken(bool $fresh = false): ?Token { - if ($this->token === null) { + if ($this->token === null || $fresh === true) { try { $this->parseToken(); } catch (JWTException $e) { @@ -176,10 +141,8 @@ public function getToken() * Parse the token from the request. * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return $this */ - public function parseToken() + public function parseToken(): self { if (! $token = $this->parser->parseToken()) { throw new JWTException('The token could not be parsed from the request'); @@ -190,128 +153,56 @@ public function parseToken() /** * Get the raw Payload instance. - * - * @return \Tymon\JWTAuth\Payload */ - public function getPayload() + public function payload(): Payload { $this->requireToken(); return $this->manager->decode($this->token); } - /** - * Alias for getPayload(). - * - * @return \Tymon\JWTAuth\Payload - */ - public function payload() - { - return $this->getPayload(); - } - /** * Convenience method to get a claim value. * - * @param string $claim - * * @return mixed */ - public function getClaim($claim) + public function getClaim(string $claim) { return $this->payload()->get($claim); } /** - * Create a Payload instance. - * - * @param \Tymon\JWTAuth\Contracts\JWTSubject $subject - * - * @return \Tymon\JWTAuth\Payload - */ - public function makePayload(JWTSubject $subject) - { - return $this->factory()->customClaims($this->getClaimsArray($subject))->make(); - } - - /** - * Build the claims array and return it. - * - * @param \Tymon\JWTAuth\Contracts\JWTSubject $subject - * - * @return array - */ - protected function getClaimsArray(JWTSubject $subject) - { - return array_merge( - $this->getClaimsForSubject($subject), - $subject->getJWTCustomClaims(), // custom claims from JWTSubject method - $this->customClaims // custom claims from inline setter - ); - } - - /** - * Get the claims associated with a given subject. - * - * @param \Tymon\JWTAuth\Contracts\JWTSubject $subject - * - * @return array - */ - protected function getClaimsForSubject(JWTSubject $subject) - { - return array_merge([ - 'sub' => $subject->getJWTIdentifier(), - ], $this->lockSubject ? ['prv' => $this->hashSubjectModel($subject)] : []); - } - - /** - * Hash the subject model and return it. - * - * @param string|object $model - * - * @return string + * Check if the subject model matches the one saved in the payload. */ - protected function hashSubjectModel($model) + public function checkSubjectModel($model, ?Payload $payload = null): bool { - return sha1(is_object($model) ? get_class($model) : $model); - } + $payload ??= $this->payload(); - /** - * Check if the subject model matches the one saved in the token. - * - * @param string|object $model - * - * @return bool - */ - public function checkSubjectModel($model) - { - if (($prv = $this->payload()->get('prv')) === null) { + if (! $hash = $payload->get(HashedSubject::NAME)) { return true; } - return $this->hashSubjectModel($model) === $prv; + return $this->builder->hashSubjectModel($model) === $hash; } /** * Set the token. * * @param \Tymon\JWTAuth\Token|string $token - * - * @return $this */ - public function setToken($token) + public function setToken($token): self { - $this->token = $token instanceof Token ? $token : new Token($token); + $this->token = $token instanceof Token + ? $token + : new Token($token); return $this; } /** * Unset the current token. - * - * @return $this */ - public function unsetToken() + public function unsetToken(): self { $this->token = null; @@ -322,10 +213,8 @@ public function unsetToken() * Ensure that a token is available. * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return void */ - protected function requireToken() + protected function requireToken(): void { if (! $this->token) { throw new JWTException('A token is required'); @@ -334,38 +223,27 @@ protected function requireToken() /** * Set the request instance. - * - * @param \Illuminate\Http\Request $request - * - * @return $this */ - public function setRequest(Request $request) + public function setRequest(Request $request): self { + $this->builder->setRequest($request); $this->parser->setRequest($request); return $this; } /** - * Set whether the subject should be "locked". - * - * @param bool $lock - * - * @return $this + * Get the Builder instance. */ - public function lockSubject($lock) + public function builder(): Builder { - $this->lockSubject = $lock; - - return $this; + return $this->builder; } /** * Get the Manager instance. - * - * @return \Tymon\JWTAuth\Manager */ - public function manager() + public function manager(): Manager { return $this->manager; } @@ -373,49 +251,81 @@ public function manager() /** * Get the Parser instance. * - * @return \Tymon\JWTAuth\Http\Parser\Parser + * @return \Tymon\JWTAuth\Http\Parser\Parser|\Tymon\JWTAuth\Contracts\Http\Parser */ - public function parser() + public function parser(?string $key = null) { - return $this->parser; + return $key === null + ? $this->parser + : $this->parser->get($key); } /** - * Get the Payload Factory. - * - * @return \Tymon\JWTAuth\Factory + * Get the Blacklist. */ - public function factory() + public function blacklist(): Blacklist { - return $this->manager->getPayloadFactory(); + return $this->manager->getBlacklist(); } /** - * Get the Blacklist. - * - * @return \Tymon\JWTAuth\Blacklist + * Set the token ttl (in minutes). */ - public function blacklist() + public function setTTL(?int $ttl): self { - return $this->manager->getBlacklist(); + $this->builder->setTTL($ttl); + + return $this; + } + + /** + * Get the token ttl. + */ + public function getTTL(): ?int + { + return $this->builder->getTTL(); + } + + /** + * Set the secret. + */ + public function setSecret(string $secret): self + { + $this->manager->getJWTProvider() + ->setSecret($secret); + + return $this; + } + + /** + * Set the required claims. + */ + public function setRequiredClaims(array $claims = []): self + { + $this->builder->setRequiredClaims($claims); + + return $this; + } + + /** + * Register a custom claim validator. + */ + public function registerCustomValidator(string $key, callable $validator): self + { + $this->builder->setCustomValidator($key, $validator); + + return $this; } /** * Magically call the JWT Manager. * - * @param string $method - * @param array $parameters - * * @throws \BadMethodCallException * * @return mixed */ - public function __call($method, $parameters) + public function __call(string $method, array $parameters) { - if (method_exists($this->manager, $method)) { - return call_user_func_array([$this->manager, $method], $parameters); - } - - throw new BadMethodCallException("Method [$method] does not exist."); + return $this->forwardCallTo($this->manager, $method, $parameters); } } diff --git a/src/JWTAuth.php b/src/JWTAuth.php deleted file mode 100644 index 27df1d85b..000000000 --- a/src/JWTAuth.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * 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\Http\Parser\Parser; -use Tymon\JWTAuth\Contracts\Providers\Auth; - -class JWTAuth extends JWT -{ - /** - * The authentication provider. - * - * @var \Tymon\JWTAuth\Contracts\Providers\Auth - */ - protected $auth; - - /** - * Constructor. - * - * @param \Tymon\JWTAuth\Manager $manager - * @param \Tymon\JWTAuth\Contracts\Providers\Auth $auth - * @param \Tymon\JWTAuth\Http\Parser\Parser $parser - * - * @return void - */ - public function __construct(Manager $manager, Auth $auth, Parser $parser) - { - parent::__construct($manager, $parser); - $this->auth = $auth; - } - - /** - * Attempt to authenticate the user and return the token. - * - * @param array $credentials - * - * @return false|string - */ - public function attempt(array $credentials) - { - if (! $this->auth->byCredentials($credentials)) { - return false; - } - - return $this->fromUser($this->user()); - } - - /** - * Authenticate a user via a token. - * - * @return \Tymon\JWTAuth\Contracts\JWTSubject|false - */ - public function authenticate() - { - $id = $this->getPayload()->get('sub'); - - if (! $this->auth->byId($id)) { - return false; - } - - return $this->user(); - } - - /** - * Alias for authenticate(). - * - * @return \Tymon\JWTAuth\Contracts\JWTSubject|false - */ - public function toUser() - { - return $this->authenticate(); - } - - /** - * Get the authenticated user. - * - * @return \Tymon\JWTAuth\Contracts\JWTSubject - */ - public function user() - { - return $this->auth->user(); - } -} diff --git a/src/JWTGuard.php b/src/JWTGuard.php index 39bb06c6d..53b0c702a 100644 --- a/src/JWTGuard.php +++ b/src/JWTGuard.php @@ -1,5 +1,7 @@ jwt = $jwt; $this->provider = $provider; $this->request = $request; + $this->events = $events; } /** @@ -72,11 +88,8 @@ public function user() return $this->user; } - if ($this->jwt->setRequest($this->request)->getToken() && - ($payload = $this->jwt->check(true)) && - $this->validateSubject() - ) { - return $this->user = $this->provider->retrieveById($payload['sub']); + if (($payload = $this->getPayload()) && $this->validateSubject($payload)) { + return $this->user = $this->provider->retrieveById($payload[Subject::NAME]); } } @@ -98,12 +111,8 @@ public function userOrFail() /** * Validate a user's credentials. - * - * @param array $credentials - * - * @return bool */ - public function validate(array $credentials = []) + public function validate(array $credentials = []): bool { return (bool) $this->attempt($credentials, false); } @@ -111,15 +120,14 @@ public function validate(array $credentials = []) /** * Attempt to authenticate the user using the given credentials and return the token. * - * @param array $credentials - * @param bool $login - * - * @return bool|string + * @return bool|Token */ - public function attempt(array $credentials = [], $login = true) + public function attempt(array $credentials = [], bool $login = true) { $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + $this->events->dispatch(new JWTAttempt($user)); + if ($this->hasValidCredentials($user, $credentials)) { return $login ? $this->login($user) : true; } @@ -130,28 +138,30 @@ public function attempt(array $credentials = [], $login = true) /** * Create a token for a user. * - * @param \Tymon\JWTAuth\Contracts\JWTSubject $user - * - * @return string + * @return \Tymon\JWTAuth\Http\TokenResponse|\Tymon\JWTAuth\Token */ public function login(JWTSubject $user) { $token = $this->jwt->fromUser($user); $this->setToken($token)->setUser($user); - return $token; + $this->events->dispatch( + new JWTLogin($user, $token) + ); + + return $this->tokenResponse($token); } /** * Logout the user, thus invalidating the token. - * - * @param bool $forceForever - * - * @return void */ - public function logout($forceForever = false) + public function logout(): void { - $this->requireToken()->invalidate($forceForever); + $this->requireToken()->invalidate(); + + $this->events->dispatch( + new JWTLogout($this->user, $this->jwt) + ); $this->user = null; $this->jwt->unsetToken(); @@ -160,50 +170,49 @@ public function logout($forceForever = false) /** * Refresh the token. * - * @param bool $forceForever - * @param bool $resetClaims - * - * @return string + * @return \Tymon\JWTAuth\Http\TokenResponse|\Tymon\JWTAuth\Token */ - public function refresh($forceForever = false, $resetClaims = false) + public function refresh() { - return $this->requireToken()->refresh($forceForever, $resetClaims); + $token = $this->requireToken()->refresh(); + + $this->events->dispatch( + new JWTRefresh($this->user, $token) + ); + + return $this->tokenResponse($token); } /** * Invalidate the token. - * - * @param bool $forceForever - * - * @return \Tymon\JWTAuth\JWT */ - public function invalidate($forceForever = false) + public function invalidate(): self { - return $this->requireToken()->invalidate($forceForever); + $this->requireToken()->invalidate(); + + $this->events->dispatch( + new JWTInvalidate($this->user, $this->jwt) + ); + + return $this; } /** * Create a new token by User id. * * @param mixed $id - * - * @return string|null */ - public function tokenById($id) + public function tokenById($id): ?Token { - if ($user = $this->provider->retrieveById($id)) { - return $this->jwt->fromUser($user); - } + return ($user = $this->provider->retrieveById($id)) + ? $this->jwt->fromUser($user) + : null; } /** * Log a user into the application using their credentials. - * - * @param array $credentials - * - * @return bool */ - public function once(array $credentials = []) + public function once(array $credentials = []): bool { if ($this->validate($credentials)) { $this->setUser($this->lastAttempted); @@ -218,10 +227,8 @@ public function once(array $credentials = []) * Log the given User into the application. * * @param mixed $id - * - * @return bool */ - public function onceUsingId($id) + public function onceUsingId($id): bool { if ($user = $this->provider->retrieveById($id)) { $this->setUser($user); @@ -236,22 +243,16 @@ public function onceUsingId($id) * Alias for onceUsingId. * * @param mixed $id - * - * @return bool */ - public function byId($id) + public function byId($id): bool { return $this->onceUsingId($id); } /** * Add any custom claims. - * - * @param array $claims - * - * @return $this */ - public function claims(array $claims) + public function claims(array $claims): self { $this->jwt->claims($claims); @@ -259,33 +260,19 @@ public function claims(array $claims) } /** - * Get the raw Payload instance. - * - * @return \Tymon\JWTAuth\Payload + * Get the payload. */ - public function getPayload() + public function payload(): Payload { - return $this->requireToken()->getPayload(); - } - - /** - * Alias for getPayload(). - * - * @return \Tymon\JWTAuth\Payload - */ - public function payload() - { - return $this->getPayload(); + return $this->requireToken()->payload(); } /** * Set the token. * * @param \Tymon\JWTAuth\Token|string $token - * - * @return $this */ - public function setToken($token) + public function setToken($token): self { $this->jwt->setToken($token); @@ -294,36 +281,26 @@ public function setToken($token) /** * Set the token ttl. - * - * @param int $ttl - * - * @return $this */ - public function setTTL($ttl) + public function setTTL(?int $ttl): self { - $this->jwt->factory()->setTTL($ttl); + $this->jwt->setTTL($ttl); return $this; } /** * Get the user provider used by the guard. - * - * @return \Illuminate\Contracts\Auth\UserProvider */ - public function getProvider() + public function getProvider(): UserProvider { return $this->provider; } /** * Set the user provider used by the guard. - * - * @param \Illuminate\Contracts\Auth\UserProvider $provider - * - * @return $this */ - public function setProvider(UserProvider $provider) + public function setProvider(UserProvider $provider): self { $this->provider = $provider; @@ -342,22 +319,16 @@ public function getUser() /** * Get the current request instance. - * - * @return \Illuminate\Http\Request */ - public function getRequest() + public function getRequest(): Request { - return $this->request ?: Request::createFromGlobals(); + return $this->request ?? Request::createFromGlobals(); } /** * Set the current request instance. - * - * @param \Illuminate\Http\Request $request - * - * @return $this */ - public function setRequest(Request $request) + public function setRequest(Request $request): self { $this->request = $request; @@ -374,25 +345,44 @@ public function getLastAttempted() return $this->lastAttempted; } + /** + * Get the responsable Token. + * + * @return \Tymon\JWTAuth\Http\TokenResponse|\Tymon\JWTAuth\Token + */ + protected function tokenResponse(Token $token) + { + return $this->useResponsable + ? new TokenResponse($token, $this->jwt->getTTL()) + : $token; + } + + /** + * Get the payload from a token that may exist in the request. + */ + protected function getPayload(): ?Payload + { + if ($this->jwt->setRequest($this->request)->getToken() === null) { + return null; + } + + return $this->jwt->check(true) ?: null; + } + /** * Determine if the user matches the credentials. * * @param mixed $user - * @param array $credentials - * - * @return bool */ - protected function hasValidCredentials($user, $credentials) + protected function hasValidCredentials($user, array $credentials): bool { return $user !== null && $this->provider->validateCredentials($user, $credentials); } /** * Ensure the JWTSubject matches what is in the token. - * - * @return bool */ - protected function validateSubject() + protected function validateSubject(?Payload $payload = null): bool { // If the provider doesn't have the necessary method // to get the underlying model name then allow. @@ -400,17 +390,15 @@ protected function validateSubject() return true; } - return $this->jwt->checkSubjectModel($this->provider->getModel()); + return $this->jwt->checkSubjectModel($this->provider->getModel(), $payload); } /** * Ensure that a token is available in the request. * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return \Tymon\JWTAuth\JWT */ - protected function requireToken() + protected function requireToken(): JWT { if (! $this->jwt->setRequest($this->getRequest())->getToken()) { throw new JWTException('Token could not be parsed from the request.'); @@ -419,22 +407,29 @@ protected function requireToken() return $this->jwt; } + /** + * Determine whether to use Laravel Responsable interface. + */ + public function useResponsable(bool $use = true): self + { + $this->useResponsable = $use; + + return $this; + } + /** * Magically call the JWT instance. * - * @param string $method - * @param array $parameters - * * @throws \BadMethodCallException * * @return mixed */ - public function __call($method, $parameters) + public function __call(string $method, array $parameters) { - if (method_exists($this->jwt, $method)) { - return call_user_func_array([$this->jwt, $method], $parameters); + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); } - throw new BadMethodCallException("Method [$method] does not exist."); + return $this->forwardCallTo($this->jwt, $method, $parameters); } } diff --git a/src/Manager.php b/src/Manager.php index b317470c1..2b332eed7 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -1,5 +1,7 @@ provider = $provider; $this->blacklist = $blacklist; - $this->payloadFactory = $payloadFactory; + $this->builder = $builder; } /** * Encode a Payload and return the Token. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return \Tymon\JWTAuth\Token */ - public function encode(Payload $payload) + public function encode(Payload $payload): Token { - $token = $this->provider->encode($payload->get()); - - return new Token($token); + return $this->provider->token($payload); } /** * Decode a Token and return the Payload. * - * @param \Tymon\JWTAuth\Token $token - * @param bool $checkBlacklist - * * @throws \Tymon\JWTAuth\Exceptions\TokenBlacklistedException - * - * @return \Tymon\JWTAuth\Payload */ - public function decode(Token $token, $checkBlacklist = true) + public function decode(Token $token, bool $checkBlacklist = true): Payload { - $payloadArray = $this->provider->decode($token->get()); - - $payload = $this->payloadFactory - ->setRefreshFlow($this->refreshFlow) - ->customClaims($payloadArray) - ->make(); + $payload = $this->provider->payload($token, $this->builder->getOptions()); if ($checkBlacklist && $this->blacklistEnabled && $this->blacklist->has($payload)) { - throw new TokenBlacklistedException('The token has been blacklisted'); + throw new TokenBlacklistedException(); } return $payload; @@ -114,125 +79,75 @@ public function decode(Token $token, $checkBlacklist = true) /** * Refresh a Token and return a new Token. - * - * @param \Tymon\JWTAuth\Token $token - * @param bool $forceForever - * @param bool $resetClaims - * - * @return \Tymon\JWTAuth\Token */ - public function refresh(Token $token, $forceForever = false, $resetClaims = false) + public function refresh(Token $token): Token { - $this->setRefreshFlow(); - - $claims = $this->buildRefreshClaims($this->decode($token)); + // Get the claims for the new token + $claims = $this->builder->buildRefreshClaims($this->decode($token)); if ($this->blacklistEnabled) { // Invalidate old token - $this->invalidate($token, $forceForever); + $this->invalidate($token); } // Return the new token - return $this->encode( - $this->payloadFactory->customClaims($claims)->make($resetClaims) - ); + return $this->encode($this->builder->make($claims)); } /** * Invalidate a Token by adding it to the blacklist. * - * @param \Tymon\JWTAuth\Token $token - * @param bool $forceForever - * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return bool */ - public function invalidate(Token $token, $forceForever = false) + public function invalidate(Token $token): void { if (! $this->blacklistEnabled) { throw new JWTException('You must have the blacklist enabled to invalidate a token.'); } - return call_user_func( - [$this->blacklist, $forceForever ? 'addForever' : 'add'], - $this->decode($token, false) - ); + $this->blacklist->add($this->decode($token, false)); } /** - * Build the claims to go into the refreshed token. - * - * @param \Tymon\JWTAuth\Payload $payload - * - * @return array + * Get a token for the given subject and claims. */ - protected function buildRefreshClaims(Payload $payload) + public function tokenForSubject(JWTSubject $subject, array $claims = []): Token { - // assign the payload values as variables for use later - extract($payload->toArray()); - - // persist the relevant claims - return array_merge( - $this->customClaims, - compact($this->persistentClaims, 'sub', 'iat') - ); - } + $payload = $this->builder->makeForSubject($subject, $claims); - /** - * Get the Payload Factory instance. - * - * @return \Tymon\JWTAuth\Factory - */ - public function getPayloadFactory() - { - return $this->payloadFactory; + return $this->encode($payload); } /** * Get the JWTProvider instance. - * - * @return \Tymon\JWTAuth\Contracts\Providers\JWT */ - public function getJWTProvider() + public function getJWTProvider(): JWTContract { return $this->provider; } /** * Get the Blacklist instance. - * - * @return \Tymon\JWTAuth\Blacklist */ - public function getBlacklist() + public function getBlacklist(): Blacklist { return $this->blacklist; } /** - * Set whether the blacklist is enabled. - * - * @param bool $enabled - * - * @return $this + * Get the Builder instance. */ - public function setBlacklistEnabled($enabled) + public function builder(): Builder { - $this->blacklistEnabled = $enabled; - - return $this; + return $this->builder; } /** - * Set the claims to be persisted when refreshing a token. - * - * @param array $claims - * - * @return $this + * Set whether the blacklist is enabled. */ - public function setPersistentClaims(array $claims) + public function setBlacklistEnabled(bool $enabled): self { - $this->persistentClaims = $claims; + $this->blacklistEnabled = $enabled; return $this; } diff --git a/src/Options.php b/src/Options.php new file mode 100644 index 000000000..a1f1ec437 --- /dev/null +++ b/src/Options.php @@ -0,0 +1,84 @@ + + * + * 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\Arr; + +final class Options +{ + /** + * @var string + */ + const LEEWAY = 'leeway'; + + /** + * @var string + */ + const REQUIRED_CLAIMS = 'required_claims'; + + /** + * @var string + */ + const MAX_REFRESH_PERIOD = 'max_refresh_period'; + + /** + * @var string + */ + const VALIDATORS = 'validators'; + + /** + * The provided options. + */ + protected array $options = []; + + /** + * Options constructor. + */ + public function __construct(array $options = []) + { + $this->options = $options; + } + + /** + * Get the required claims. + */ + public function requiredClaims(): array + { + return Arr::get($this->options, static::REQUIRED_CLAIMS, []); + } + + /** + * Get the leeway. + */ + public function leeway(): int + { + return Arr::get($this->options, static::LEEWAY, 0); + } + + /** + * Get the maximum refresh period. + */ + public function maxRefreshPeriod(): ?int + { + return Arr::get($this->options, static::MAX_REFRESH_PERIOD); + } + + /** + * Get the custom validators. + */ + public function validators(): array + { + return Arr::get($this->options, static::VALIDATORS, []); + } +} diff --git a/src/Payload.php b/src/Payload.php index f1030cea4..f68d2ae9e 100644 --- a/src/Payload.php +++ b/src/Payload.php @@ -1,5 +1,7 @@ claims = $validator->setRefreshFlow($refreshFlow)->check($claims); + $this->claims = $claims; } /** - * Get the array of claim instances. - * - * @return \Tymon\JWTAuth\Claims\Collection + * Get the collection of claim instances. */ - public function getClaims() + public function getClaims(): Collection { return $this->claims; } /** * Checks if a payload matches some expected values. - * - * @param array $values - * @param bool $strict - * - * @return bool */ - public function matches(array $values, $strict = false) + public function matches(array $values, bool $strict = false): bool { if (empty($values)) { return false; @@ -83,12 +74,8 @@ public function matches(array $values, $strict = false) /** * Checks if a payload strictly matches some expected values. - * - * @param array $values - * - * @return bool */ - public function matchesStrict(array $values) + public function matchesStrict(array $values): bool { return $this->matches($values, true); } @@ -102,8 +89,6 @@ public function matchesStrict(array $values) */ public function get($claim = null) { - $claim = value($claim); - if ($claim !== null) { if (is_array($claim)) { return array_map([$this, 'get'], $claim); @@ -117,78 +102,64 @@ public function get($claim = null) /** * Get the underlying Claim instance. - * - * @param string $claim - * - * @return \Tymon\JWTAuth\Claims\Claim */ - public function getInternal($claim) + public function getInternal(string $claim): ?ClaimContract { return $this->claims->getByClaimName($claim); } /** * Determine whether the payload has the claim (by instance). - * - * @param \Tymon\JWTAuth\Claims\Claim $claim - * - * @return bool */ - public function has(Claim $claim) + public function has(Claim $claim): bool { return $this->claims->has($claim->getName()); } /** * Determine whether the payload has the claim (by key). - * - * @param string $claim - * - * @return bool */ - public function hasKey($claim) + public function hasKey(string $claim): bool { return $this->offsetExists($claim); } + /** + * Get the token for this payload. + */ + public function token(): Token + { + return JWTManager::encode($this); + } + /** * Get the array of claims. - * - * @return array */ - public function toArray() + public function toArray(): array { return $this->claims->toPlainArray(); } /** * Convert the object into something JSON serializable. - * - * @return array */ - public function jsonSerialize() + public function jsonSerialize(): array { return $this->toArray(); } /** * Get the payload as JSON. - * - * @param int $options - * - * @return string */ - public function toJson($options = JSON_UNESCAPED_SLASHES) + public function toJson($options = JSON_UNESCAPED_SLASHES): string { return json_encode($this->toArray(), $options); } /** * Get the payload as a string. - * - * @return string */ - public function __toString() + public function __toString(): string { return $this->toJson(); } @@ -197,10 +168,8 @@ public function __toString() * Determine if an item exists at an offset. * * @param mixed $key - * - * @return bool */ - public function offsetExists($key) + public function offsetExists($key): bool { return Arr::has($this->toArray(), $key); } @@ -227,29 +196,25 @@ public function offsetGet($key) */ public function offsetSet($key, $value) { - throw new PayloadException('The payload is immutable'); + throw new PayloadException(); } /** * Don't allow changing the payload as it should be immutable. * - * @param string $key + * @param mixed $key * * @throws \Tymon\JWTAuth\Exceptions\PayloadException - * - * @return void */ public function offsetUnset($key) { - throw new PayloadException('The payload is immutable'); + throw new PayloadException(); } /** * Count the number of claims. - * - * @return int */ - public function count() + public function count(): int { return count($this->toArray()); } @@ -269,23 +234,25 @@ public function __invoke($claim = null) /** * Magically get a claim value. * - * @param string $method - * @param array $parameters - * * @throws \BadMethodCallException * * @return mixed */ - public function __call($method, $parameters) + public function __call(string $method, array $parameters) { if (preg_match('/get(.+)\b/i', $method, $matches)) { + $match = $matches[1]; foreach ($this->claims as $claim) { - if (get_class($claim) === 'Tymon\\JWTAuth\\Claims\\'.$matches[1]) { + if (get_class($claim) === 'Tymon\\JWTAuth\\Claims\\'.$match) { return $claim->getValue(); } } + + throw new BadMethodCallException( + sprintf('The claim [%s] does not exist on the payload.', $match ?? $method) + ); } - throw new BadMethodCallException(sprintf('The claim [%s] does not exist on the payload.', $method)); + static::throwBadMethodCallException($method); } } diff --git a/src/Providers/AbstractServiceProvider.php b/src/Providers/AbstractServiceProvider.php index 97f536dd7..152bcc150 100644 --- a/src/Providers/AbstractServiceProvider.php +++ b/src/Providers/AbstractServiceProvider.php @@ -11,78 +11,49 @@ namespace Tymon\JWTAuth\Providers; -use Namshi\JOSE\JWS; -use Tymon\JWTAuth\JWT; -use Tymon\JWTAuth\Factory; -use Tymon\JWTAuth\JWTAuth; -use Tymon\JWTAuth\Manager; -use Tymon\JWTAuth\JWTGuard; -use Tymon\JWTAuth\Blacklist; -use Lcobucci\JWT\Parser as JWTParser; -use Tymon\JWTAuth\Http\Parser\Parser; -use Tymon\JWTAuth\Http\Parser\Cookies; use Illuminate\Support\ServiceProvider; use Lcobucci\JWT\Builder as JWTBuilder; -use Tymon\JWTAuth\Providers\JWT\Namshi; -use Tymon\JWTAuth\Http\Middleware\Check; -use Tymon\JWTAuth\Providers\JWT\Lcobucci; +use Lcobucci\JWT\Parser as JWTParser; +use Tymon\JWTAuth\Blacklist; +use Tymon\JWTAuth\Builder; +use Tymon\JWTAuth\Console\JWTGenerateSecretCommand; +use Tymon\JWTAuth\Contracts\Providers\Auth; +use Tymon\JWTAuth\Contracts\Providers\JWT as JWTContract; +use Tymon\JWTAuth\Contracts\Providers\Storage; use Tymon\JWTAuth\Http\Parser\AuthHeaders; +use Tymon\JWTAuth\Http\Parser\Cookies; use Tymon\JWTAuth\Http\Parser\InputSource; +use Tymon\JWTAuth\Http\Parser\Parser; use Tymon\JWTAuth\Http\Parser\QueryString; use Tymon\JWTAuth\Http\Parser\RouteParams; -use Tymon\JWTAuth\Contracts\Providers\Auth; -use Tymon\JWTAuth\Contracts\Providers\Storage; -use Tymon\JWTAuth\Validators\PayloadValidator; -use Tymon\JWTAuth\Http\Middleware\Authenticate; -use Tymon\JWTAuth\Http\Middleware\RefreshToken; -use Tymon\JWTAuth\Claims\Factory as ClaimFactory; -use Tymon\JWTAuth\Console\JWTGenerateSecretCommand; -use Tymon\JWTAuth\Http\Middleware\AuthenticateAndRenew; -use Tymon\JWTAuth\Contracts\Providers\JWT as JWTContract; +use Tymon\JWTAuth\JWT; +use Tymon\JWTAuth\JWTGuard; +use Tymon\JWTAuth\Manager; +use Tymon\JWTAuth\Providers\JWT\Lcobucci; abstract class AbstractServiceProvider extends ServiceProvider { - /** - * The middleware aliases. - * - * @var array - */ - protected $middlewareAliases = [ - 'jwt.auth' => Authenticate::class, - 'jwt.check' => Check::class, - 'jwt.refresh' => RefreshToken::class, - 'jwt.renew' => AuthenticateAndRenew::class, - ]; - /** * Boot the service provider. - * - * @return void */ abstract public function boot(); /** * Register the service provider. - * - * @return void */ public function register() { $this->registerAliases(); $this->registerJWTProvider(); - $this->registerAuthProvider(); $this->registerStorageProvider(); $this->registerJWTBlacklist(); + $this->registerBuilder(); $this->registerManager(); $this->registerTokenParser(); $this->registerJWT(); - $this->registerJWTAuth(); - $this->registerPayloadValidator(); - $this->registerClaimFactory(); - $this->registerPayloadFactory(); $this->registerJWTCommand(); $this->commands('tymon.jwt.secret'); @@ -90,8 +61,6 @@ public function register() /** * Extend Laravel's Auth. - * - * @return void */ protected function extendAuthGuard() { @@ -99,7 +68,8 @@ protected function extendAuthGuard() $guard = new JwtGuard( $app['tymon.jwt'], $app['auth']->createUserProvider($config['provider']), - $app['request'] + $app['request'], + $app['events'] ); $app->refresh('request', $guard, 'setRequest'); @@ -110,32 +80,23 @@ protected function extendAuthGuard() /** * Bind some aliases. - * - * @return void */ protected function registerAliases() { $this->app->alias('tymon.jwt', JWT::class); - $this->app->alias('tymon.jwt.auth', JWTAuth::class); $this->app->alias('tymon.jwt.provider.jwt', JWTContract::class); - $this->app->alias('tymon.jwt.provider.jwt.namshi', Namshi::class); $this->app->alias('tymon.jwt.provider.jwt.lcobucci', Lcobucci::class); - $this->app->alias('tymon.jwt.provider.auth', Auth::class); $this->app->alias('tymon.jwt.provider.storage', Storage::class); + $this->app->alias('tymon.jwt.builder', Builder::class); $this->app->alias('tymon.jwt.manager', Manager::class); $this->app->alias('tymon.jwt.blacklist', Blacklist::class); - $this->app->alias('tymon.jwt.payload.factory', Factory::class); - $this->app->alias('tymon.jwt.validators.payload', PayloadValidator::class); } /** * Register the bindings for the JSON Web Token provider. - * - * @return void */ protected function registerJWTProvider() { - $this->registerNamshiProvider(); $this->registerLcobucciProvider(); $this->app->singleton('tymon.jwt.provider.jwt', function ($app) { @@ -145,25 +106,6 @@ protected function registerJWTProvider() /** * Register the bindings for the Lcobucci JWT provider. - * - * @return void - */ - protected function registerNamshiProvider() - { - $this->app->singleton('tymon.jwt.provider.jwt.namshi', function ($app) { - return new Namshi( - new JWS(['typ' => 'JWT', 'alg' => $this->config('algo')]), - $this->config('secret'), - $this->config('algo'), - $this->config('keys') - ); - }); - } - - /** - * Register the bindings for the Lcobucci JWT provider. - * - * @return void */ protected function registerLcobucciProvider() { @@ -179,66 +121,62 @@ protected function registerLcobucciProvider() } /** - * Register the bindings for the Auth provider. - * - * @return void + * Register the bindings for the Storage provider. */ - protected function registerAuthProvider() + protected function registerStorageProvider() { - $this->app->singleton('tymon.jwt.provider.auth', function () { - return $this->getConfigInstance('providers.auth'); + $this->app->singleton('tymon.jwt.provider.storage', function () { + return $this->getConfigInstance('providers.storage'); }); } /** - * Register the bindings for the Storage provider. - * - * @return void + * Register the bindings for the JWT builder. */ - protected function registerStorageProvider() + protected function registerBuilder() { - $this->app->singleton('tymon.jwt.provider.storage', function () { - return $this->getConfigInstance('providers.storage'); + $this->app->singleton('tymon.jwt.builder', function ($app) { + $builder = new Builder($app['request']); + + $app->refresh('request', $builder, 'setRequest'); + + return $builder->lockSubject($this->config('lock_subject')) + ->setTTL($this->config('ttl')) + ->setRequiredClaims($this->config('required_claims')) + ->setLeeway($this->config('leeway')) + ->setMaxRefreshPeriod($this->config('max_refresh_period')); }); } /** * Register the bindings for the JWT Manager. - * - * @return void */ protected function registerManager() { $this->app->singleton('tymon.jwt.manager', function ($app) { - $instance = new Manager( + $manager = new Manager( $app['tymon.jwt.provider.jwt'], $app['tymon.jwt.blacklist'], - $app['tymon.jwt.payload.factory'] + $app['tymon.jwt.builder'] ); - return $instance->setBlacklistEnabled((bool) $this->config('blacklist_enabled')) - ->setPersistentClaims($this->config('persistent_claims')); + return $manager->setBlacklistEnabled((bool) $this->config('blacklist_enabled')); }); } /** * Register the bindings for the Token Parser. - * - * @return void */ protected function registerTokenParser() { $this->app->singleton('tymon.jwt.parser', function ($app) { - $parser = new Parser( - $app['request'], - [ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, - new Cookies($this->config('decrypt_cookies')), - ] - ); + $parser = new Parser($app['request'], [ + 'header' => new AuthHeaders, + 'query' => new QueryString, + 'input' => new InputSource, + 'route' => new RouteParams, + 'cookie' => new Cookies($this->config('decrypt_cookies')), + ]); $app->refresh('request', $parser, 'setRequest'); @@ -248,30 +186,13 @@ protected function registerTokenParser() /** * Register the bindings for the main JWT class. - * - * @return void */ protected function registerJWT() { $this->app->singleton('tymon.jwt', function ($app) { - return (new JWT( - $app['tymon.jwt.manager'], - $app['tymon.jwt.parser'] - ))->lockSubject($this->config('lock_subject')); - }); - } - - /** - * Register the bindings for the main JWTAuth class. - * - * @return void - */ - protected function registerJWTAuth() - { - $this->app->singleton('tymon.jwt.auth', function ($app) { - return new JWTAuth( + return new JWT( + $app['tymon.jwt.builder'], $app['tymon.jwt.manager'], - $app['tymon.jwt.provider.auth'], $app['tymon.jwt.parser'] ); }); @@ -279,68 +200,18 @@ protected function registerJWTAuth() /** * Register the bindings for the Blacklist. - * - * @return void */ protected function registerJWTBlacklist() { $this->app->singleton('tymon.jwt.blacklist', function ($app) { - $instance = new Blacklist($app['tymon.jwt.provider.storage']); + $blacklist = new Blacklist($app['tymon.jwt.provider.storage']); - return $instance->setGracePeriod($this->config('blacklist_grace_period')) - ->setRefreshTTL($this->config('refresh_ttl')); - }); - } - - /** - * Register the bindings for the payload validator. - * - * @return void - */ - protected function registerPayloadValidator() - { - $this->app->singleton('tymon.jwt.validators.payload', function () { - return (new PayloadValidator) - ->setRefreshTTL($this->config('refresh_ttl')) - ->setRequiredClaims($this->config('required_claims')); - }); - } - - /** - * Register the bindings for the Claim Factory. - * - * @return void - */ - protected function registerClaimFactory() - { - $this->app->singleton('tymon.jwt.claim.factory', function ($app) { - $factory = new ClaimFactory($app['request']); - $app->refresh('request', $factory, 'setRequest'); - - return $factory->setTTL($this->config('ttl')) - ->setLeeway($this->config('leeway')); - }); - } - - /** - * Register the bindings for the Payload Factory. - * - * @return void - */ - protected function registerPayloadFactory() - { - $this->app->singleton('tymon.jwt.payload.factory', function ($app) { - return new Factory( - $app['tymon.jwt.claim.factory'], - $app['tymon.jwt.validators.payload'] - ); + return $blacklist->setGracePeriod($this->config('blacklist_grace_period')); }); } /** * Register the Artisan command. - * - * @return void */ protected function registerJWTCommand() { @@ -352,12 +223,11 @@ protected function registerJWTCommand() /** * Helper to get the config values. * - * @param string $key - * @param string $default + * @param mixed $default * * @return mixed */ - protected function config($key, $default = null) + protected function config(string $key, $default = null) { return config("jwt.$key", $default); } @@ -365,11 +235,9 @@ protected function config($key, $default = null) /** * Get an instantiable configuration instance. * - * @param string $key - * * @return mixed */ - protected function getConfigInstance($key) + protected function getConfigInstance(string $key) { $instance = $this->config($key); diff --git a/src/Providers/Auth/Illuminate.php b/src/Providers/Auth/Illuminate.php deleted file mode 100644 index feec83adf..000000000 --- a/src/Providers/Auth/Illuminate.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * 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 Tymon\JWTAuth\Contracts\Providers\Auth; -use Illuminate\Contracts\Auth\Guard as GuardContract; - -class Illuminate implements Auth -{ - /** - * The authentication guard. - * - * @var \Illuminate\Contracts\Auth\Guard - */ - protected $auth; - - /** - * Constructor. - * - * @param \Illuminate\Contracts\Auth\Guard $auth - * - * @return void - */ - public function __construct(GuardContract $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/Auth/Sentinel.php b/src/Providers/Auth/Sentinel.php deleted file mode 100644 index ca679f7f7..000000000 --- a/src/Providers/Auth/Sentinel.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * 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 Tymon\JWTAuth\Contracts\Providers\Auth; -use Cartalyst\Sentinel\Sentinel as SentinelAuth; - -class Sentinel implements Auth -{ - /** - * The sentinel authentication. - * - * @var \Cartalyst\Sentinel\Sentinel - */ - protected $sentinel; - - /** - * Constructor. - * - * @param \Cartalyst\Sentinel\Sentinel $sentinel - * - * @return void - */ - public function __construct(SentinelAuth $sentinel) - { - $this->sentinel = $sentinel; - } - - /** - * Check a user's credentials. - * - * @param array $credentials - * - * @return mixed - */ - public function byCredentials(array $credentials) - { - return $this->sentinel->stateless($credentials); - } - - /** - * Authenticate a user via the id. - * - * @param mixed $id - * - * @return bool - */ - public function byId($id) - { - if ($user = $this->sentinel->getUserRepository()->findById($id)) { - $this->sentinel->setUser($user); - - return true; - } - - return false; - } - - /** - * Get the currently authenticated user. - * - * @return \Cartalyst\Sentinel\Users\UserInterface - */ - public function user() - { - return $this->sentinel->getUser(); - } -} diff --git a/src/Providers/JWT/Lcobucci.php b/src/Providers/JWT/Lcobucci.php index 1dd172ca4..8774a1647 100644 --- a/src/Providers/JWT/Lcobucci.php +++ b/src/Providers/JWT/Lcobucci.php @@ -1,5 +1,7 @@ HS256::class, - 'HS384' => HS384::class, - 'HS512' => HS512::class, - 'RS256' => RS256::class, - 'RS384' => RS384::class, - 'RS512' => RS512::class, - 'ES256' => ES256::class, - 'ES384' => ES384::class, - 'ES512' => ES512::class, + protected $algorithms = [ + 'HS256' => Signer\Hmac\Sha256::class, + 'HS384' => Signer\Hmac\Sha384::class, + 'HS512' => Signer\Hmac\Sha512::class, + 'RS256' => Signer\Rsa\Sha256::class, + 'RS384' => Signer\Rsa\Sha384::class, + 'RS512' => Signer\Rsa\Sha512::class, + 'ES256' => Signer\Ecdsa\Sha256::class, + 'ES384' => Signer\Ecdsa\Sha384::class, + 'ES512' => Signer\Ecdsa\Sha512::class, ]; /** * Create a JSON Web Token. * - * @param array $payload - * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return string */ - public function encode(array $payload) + public function encode(array $payload): string { // Remove the signature on the builder instance first. $this->builder->unsign(); @@ -119,53 +98,52 @@ public function encode(array $payload) /** * Decode a JSON Web Token. * - * @param string $token - * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return array */ - public function decode($token) + public function decode(string $token): array { try { $jwt = $this->parser->parse($token); } catch (Exception $e) { - throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), $e->getCode(), $e); + throw new TokenInvalidException( + 'Could not decode token: '.$e->getMessage(), + $e->getCode(), + $e + ); } if (! $jwt->verify($this->signer, $this->getVerificationKey())) { throw new TokenInvalidException('Token Signature could not be verified.'); } - return (new Collection($jwt->getClaims()))->map(function ($claim) { - return is_object($claim) ? $claim->getValue() : $claim; - })->toArray(); + return Collection::make($jwt->getClaims()) + ->map(fn ($claim) => is_object($claim) ? $claim->getValue() : $claim) + ->toArray(); } /** - * Get the signer instance. + * Get the Signer instance. * * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return \Lcobucci\JWT\Signer */ - protected function getSigner() + protected function getSigner(): Signer { - if (! array_key_exists($this->algo, $this->signers)) { + if (! array_key_exists($this->algo, $this->algorithms)) { throw new JWTException('The given algorithm could not be found'); } - return new $this->signers[$this->algo]; + return new $this->algorithms[$this->algo]; } /** * {@inheritdoc} */ - protected function isAsymmetric() + protected function isAsymmetric(): bool { $reflect = new ReflectionClass($this->signer); - return $reflect->isSubclassOf(Rsa::class) || $reflect->isSubclassOf(Ecdsa::class); + return $reflect->isSubclassOf(Signer\Rsa::class) + || $reflect->isSubclassOf(Signer\Ecdsa::class); } /** @@ -173,9 +151,9 @@ protected function isAsymmetric() */ protected function getSigningKey() { - return $this->isAsymmetric() ? - (new Keychain())->getPrivateKey($this->getPrivateKey(), $this->getPassphrase()) : - $this->getSecret(); + return $this->isAsymmetric() + ? (new Signer\Keychain())->getPrivateKey($this->getPrivateKey(), $this->getPassphrase()) + : $this->getSecret(); } /** @@ -183,8 +161,8 @@ protected function getSigningKey() */ protected function getVerificationKey() { - return $this->isAsymmetric() ? - (new Keychain())->getPublicKey($this->getPublicKey()) : - $this->getSecret(); + return $this->isAsymmetric() + ? (new Signer\Keychain())->getPublicKey($this->getPublicKey()) + : $this->getSecret(); } } diff --git a/src/Providers/JWT/Namshi.php b/src/Providers/JWT/Namshi.php deleted file mode 100644 index 361a03754..000000000 --- a/src/Providers/JWT/Namshi.php +++ /dev/null @@ -1,106 +0,0 @@ - - * - * 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 ReflectionClass; -use ReflectionException; -use InvalidArgumentException; -use Namshi\JOSE\Signer\OpenSSL\PublicKey; -use Tymon\JWTAuth\Contracts\Providers\JWT; -use Tymon\JWTAuth\Exceptions\JWTException; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; - -class Namshi extends Provider implements JWT -{ - /** - * The JWS. - * - * @var \Namshi\JOSE\JWS - */ - protected $jws; - - /** - * Constructor. - * - * @param \Namshi\JOSE\JWS $jws - * @param string $secret - * @param string $algo - * @param array $keys - * - * @return void - */ - public function __construct(JWS $jws, $secret, $algo, array $keys) - { - parent::__construct($secret, $algo, $keys); - - $this->jws = $jws; - } - - /** - * Create a JSON Web Token. - * - * @param array $payload - * - * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return string - */ - public function encode(array $payload) - { - try { - $this->jws->setPayload($payload)->sign($this->getSigningKey(), $this->getPassphrase()); - - return (string) $this->jws->getTokenString(); - } catch (Exception $e) { - throw new JWTException('Could not create token: '.$e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Decode a JSON Web Token. - * - * @param string $token - * - * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return array - */ - public function decode($token) - { - try { - // Let's never allow insecure tokens - $jws = $this->jws->load($token, false); - } catch (InvalidArgumentException $e) { - throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), $e->getCode(), $e); - } - - if (! $jws->verify($this->getVerificationKey(), $this->getAlgo())) { - throw new TokenInvalidException('Token Signature could not be verified.'); - } - - return (array) $jws->getPayload(); - } - - /** - * {@inheritdoc} - */ - protected function isAsymmetric() - { - try { - return (new ReflectionClass(sprintf('Namshi\\JOSE\\Signer\\OpenSSL\\%s', $this->getAlgo())))->isSubclassOf(PublicKey::class); - } catch (ReflectionException $e) { - throw new JWTException('The given algorithm could not be found', $e->getCode(), $e); - } - } -} diff --git a/src/Providers/JWT/Provider.php b/src/Providers/JWT/Provider.php index 2b08f71f0..2181d5741 100644 --- a/src/Providers/JWT/Provider.php +++ b/src/Providers/JWT/Provider.php @@ -1,5 +1,7 @@ secret = $secret; $this->algo = $algo; $this->keys = $keys; } + /** + * Get the decoded token as a Payload instance. + */ + public function payload(Token $token, ?Options $options = null): Payload + { + return Factory::make($this->decode($token->get()), $options); + } + + /** + * Get an encoded Token instance. + */ + public function token(Payload $payload): Token + { + return new Token($this->encode($payload->get())); + } + /** * Set the algorithm used to sign the token. - * - * @param string $algo - * - * @return $this */ - public function setAlgo($algo) + public function setAlgo(string $algo): self { $this->algo = $algo; @@ -68,22 +75,16 @@ public function setAlgo($algo) /** * Get the algorithm used to sign the token. - * - * @return string */ - public function getAlgo() + public function getAlgo(): string { return $this->algo; } /** * Set the secret used to sign the token. - * - * @param string $secret - * - * @return $this */ - public function setSecret($secret) + public function setSecret(string $secret): self { $this->secret = $secret; @@ -92,22 +93,16 @@ public function setSecret($secret) /** * Get the secret used to sign the token. - * - * @return string */ - public function getSecret() + public function getSecret(): string { return $this->secret; } /** * Set the keys used to sign the token. - * - * @param array $keys - * - * @return $this */ - public function setKeys(array $keys) + public function setKeys(array $keys): self { $this->keys = $keys; @@ -117,10 +112,8 @@ public function setKeys(array $keys) /** * Get the array of keys used to sign tokens * with an asymmetric algorithm. - * - * @return array */ - public function getKeys() + public function getKeys(): array { return $this->keys; } @@ -150,10 +143,8 @@ public function getPrivateKey() /** * Get the passphrase used to sign tokens * with an asymmetric algorithm. - * - * @return string */ - public function getPassphrase() + public function getPassphrase(): ?string { return Arr::get($this->keys, 'passphrase'); } @@ -165,7 +156,9 @@ public function getPassphrase() */ protected function getSigningKey() { - return $this->isAsymmetric() ? $this->getPrivateKey() : $this->getSecret(); + return $this->isAsymmetric() + ? $this->getPrivateKey() + : $this->getSecret(); } /** @@ -175,16 +168,14 @@ protected function getSigningKey() */ protected function getVerificationKey() { - return $this->isAsymmetric() ? $this->getPublicKey() : $this->getSecret(); + return $this->isAsymmetric() + ? $this->getPublicKey() + : $this->getSecret(); } /** * Determine if the algorithm is asymmetric, and thus * requires a public/private key combo. - * - * @throws \Tymon\JWTAuth\Exceptions\JWTException - * - * @return bool */ - abstract protected function isAsymmetric(); + abstract protected function isAsymmetric(): bool; } diff --git a/src/Providers/JWT/WebToken.php b/src/Providers/JWT/WebToken.php new file mode 100644 index 000000000..4fc209358 --- /dev/null +++ b/src/Providers/JWT/WebToken.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\Providers\JWT; + +use Exception; +use Jose\Component\Checker\AlgorithmChecker; +use Jose\Component\Checker\HeaderCheckerManager; +use Jose\Component\Core\Algorithm; +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Core\JWK; +use Jose\Component\KeyManagement\JWKFactory; +use Jose\Component\Signature\Algorithm as Algorithms; +use Jose\Component\Signature\JWSBuilder; +use Jose\Component\Signature\JWSLoader; +use Jose\Component\Signature\JWSTokenSupport; +use Jose\Component\Signature\JWSVerifier; +use Jose\Component\Signature\Serializer\CompactSerializer; +use Jose\Component\Signature\Serializer\JWSSerializer; +use Jose\Component\Signature\Serializer\JWSSerializerManager; +use RuntimeException; +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; + +class WebToken extends Provider +{ + /** + * Algorithms that this provider supports. + */ + protected array $algorithms = [ + 'HS256' => Algorithms\HS256::class, + 'HS384' => Algorithms\HS384::class, + 'HS512' => Algorithms\HS512::class, + 'RS256' => Algorithms\RS256::class, + 'RS384' => Algorithms\RS384::class, + 'RS512' => Algorithms\RS512::class, + 'ES256' => Algorithms\ES256::class, + 'ES384' => Algorithms\ES384::class, + 'ES512' => Algorithms\ES512::class, + ]; + + /** + * Create a JSON Web Token. + * + * @throws \Tymon\JWTAuth\Exceptions\JWTException + */ + public function encode(array $payload): string + { + try { + $jws = $this->getJWSBuilder() + ->create() + ->withPayload(json_encode($payload)) + ->addSignature($this->getJWK(), ['alg' => $this->getAlgo()]) + ->build(); + } catch (RuntimeException $e) { + throw new JWTException('Could not create token: '.$e->getMessage(), $e->getCode(), $e); + } + + return $this->getSerializer()->serialize($jws); + } + + /** + * Decode a JSON Web Token. + * + * @throws \Tymon\JWTAuth\Exceptions\JWTException + */ + public function decode(string $token): array + { + $signature = 0; + + try { + $jws = $this->getJWSLoader()->loadAndVerifyWithKey($token, $this->getJWK(), $signature); + } catch (Exception $e) { + throw new TokenInvalidException('Token Signature could not be verified.', $e->getCode(), $e); + } + + return json_decode($jws->getPayload(), true); + } + + /** + * Get the Algorithm instance. + * + * @throws \Tymon\JWTAuth\Exceptions\JWTException + */ + protected function getAlgorithm(): Algorithm + { + if (! array_key_exists($this->algo, $this->algorithms)) { + throw new JWTException('The given algorithm could not be found'); + } + + return new $this->algorithms[$this->algo]; + } + + /** + * {@inheritdoc} + */ + protected function isAsymmetric(): bool + { + return ! in_array('oct', $this->getAlgorithm()->allowedKeyTypes()); + } + + /** + * Get the JWK used to create and verify the token. + */ + protected function getJWK(): JWK + { + if ($this->isAsymmetric()) { + return JWKFactory::createFromKeyFile($this->getPrivateKey(), $this->getPassphrase(), [ + 'use' => 'sig', + ]); + } + + return JWKFactory::createFromSecret($this->getSecret(), [ + 'alg' => $this->getAlgo(), + 'use' => 'sig', + ]); + } + + /** + * Get the JWS builder. + */ + protected function getJWSBuilder(): JWSBuilder + { + return new JWSBuilder($this->getAlgorithmManager()); + } + + /** + * Get the JWS loader. + */ + protected function getJWSLoader(): JWSLoader + { + return new JWSLoader( + $this->getSerializerManager(), + new JWSVerifier($this->getAlgorithmManager()), + $this->getHeaderCheckerManager() + ); + } + + /** + * Get the JWS serializer. + */ + protected function getSerializer(): JWSSerializer + { + return new CompactSerializer(); + } + + /** + * Get the algorithm manager. + */ + protected function getAlgorithmManager(): AlgorithmManager + { + return new AlgorithmManager([ + $this->getAlgorithm(), + ]); + } + + /** + * Get the serializer manager. + */ + protected function getSerializerManager(): JWSSerializerManager + { + return new JWSSerializerManager([ + $this->getSerializer(), + ]); + } + + /** + * Get the header checker manager. + */ + protected function getHeaderCheckerManager(): HeaderCheckerManager + { + return new HeaderCheckerManager([ + new AlgorithmChecker([$this->getAlgo()]), + ], [ + new JWSTokenSupport(), + ]); + } +} diff --git a/src/Providers/LaravelServiceProvider.php b/src/Providers/LaravelServiceProvider.php index 5c772d378..51674fa23 100644 --- a/src/Providers/LaravelServiceProvider.php +++ b/src/Providers/LaravelServiceProvider.php @@ -23,24 +23,22 @@ public function boot() $this->publishes([$path => config_path('jwt.php')], 'config'); $this->mergeConfigFrom($path, 'jwt'); - $this->aliasMiddleware(); - $this->extendAuthGuard(); } /** - * Alias the middleware. - * - * @return void + * {@inheritdoc} */ - protected function aliasMiddleware() + protected function registerStorageProvider() { - $router = $this->app['router']; + $this->app->singleton('tymon.jwt.provider.storage', function () { + $instance = $this->getConfigInstance('providers.storage'); - $method = method_exists($router, 'aliasMiddleware') ? 'aliasMiddleware' : 'middleware'; + if (method_exists($instance, 'setLaravelVersion')) { + $instance->setLaravelVersion($this->app->version()); + } - foreach ($this->middlewareAliases as $alias => $middleware) { - $router->$method($alias, $middleware); - } + return $instance; + }); } } diff --git a/src/Providers/LumenServiceProvider.php b/src/Providers/LumenServiceProvider.php index 7397e7619..212d1e407 100644 --- a/src/Providers/LumenServiceProvider.php +++ b/src/Providers/LumenServiceProvider.php @@ -12,9 +12,10 @@ namespace Tymon\JWTAuth\Providers; use Tymon\JWTAuth\Http\Parser\AuthHeaders; +use Tymon\JWTAuth\Http\Parser\Cookies; use Tymon\JWTAuth\Http\Parser\InputSource; -use Tymon\JWTAuth\Http\Parser\QueryString; use Tymon\JWTAuth\Http\Parser\LumenRouteParams; +use Tymon\JWTAuth\Http\Parser\QueryString; class LumenServiceProvider extends AbstractServiceProvider { @@ -28,15 +29,14 @@ public function boot() $path = realpath(__DIR__.'/../../config/config.php'); $this->mergeConfigFrom($path, 'jwt'); - $this->app->routeMiddleware($this->middlewareAliases); - $this->extendAuthGuard(); $this->app['tymon.jwt.parser']->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new LumenRouteParams, + 'header' => new AuthHeaders, + 'query' => new QueryString, + 'input' => new InputSource, + 'route' => new LumenRouteParams, + 'cookie' => new Cookies($this->config('decrypt_cookies')), ]); } } diff --git a/src/Providers/Storage/Illuminate.php b/src/Providers/Storage/Illuminate.php index f0a595942..b001f40cc 100644 --- a/src/Providers/Storage/Illuminate.php +++ b/src/Providers/Storage/Illuminate.php @@ -12,37 +12,33 @@ namespace Tymon\JWTAuth\Providers\Storage; use BadMethodCallException; -use Tymon\JWTAuth\Contracts\Providers\Storage; -use Psr\SimpleCache\CacheInterface as PsrCacheInterface; use Illuminate\Contracts\Cache\Repository as CacheContract; +use Tymon\JWTAuth\Contracts\Providers\Storage; class Illuminate implements Storage { /** * The cache repository contract. - * - * @var \Illuminate\Contracts\Cache\Repository */ - protected $cache; + protected CacheContract $cache; /** * The used cache tag. - * - * @var string */ - protected $tag = 'tymon.jwt'; + protected string $tag = 'tymon.jwt'; /** - * @var bool + * Whether the storage driver supports tagging. */ - protected $supportsTags; + protected ?bool $supportsTags = null; + + /** + * The laravel version. + */ + protected ?string $laravelVersion = null; /** * Constructor. - * - * @param \Illuminate\Contracts\Cache\Repository $cache - * - * @return void */ public function __construct(CacheContract $cache) { @@ -51,71 +47,56 @@ public function __construct(CacheContract $cache) /** * Add a new item into storage. - * - * @param string $key - * @param mixed $value - * @param int $minutes - * - * @return void */ - public function add($key, $value, $minutes) + public function add(string $key, $value, $minutes): void { + // If the laravel version is 5.8 or higher then convert minutes to seconds. + if ($this->laravelVersion !== null + && is_int($minutes) + && version_compare($this->laravelVersion, '5.8', '>=') + ) { + $minutes = $minutes * 60; + } + $this->cache()->put($key, $value, $minutes); } /** * Add a new item into storage forever. - * - * @param string $key - * @param mixed $value - * - * @return void */ - public function forever($key, $value) + public function forever(string $key, $value): void { $this->cache()->forever($key, $value); } /** * Get an item from storage. - * - * @param string $key - * - * @return mixed */ - public function get($key) + public function get(string $key) { return $this->cache()->get($key); } /** * Remove an item from storage. - * - * @param string $key - * - * @return bool */ - public function destroy($key) + public function destroy(string $key): void { - return $this->cache()->forget($key); + $this->cache()->forget($key); } /** * Remove all items associated with the tag. - * - * @return void */ - public function flush() + public function flush(): void { $this->cache()->flush(); } /** * Return the cache instance with tags attached. - * - * @return \Illuminate\Contracts\Cache\Repository */ - protected function cache() + protected function cache(): CacheContract { if ($this->supportsTags === null) { $this->determineTagSupport(); @@ -128,33 +109,28 @@ protected function cache() return $this->cache; } + /** + * Set the laravel version. + */ + public function setLaravelVersion(string $version) + { + $this->laravelVersion = $version; + + return $this; + } + /** * Detect as best we can whether tags are supported with this repository & store, * and save our result on the $supportsTags flag. - * - * @return void */ - protected function determineTagSupport() + protected function determineTagSupport(): void { - // Laravel >= 5.1.28 - if (method_exists($this->cache, 'tags') || $this->cache instanceof PsrCacheInterface) { - try { - // Attempt the repository tags command, which throws exceptions when unsupported - $this->cache->tags($this->tag); - $this->supportsTags = true; - } catch (BadMethodCallException $ex) { - $this->supportsTags = false; - } - } else { - // Laravel <= 5.1.27 - if (method_exists($this->cache, 'getStore')) { - // Check for the tags function directly on the store - $this->supportsTags = method_exists($this->cache->getStore(), 'tags'); - } else { - // Must be using custom cache repository without getStore(), and all bets are off, - // or we are mocking the cache contract (in testing), which will not create a getStore method - $this->supportsTags = false; - } + try { + // Attempt the repository tags command, which throws exceptions when unsupported + $this->cache->tags($this->tag); + $this->supportsTags = true; + } catch (BadMethodCallException $ex) { + $this->supportsTags = false; } } } diff --git a/src/Support/CustomClaims.php b/src/Support/CustomClaims.php index d5443eb99..aa95ebaff 100644 --- a/src/Support/CustomClaims.php +++ b/src/Support/CustomClaims.php @@ -1,5 +1,7 @@ customClaims = $customClaims; @@ -36,22 +32,16 @@ public function customClaims(array $customClaims) /** * Alias to set the custom claims. - * - * @param array $customClaims - * - * @return $this */ - public function claims(array $customClaims) + public function claims(array $customClaims): self { return $this->customClaims($customClaims); } /** * Get the custom claims. - * - * @return array */ - public function getCustomClaims() + public function getCustomClaims(): array { return $this->customClaims; } diff --git a/src/Support/RefreshFlow.php b/src/Support/RefreshFlow.php deleted file mode 100644 index 988b6eae1..000000000 --- a/src/Support/RefreshFlow.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Support; - -trait RefreshFlow -{ - /** - * The refresh flow flag. - * - * @var bool - */ - protected $refreshFlow = false; - - /** - * Set the refresh flow flag. - * - * @param bool $refreshFlow - * - * @return $this - */ - public function setRefreshFlow($refreshFlow = true) - { - $this->refreshFlow = $refreshFlow; - - return $this; - } -} diff --git a/src/Support/Utils.php b/src/Support/Utils.php deleted file mode 100644 index 5738e6ed7..000000000 --- a/src/Support/Utils.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Support; - -use Carbon\Carbon; - -class Utils -{ - /** - * Get the Carbon instance for the current time. - * - * @return \Carbon\Carbon - */ - public static function now() - { - return Carbon::now('UTC'); - } - - /** - * Get the Carbon instance for the timestamp. - * - * @param int $timestamp - * - * @return \Carbon\Carbon - */ - public static function timestamp($timestamp) - { - return Carbon::createFromTimestampUTC($timestamp)->timezone('UTC'); - } - - /** - * Checks if a timestamp is in the past. - * - * @param int $timestamp - * @param int $leeway - * - * @return bool - */ - public static function isPast($timestamp, $leeway = 0) - { - $timestamp = static::timestamp($timestamp); - - return $leeway > 0 - ? $timestamp->addSeconds($leeway)->isPast() - : $timestamp->isPast(); - } - - /** - * Checks if a timestamp is in the future. - * - * @param int $timestamp - * @param int $leeway - * - * @return bool - */ - public static function isFuture($timestamp, $leeway = 0) - { - $timestamp = static::timestamp($timestamp); - - return $leeway > 0 - ? $timestamp->subSeconds($leeway)->isFuture() - : $timestamp->isFuture(); - } -} diff --git a/src/Support/helpers.php b/src/Support/helpers.php new file mode 100644 index 000000000..4e24089cc --- /dev/null +++ b/src/Support/helpers.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tymon\JWTAuth\Support; + +use Carbon\Carbon; + +/** + * Get the Carbon instance for the current time. + */ +function now() +{ + return Carbon::now('UTC'); +} + +/** + * Get the Carbon instance for the timestamp. + */ +function timestamp(int $timestamp): Carbon +{ + return Carbon::createFromTimestampUTC($timestamp) + ->timezone('UTC'); +} + +/** + * Checks if a timestamp is in the past. + */ +function is_past(int $timestamp, int $leeway = 0): bool +{ + return timestamp($timestamp) + ->addSeconds($leeway) + ->isPast(); +} + +/** + * Checks if a timestamp is in the future. + */ +function is_future(int $timestamp, int $leeway = 0): bool +{ + return timestamp($timestamp) + ->subSeconds($leeway) + ->isFuture(); +} diff --git a/src/Token.php b/src/Token.php index a51a73626..c9d002523 100644 --- a/src/Token.php +++ b/src/Token.php @@ -1,5 +1,7 @@ value = (string) (new TokenValidator)->check($value); + $this->value = TokenValidator::check($value); } /** * Get the token. - * - * @return string */ - public function get() + public function get(): string { return $this->value; } + /** + * Get the payload for this token. + */ + public function payload(bool $checkBlacklist = true): Payload + { + return JWTManager::decode($this, $checkBlacklist); + } + + /** + * Checks if a token matches this one. + */ + public function matches($token): bool + { + return (string) $this->get() === (string) $token; + } + /** * Get the token when casting to string. - * - * @return string */ - public function __toString() + public function __toString(): string { return $this->get(); } diff --git a/src/Validators/PayloadValidator.php b/src/Validators/PayloadValidator.php index f7f73b7b9..5eaaa33d9 100644 --- a/src/Validators/PayloadValidator.php +++ b/src/Validators/PayloadValidator.php @@ -11,117 +11,53 @@ namespace Tymon\JWTAuth\Validators; +use Illuminate\Support\Arr; use Tymon\JWTAuth\Claims\Collection; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; +use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Options; +use Tymon\JWTAuth\Payload; class PayloadValidator extends Validator { - /** - * The required claims. - * - * @var array - */ - protected $requiredClaims = [ - 'iss', - 'iat', - 'exp', - 'nbf', - 'sub', - 'jti', - ]; - - /** - * The refresh TTL. - * - * @var int - */ - protected $refreshTTL = 20160; - /** * Run the validations on the payload array. * - * @param \Tymon\JWTAuth\Claims\Collection $value - * - * @return \Tymon\JWTAuth\Claims\Collection - */ - public function check($value) - { - $this->validateStructure($value); - - return $this->refreshFlow ? $this->validateRefresh($value) : $this->validatePayload($value); - } - - /** - * Ensure the payload contains the required claims and - * the claims have the relevant type. - * - * @param \Tymon\JWTAuth\Claims\Collection $claims - * - * @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException - * - * @return void - */ - protected function validateStructure(Collection $claims) - { - if (! $claims->hasAllClaims($this->requiredClaims)) { - throw new TokenInvalidException('JWT payload does not contain the required claims'); - } - } - - /** - * Validate the payload timestamps. - * - * @param \Tymon\JWTAuth\Claims\Collection $claims - * * @throws \Tymon\JWTAuth\Exceptions\TokenExpiredException * @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException - * - * @return \Tymon\JWTAuth\Claims\Collection */ - protected function validatePayload(Collection $claims) + public static function check(Collection $claims, ?Options $options = null): Payload { - return $claims->validate('payload'); - } + $options ??= new Options(); - /** - * Check the token in the refresh flow context. - * - * @param \Tymon\JWTAuth\Claims\Collection $claims - * - * @throws \Tymon\JWTAuth\Exceptions\TokenExpiredException - * - * @return \Tymon\JWTAuth\Claims\Collection - */ - protected function validateRefresh(Collection $claims) - { - return $this->refreshTTL === null ? $claims : $claims->validate('refresh', $this->refreshTTL); - } + if (! static::hasRequiredClaims($claims, $options)) { + static::throwFailed('JWT does not contain the required claims'); + } - /** - * Set the required claims. - * - * @param array $claims - * - * @return $this - */ - public function setRequiredClaims(array $claims) - { - $this->requiredClaims = $claims; + // Run the built in verifications + $claims->verify(); + + // Run any custom validators + foreach ($options->validators() as $name => $validator) { + if ($claim = $claims->getByClaimName($name)) { + if ($validator($claim->getValue(), $name) === false) { + static::throwFailed('Validation failed for claim ['.$name.']'); + } + } + } - return $this; + return new Payload($claims); } /** - * Set the refresh ttl. - * - * @param int $ttl - * - * @return $this + * Determine whether the given collection of claims has all the required claims. */ - public function setRefreshTTL($ttl) + protected static function hasRequiredClaims(Collection $claims, ?Options $options = null): bool { - $this->refreshTTL = $ttl; + // If the collection doesn't have an exp then remove it from the required claims. + $requiredClaims = $claims->has(Expiration::NAME) + ? $options->requiredClaims() + : Arr::except($options->requiredClaims(), [Expiration::NAME]); - return $this; + return $claims->hasAllClaims($requiredClaims); } } diff --git a/src/Validators/TokenValidator.php b/src/Validators/TokenValidator.php index b76f13e9e..5f0be6b5a 100644 --- a/src/Validators/TokenValidator.php +++ b/src/Validators/TokenValidator.php @@ -11,41 +11,25 @@ namespace Tymon\JWTAuth\Validators; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; - class TokenValidator extends Validator { /** * Check the structure of the token. * - * @param string $value - * - * @return string - */ - public function check($value) - { - return $this->validateStructure($value); - } - - /** - * @param string $token - * * @throws \Tymon\JWTAuth\Exceptions\TokenInvalidException - * - * @return string */ - protected function validateStructure($token) + public static function check(string $token): string { $parts = explode('.', $token); if (count($parts) !== 3) { - throw new TokenInvalidException('Wrong number of segments'); + static::throwFailed('Wrong number of segments'); } $parts = array_filter(array_map('trim', $parts)); if (count($parts) !== 3 || implode('.', $parts) !== $token) { - throw new TokenInvalidException('Malformed token'); + static::throwFailed('Malformed token'); } return $token; diff --git a/src/Validators/Validator.php b/src/Validators/Validator.php index bb97f1f83..78d369aab 100644 --- a/src/Validators/Validator.php +++ b/src/Validators/Validator.php @@ -11,25 +11,18 @@ namespace Tymon\JWTAuth\Validators; -use Tymon\JWTAuth\Support\RefreshFlow; use Tymon\JWTAuth\Exceptions\JWTException; -use Tymon\JWTAuth\Contracts\Validator as ValidatorContract; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; -abstract class Validator implements ValidatorContract +abstract class Validator { - use RefreshFlow; - /** * Helper function to return a boolean. - * - * @param array $value - * - * @return bool */ - public function isValid($value) + public static function isValid(...$args): bool { try { - $this->check($value); + forward_static_call('static::check', ...$args); } catch (JWTException $e) { return false; } @@ -38,11 +31,10 @@ public function isValid($value) } /** - * Run the validation. - * - * @param array $value - * - * @return void + * Validation failed. */ - abstract public function check($value); + public static function throwFailed(string $message = 'Invalid'): void + { + throw new TokenInvalidException($message); + } } diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index 7ba31e3f3..84d5261b0 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -11,8 +11,8 @@ namespace Tymon\JWTAuth\Test; -use Mockery; use Carbon\Carbon; +use Mockery; use PHPUnit\Framework\TestCase; abstract class AbstractTestCase extends TestCase @@ -22,7 +22,7 @@ abstract class AbstractTestCase extends TestCase */ protected $testNowTimestamp; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -30,7 +30,7 @@ public function setUp() $this->testNowTimestamp = $now->getTimestamp(); } - public function tearDown() + public function tearDown(): void { Carbon::setTestNow(); Mockery::close(); diff --git a/tests/BlacklistTest.php b/tests/BlacklistTest.php index e9c0396c5..21a4f10d8 100644 --- a/tests/BlacklistTest.php +++ b/tests/BlacklistTest.php @@ -12,17 +12,15 @@ namespace Tymon\JWTAuth\Test; use Mockery; -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\Expiration; use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; use Tymon\JWTAuth\Claims\NotBefore; -use Tymon\JWTAuth\Claims\Collection; -use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Claims\Subject; use Tymon\JWTAuth\Contracts\Providers\Storage; -use Tymon\JWTAuth\Validators\PayloadValidator; +use Tymon\JWTAuth\Factory; class BlacklistTest extends AbstractTestCase { @@ -36,117 +34,100 @@ class BlacklistTest extends AbstractTestCase */ protected $blacklist; - /** - * @var \Mockery\MockInterface|\Tymon\JWTAuth\Validators\Validator - */ - protected $validator; - - public function setUp() + public function setUp(): void { parent::setUp(); $this->storage = Mockery::mock(Storage::class); $this->blacklist = new Blacklist($this->storage); - $this->validator = Mockery::mock(PayloadValidator::class); } /** @test */ public function it_should_add_a_valid_token_to_the_blacklist() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + $this->storage->shouldReceive('get') + ->with('foo') + ->once() + ->andReturn([]); - $payload = new Payload($collection, $this->validator); + $this->storage->shouldReceive('add') + ->with('foo', ['valid_until' => $this->testNowTimestamp], 61) + ->once(); - $this->storage->shouldReceive('add')->with('foo', ['valid_until' => $this->testNowTimestamp], 20161)->once(); $this->blacklist->add($payload); } /** @test */ - public function it_should_add_a_token_with_no_exp_to_the_blacklist_forever() + public function it_should_return_early_when_adding_an_item_and_it_already_exists() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), + new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + $this->storage->shouldReceive('get') + ->with('foo') + ->once() + ->andReturn(['valid_until' => $this->testNowTimestamp]); - $payload = new Payload($collection, $this->validator); + $this->storage->shouldReceive('add') + ->with('foo', ['valid_until' => $this->testNowTimestamp], 61) + ->never(); - $this->storage->shouldReceive('forever')->with('foo', 'forever')->once(); $this->blacklist->add($payload); } /** @test */ - public function it_should_return_true_when_adding_an_expired_token_to_the_blacklist() + public function it_should_add_a_token_with_no_exp_to_the_blacklist_forever() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp - 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - $collection = Collection::make($claims); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + ]); - $payload = new Payload($collection, $this->validator, true); + $this->storage->shouldReceive('forever') + ->with('foo', $this->blacklist::FOREVER) + ->once(); - $this->storage->shouldReceive('add')->with('foo', ['valid_until' => $this->testNowTimestamp], 20161)->once(); - $this->assertTrue($this->blacklist->add($payload)); + $this->blacklist->add($payload); } /** @test */ public function it_should_check_whether_a_token_has_been_blacklisted() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foobar'), - ]; - - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - - $payload = new Payload($collection, $this->validator); - - $this->storage->shouldReceive('get')->with('foobar')->once()->andReturn(['valid_until' => $this->testNowTimestamp]); + $this->storage->shouldReceive('get') + ->with('foobar') + ->once() + ->andReturn(['valid_until' => $this->testNowTimestamp]); $this->assertTrue($this->blacklist->has($payload)); } - public function blacklist_provider() - { - return [ - [null], - [0], - [''], - [[]], - [['valid_until' => strtotime('+1day')]], - ]; - } - /** * @test * @dataProvider blacklist_provider @@ -155,43 +136,39 @@ public function blacklist_provider() */ public function it_should_check_whether_a_token_has_not_been_blacklisted($result) { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foobar'), - ]; - - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + $this->storage->shouldReceive('get') + ->with('foobar') + ->once() + ->andReturn($result); - $payload = new Payload($collection, $this->validator); - - $this->storage->shouldReceive('get')->with('foobar')->once()->andReturn($result); $this->assertFalse($this->blacklist->has($payload)); } /** @test */ public function it_should_check_whether_a_token_has_been_blacklisted_forever() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foobar'), - ]; - $collection = Collection::make($claims); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + ]); - $payload = new Payload($collection, $this->validator); - - $this->storage->shouldReceive('get')->with('foobar')->once()->andReturn('forever'); + $this->storage->shouldReceive('get') + ->with('foobar') + ->once() + ->andReturn($this->blacklist::FOREVER); $this->assertTrue($this->blacklist->has($payload)); } @@ -199,21 +176,19 @@ public function it_should_check_whether_a_token_has_been_blacklisted_forever() /** @test */ public function it_should_check_whether_a_token_has_been_blacklisted_when_the_token_is_not_blacklisted() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foobar'), - ]; - $collection = Collection::make($claims); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + ]); - $payload = new Payload($collection, $this->validator); - - $this->storage->shouldReceive('get')->with('foobar')->once()->andReturn(null); + $this->storage->shouldReceive('get') + ->with('foobar') + ->once() + ->andReturn(null); $this->assertFalse($this->blacklist->has($payload)); } @@ -221,52 +196,48 @@ public function it_should_check_whether_a_token_has_been_blacklisted_when_the_to /** @test */ public function it_should_remove_a_token_from_the_blacklist() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foobar'), - ]; - $collection = Collection::make($claims); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + ]); - $payload = new Payload($collection, $this->validator); + $this->storage->shouldReceive('destroy') + ->with('foobar') + ->once(); - $this->storage->shouldReceive('destroy')->with('foobar')->andReturn(true); - $this->assertTrue($this->blacklist->remove($payload)); + $this->blacklist->remove($payload); } /** @test */ public function it_should_set_a_custom_unique_key_for_the_blacklist() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foobar'), - ]; - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); + $this->storage->shouldReceive('get') + ->with(1) + ->once() + ->andReturn(['valid_until' => $this->testNowTimestamp]); - $payload = new Payload($collection, $this->validator); - - $this->storage->shouldReceive('get')->with(1)->once()->andReturn(['valid_until' => $this->testNowTimestamp]); - - $this->assertTrue($this->blacklist->setKey('sub')->has($payload)); - $this->assertSame(1, $this->blacklist->getKey($payload)); + $this->assertTrue($this->blacklist->setKey(Subject::NAME)->has($payload)); + $this->assertSame('1', $this->blacklist->getKey($payload)); } /** @test */ public function it_should_empty_the_blacklist() { - $this->storage->shouldReceive('flush'); - $this->assertTrue($this->blacklist->clear()); + $this->storage->shouldReceive('flush')->once(); + $this->blacklist->clear(); } /** @test */ @@ -276,10 +247,8 @@ public function it_should_set_and_get_the_blacklist_grace_period() $this->assertSame(15, $this->blacklist->getGracePeriod()); } - /** @test */ - public function it_should_set_and_get_the_blacklist_refresh_ttl() + public function blacklist_provider() { - $this->assertInstanceOf(Blacklist::class, $this->blacklist->setRefreshTTL(15)); - $this->assertSame(15, $this->blacklist->getRefreshTTL()); + return [[null], [0], [''], [[]], [['valid_until' => strtotime('+1day')]]]; } } diff --git a/tests/Claims/ClaimTest.php b/tests/Claims/ClaimTest.php index 75f4bacf0..d199f324a 100644 --- a/tests/Claims/ClaimTest.php +++ b/tests/Claims/ClaimTest.php @@ -11,9 +11,10 @@ namespace Tymon\JWTAuth\Test\Claims; +use Illuminate\Contracts\Support\Arrayable; use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Exceptions\InvalidClaimException; use Tymon\JWTAuth\Test\AbstractTestCase; -use Illuminate\Contracts\Support\Arrayable; class ClaimTest extends AbstractTestCase { @@ -22,27 +23,26 @@ class ClaimTest extends AbstractTestCase */ protected $claim; - public function setUp() + public function setUp(): void { parent::setUp(); $this->claim = new Expiration($this->testNowTimestamp); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\InvalidClaimException - * @expectedExceptionMessage Invalid value provided for claim [exp] - */ + /** @test */ public function it_should_throw_an_exception_when_passing_an_invalid_value() { + $this->expectException(InvalidClaimException::class); + $this->expectExceptionMessage('Invalid value provided for claim [exp]'); + $this->claim->setValue('foo'); } /** @test */ public function it_should_convert_the_claim_to_an_array() { - $this->assertSame(['exp' => $this->testNowTimestamp], $this->claim->toArray()); + $this->assertSame([Expiration::NAME => $this->testNowTimestamp], $this->claim->toArray()); } /** @test */ diff --git a/tests/Claims/CollectionTest.php b/tests/Claims/CollectionTest.php index 0c9bf4780..76409f2d1 100644 --- a/tests/Claims/CollectionTest.php +++ b/tests/Claims/CollectionTest.php @@ -11,37 +11,30 @@ namespace Tymon\JWTAuth\Test\Claims; -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\Collection; use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Subject; use Tymon\JWTAuth\Test\AbstractTestCase; class CollectionTest extends AbstractTestCase { - private function getCollection() - { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp + 3600), - new NotBefore($this->testNowTimestamp), - new IssuedAt($this->testNowTimestamp), - new JwtId('foo'), - ]; - - return new Collection($claims); - } - /** @test */ public function it_should_sanitize_the_claims_to_associative_array() { $collection = $this->getCollection(); - $this->assertSame(array_keys($collection->toArray()), ['sub', 'iss', 'exp', 'nbf', 'iat', 'jti']); + $this->assertSame(array_keys($collection->toArray()), [ + Subject::NAME, + Issuer::NAME, + Expiration::NAME, + NotBefore::NAME, + IssuedAt::NAME, + JwtId::NAME, + ]); } /** @test */ @@ -49,12 +42,14 @@ public function it_should_determine_if_a_collection_contains_all_the_given_claim { $collection = $this->getCollection(); - $this->assertFalse($collection->hasAllClaims(['sub', 'iss', 'exp', 'nbf', 'iat', 'jti', 'abc'])); + $this->assertFalse( + $collection->hasAllClaims([Subject::NAME, Issuer::NAME, Expiration::NAME, NotBefore::NAME, IssuedAt::NAME, JwtId::NAME, 'abc']) + ); $this->assertFalse($collection->hasAllClaims(['foo', 'bar'])); - $this->assertFalse($collection->hasAllClaims([])); - $this->assertTrue($collection->hasAllClaims(['sub', 'iss'])); - $this->assertTrue($collection->hasAllClaims(['sub', 'iss', 'exp', 'nbf', 'iat', 'jti'])); + $this->assertTrue($collection->hasAllClaims([])); + $this->assertTrue($collection->hasAllClaims([Subject::NAME, Issuer::NAME])); + $this->assertTrue($collection->hasAllClaims([Subject::NAME, Issuer::NAME, Expiration::NAME, NotBefore::NAME, IssuedAt::NAME, JwtId::NAME])); } /** @test */ @@ -62,7 +57,21 @@ public function it_should_get_a_claim_instance_by_name() { $collection = $this->getCollection(); - $this->assertInstanceOf(Expiration::class, $collection->getByClaimName('exp')); - $this->assertInstanceOf(Subject::class, $collection->getByClaimName('sub')); + $this->assertInstanceOf(Expiration::class, $collection->getByClaimName(Expiration::NAME)); + $this->assertInstanceOf(Subject::class, $collection->getByClaimName(Subject::NAME)); + $this->assertInstanceOf(Issuer::class, $collection->getByClaimName(Issuer::NAME)); + $this->assertInstanceOf(JwtId::class, $collection->getByClaimName(JwtId::NAME)); + } + + private function getCollection() + { + return new Collection([ + new Subject(1), + new Issuer('http://example.com'), + new Expiration($this->testNowTimestamp + 3600), + new NotBefore($this->testNowTimestamp), + new IssuedAt($this->testNowTimestamp), + new JwtId('foo'), + ]); } } diff --git a/tests/Claims/DatetimeClaimTest.php b/tests/Claims/DatetimeClaimTest.php index ebd56d55c..75849e025 100644 --- a/tests/Claims/DatetimeClaimTest.php +++ b/tests/Claims/DatetimeClaimTest.php @@ -11,22 +11,21 @@ namespace Tymon\JWTAuth\Test\Claims; -use Mockery; -use DateTime; -use DateInterval; use Carbon\Carbon; +use Carbon\CarbonInterval; +use DateInterval; +use DateTime; use DateTimeImmutable; use DateTimeInterface; -use Tymon\JWTAuth\Payload; -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\Collection; use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Payload; use Tymon\JWTAuth\Test\AbstractTestCase; -use Tymon\JWTAuth\Validators\PayloadValidator; class DatetimeClaimTest extends AbstractTestCase { @@ -40,20 +39,17 @@ class DatetimeClaimTest extends AbstractTestCase */ protected $claimsTimestamp; - public function setUp() + public function setUp(): void { parent::setUp(); - $this->validator = Mockery::mock(PayloadValidator::class); - $this->validator->shouldReceive('setRefreshFlow->check'); - $this->claimsTimestamp = [ - 'sub' => new Subject(1), - 'iss' => new Issuer('http://example.com'), - 'exp' => new Expiration($this->testNowTimestamp + 3600), - 'nbf' => new NotBefore($this->testNowTimestamp), - 'iat' => new IssuedAt($this->testNowTimestamp), - 'jti' => new JwtId('foo'), + Subject::NAME => new Subject(1), + Issuer::NAME => new Issuer('http://example.com'), + Expiration::NAME => new Expiration($this->testNowTimestamp + 3600), + NotBefore::NAME => new NotBefore($this->testNowTimestamp), + IssuedAt::NAME => new IssuedAt($this->testNowTimestamp), + JwtId::NAME => new JwtId('foo'), ]; } @@ -68,16 +64,16 @@ public function it_should_handle_carbon_claims() $this->assertInstanceOf(DatetimeInterface::class, $testCarbon); $claimsDatetime = [ - 'sub' => new Subject(1), - 'iss' => new Issuer('http://example.com'), - 'exp' => new Expiration($testCarbonCopy->addHour()), - 'nbf' => new NotBefore($testCarbon), - 'iat' => new IssuedAt($testCarbon), - 'jti' => new JwtId('foo'), + Subject::NAME => new Subject(1), + Issuer::NAME => new Issuer('http://example.com'), + Expiration::NAME => new Expiration($testCarbonCopy->addHour()), + NotBefore::NAME => new NotBefore($testCarbon), + IssuedAt::NAME => new IssuedAt($testCarbon), + JwtId::NAME => new JwtId('foo'), ]; - $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp), $this->validator); - $payloadDatetime = new Payload(Collection::make($claimsDatetime), $this->validator); + $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp)); + $payloadDatetime = new Payload(Collection::make($claimsDatetime)); $this->assertEquals($payloadTimestamp, $payloadDatetime); } @@ -92,16 +88,16 @@ public function it_should_handle_datetime_claims() $this->assertInstanceOf(DatetimeInterface::class, $testDateTime); $claimsDatetime = [ - 'sub' => new Subject(1), - 'iss' => new Issuer('http://example.com'), - 'exp' => new Expiration($testDateTimeCopy->modify('+3600 seconds')), - 'nbf' => new NotBefore($testDateTime), - 'iat' => new IssuedAt($testDateTime), - 'jti' => new JwtId('foo'), + Subject::NAME => new Subject(1), + Issuer::NAME => new Issuer('http://example.com'), + Expiration::NAME => new Expiration($testDateTimeCopy->modify('+3600 seconds')), + NotBefore::NAME => new NotBefore($testDateTime), + IssuedAt::NAME => new IssuedAt($testDateTime), + JwtId::NAME => new JwtId('foo'), ]; - $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp), $this->validator); - $payloadDatetime = new Payload(Collection::make($claimsDatetime), $this->validator); + $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp)); + $payloadDatetime = new Payload(Collection::make($claimsDatetime)); $this->assertEquals($payloadTimestamp, $payloadDatetime); } @@ -109,22 +105,25 @@ public function it_should_handle_datetime_claims() /** @test */ public function it_should_handle_datetime_immutable_claims() { - $testDateTimeImmutable = DateTimeImmutable::createFromFormat('U', (string) $this->testNowTimestamp); + $testDateTimeImmutable = DateTimeImmutable::createFromFormat( + 'U', + (string) $this->testNowTimestamp + ); $this->assertInstanceOf(DateTimeImmutable::class, $testDateTimeImmutable); $this->assertInstanceOf(DatetimeInterface::class, $testDateTimeImmutable); $claimsDatetime = [ - 'sub' => new Subject(1), - 'iss' => new Issuer('http://example.com'), - 'exp' => new Expiration($testDateTimeImmutable->modify('+3600 seconds')), - 'nbf' => new NotBefore($testDateTimeImmutable), - 'iat' => new IssuedAt($testDateTimeImmutable), - 'jti' => new JwtId('foo'), + Subject::NAME => new Subject(1), + Issuer::NAME => new Issuer('http://example.com'), + Expiration::NAME => new Expiration($testDateTimeImmutable->modify('+3600 seconds')), + NotBefore::NAME => new NotBefore($testDateTimeImmutable), + IssuedAt::NAME => new IssuedAt($testDateTimeImmutable), + JwtId::NAME => new JwtId('foo'), ]; - $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp), $this->validator); - $payloadDatetime = new Payload(Collection::make($claimsDatetime), $this->validator); + $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp)); + $payloadDatetime = new Payload(Collection::make($claimsDatetime)); $this->assertEquals($payloadTimestamp, $payloadDatetime); } @@ -132,22 +131,50 @@ public function it_should_handle_datetime_immutable_claims() /** @test */ public function it_should_handle_datetinterval_claims() { - $testDateInterval = DateInterval::createFromDateString('PT1H'); + $testDateInterval = new DateInterval('PT1H'); + $carbonDateInterval = CarbonInterval::hours(1); $this->assertInstanceOf(DateInterval::class, $testDateInterval); + $this->assertInstanceOf(DateInterval::class, $carbonDateInterval); $claimsDateInterval = [ - 'sub' => new Subject(1), - 'iss' => new Issuer('http://example.com'), - 'exp' => new Expiration($testDateInterval), - 'nbf' => new NotBefore($this->testNowTimestamp), - 'iat' => new IssuedAt($this->testNowTimestamp), - 'jti' => new JwtId('foo'), + Subject::NAME => new Subject(1), + Issuer::NAME => new Issuer('http://example.com'), + Expiration::NAME => new Expiration($testDateInterval), + NotBefore::NAME => new NotBefore($this->testNowTimestamp), + IssuedAt::NAME => new IssuedAt($this->testNowTimestamp), + JwtId::NAME => new JwtId('foo'), + ]; + + $claimsCarbonInterval = [ + Subject::NAME => new Subject(1), + Issuer::NAME => new Issuer('http://example.com'), + Expiration::NAME => new Expiration($carbonDateInterval), + NotBefore::NAME => new NotBefore($this->testNowTimestamp), + IssuedAt::NAME => new IssuedAt($this->testNowTimestamp), + JwtId::NAME => new JwtId('foo'), ]; - $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp), $this->validator); - $payloadDateInterval = new Payload(Collection::make($claimsDateInterval), $this->validator); + $payloadTimestamp = new Payload(Collection::make($this->claimsTimestamp)); + + $payloadDateInterval = new Payload(Collection::make($claimsDateInterval)); + $payloadClaimInterval = new Payload(Collection::make($claimsCarbonInterval)); $this->assertEquals($payloadTimestamp, $payloadDateInterval); + $this->assertEquals($payloadTimestamp, $payloadClaimInterval); + } + + /** @test */ + public function it_should_get_the_date_interval_instance() + { + $exp = new Expiration($this->testNowTimestamp + ($seconds = 3600)); + $this->assertInstanceOf(CarbonInterval::class, $exp->asCarbonInterval()); + $this->assertEquals(CarbonInterval::seconds($seconds)->cascade(), $exp->asCarbonInterval()); + $this->assertEquals('PT1H', $exp->asCarbonInterval()->spec()); + + $iat = new IssuedAt($this->testNowTimestamp); + $this->assertInstanceOf(CarbonInterval::class, $iat->asCarbonInterval()); + $this->assertEquals(CarbonInterval::seconds(0)->cascade(), $iat->asCarbonInterval()); + $this->assertEquals('PT0S', $iat->asCarbonInterval()->spec()); } } diff --git a/tests/Claims/FactoryTest.php b/tests/Claims/FactoryTest.php index cf915890a..78af12d82 100644 --- a/tests/Claims/FactoryTest.php +++ b/tests/Claims/FactoryTest.php @@ -11,97 +11,60 @@ namespace Tymon\JWTAuth\Test\Claims; -use Illuminate\Http\Request; -use Tymon\JWTAuth\Claims\JwtId; use Tymon\JWTAuth\Claims\Custom; -use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\Expiration; use Tymon\JWTAuth\Claims\Factory; -use Tymon\JWTAuth\Claims\Subject; use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; use Tymon\JWTAuth\Claims\NotBefore; -use Tymon\JWTAuth\Claims\Expiration; -use Tymon\JWTAuth\Test\Fixtures\Foo; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Options; use Tymon\JWTAuth\Test\AbstractTestCase; class FactoryTest extends AbstractTestCase { - /** - * @var \Tymon\JWTAuth\Claims\Factory - */ - protected $factory; - - public function setUp() - { - parent::setUp(); - - $this->factory = new Factory(Request::create('/foo', 'GET')); - } - - /** @test */ - public function it_should_set_the_request() - { - $factory = $this->factory->setRequest(Request::create('/bar', 'GET')); - $this->assertInstanceOf(Factory::class, $factory); - } - - /** @test */ - public function it_should_set_the_ttl() - { - $this->assertInstanceOf(Factory::class, $this->factory->setTTL(30)); - } - - /** @test */ - public function it_should_get_the_ttl() - { - $this->factory->setTTL($ttl = 30); - $this->assertSame($ttl, $this->factory->getTTL()); - } - /** @test */ public function it_should_get_a_defined_claim_instance_when_passing_a_name_and_value() { - $this->assertInstanceOf(Subject::class, $this->factory->get('sub', 1)); - $this->assertInstanceOf(Issuer::class, $this->factory->get('iss', 'http://example.com')); - $this->assertInstanceOf(Expiration::class, $this->factory->get('exp', $this->testNowTimestamp + 3600)); - $this->assertInstanceOf(NotBefore::class, $this->factory->get('nbf', $this->testNowTimestamp)); - $this->assertInstanceOf(IssuedAt::class, $this->factory->get('iat', $this->testNowTimestamp)); - $this->assertInstanceOf(JwtId::class, $this->factory->get('jti', 'foo')); + $this->assertInstanceOf(Subject::class, Factory::get(Subject::NAME, 1)); + $this->assertInstanceOf(Issuer::class, Factory::get(Issuer::NAME, 'http://example.com')); + $this->assertInstanceOf( + Expiration::class, + Factory::get(Expiration::NAME, $this->testNowTimestamp + 3600) + ); + $this->assertInstanceOf(NotBefore::class, Factory::get(NotBefore::NAME, $this->testNowTimestamp)); + $this->assertInstanceOf(IssuedAt::class, Factory::get(IssuedAt::NAME, $this->testNowTimestamp)); + $this->assertInstanceOf(JwtId::class, Factory::get(JwtId::NAME, 'foo')); } /** @test */ public function it_should_get_a_custom_claim_instance_when_passing_a_non_defined_name_and_value() { - $this->assertInstanceOf(Custom::class, $this->factory->get('foo', ['bar'])); + $this->assertInstanceOf(Custom::class, Factory::get('foo', ['bar'])); } /** @test */ - public function it_should_make_a_claim_instance_with_a_value() + public function it_should_make_a_claim_instance_for_inferred_claims() { - $iat = $this->factory->make('iat'); - $this->assertSame($iat->getValue(), $this->testNowTimestamp); + $iat = Factory::get(IssuedAt::NAME, null, new Options([ + 'leeway' => 10, + 'max_refresh_period' => 2, + ])); + $this->assertSame($this->testNowTimestamp, $iat->getValue()); $this->assertInstanceOf(IssuedAt::class, $iat); - - $nbf = $this->factory->make('nbf'); - $this->assertSame($nbf->getValue(), $this->testNowTimestamp); + $this->assertEquals($iat->getLeeway(), 10); + $this->assertEquals($iat->getMaxRefreshPeriod(), 2); + + $nbf = Factory::get(NotBefore::NAME, null, new Options([ + 'leeway' => 20, + 'max_refresh_period' => 1, + ])); + $this->assertSame($this->testNowTimestamp, $nbf->getValue()); $this->assertInstanceOf(NotBefore::class, $nbf); + $this->assertEquals($nbf->getLeeway(), 20); + $this->assertEquals($nbf->getMaxRefreshPeriod(), 1); - $iss = $this->factory->make('iss'); - $this->assertSame($iss->getValue(), 'http://localhost/foo'); - $this->assertInstanceOf(Issuer::class, $iss); - - $exp = $this->factory->make('exp'); - $this->assertSame($exp->getValue(), $this->testNowTimestamp + 3600); - $this->assertInstanceOf(Expiration::class, $exp); - - $jti = $this->factory->make('jti'); - $this->assertInstanceOf(JwtId::class, $jti); - } - - /** @test */ - public function it_should_extend_claim_factory_to_add_a_custom_claim() - { - $this->factory->extend('foo', Foo::class); - - $this->assertInstanceOf(Foo::class, $this->factory->get('foo', 'bar')); + $this->assertInstanceOf(JwtId::class, Factory::get(JwtId::NAME)); } } diff --git a/tests/Claims/IssuedAtTest.php b/tests/Claims/IssuedAtTest.php index 700731130..f57874aea 100644 --- a/tests/Claims/IssuedAtTest.php +++ b/tests/Claims/IssuedAtTest.php @@ -12,17 +12,17 @@ namespace Tymon\JWTAuth\Test\Claims; use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Exceptions\InvalidClaimException; use Tymon\JWTAuth\Test\AbstractTestCase; class IssuedAtTest extends AbstractTestCase { - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\InvalidClaimException - * @expectedExceptionMessage Invalid value provided for claim [iat] - */ + /** @test */ public function it_should_throw_an_exception_when_passing_a_future_timestamp() { + $this->expectException(InvalidClaimException::class); + $this->expectExceptionMessage('Invalid value provided for claim [iat]'); + new IssuedAt($this->testNowTimestamp + 3600); } } diff --git a/tests/Claims/NotBeforeTest.php b/tests/Claims/NotBeforeTest.php index d8475b335..185c108aa 100644 --- a/tests/Claims/NotBeforeTest.php +++ b/tests/Claims/NotBeforeTest.php @@ -12,27 +12,17 @@ namespace Tymon\JWTAuth\Test\Claims; use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Exceptions\InvalidClaimException; use Tymon\JWTAuth\Test\AbstractTestCase; class NotBeforeTest extends AbstractTestCase { - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\InvalidClaimException - * @expectedExceptionMessage Invalid value provided for claim [nbf] - */ - public function it_should_throw_an_exception_when_passing_a_future_timestamp() - { - new NotBefore($this->testNowTimestamp + 3600); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\InvalidClaimException - * @expectedExceptionMessage Invalid value provided for claim [nbf] - */ + /** @test */ public function it_should_throw_an_exception_when_passing_an_invalid_value() { + $this->expectException(InvalidClaimException::class); + $this->expectExceptionMessage('Invalid value provided for claim [nbf]'); + new NotBefore('foo'); } } diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index f915a59d0..2f022737e 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -11,236 +11,102 @@ namespace Tymon\JWTAuth\Test; -use Mockery; -use Tymon\JWTAuth\Factory; -use Tymon\JWTAuth\Payload; -use Tymon\JWTAuth\Claims\JwtId; use Tymon\JWTAuth\Claims\Custom; -use Tymon\JWTAuth\Claims\Issuer; -use Tymon\JWTAuth\Claims\Subject; use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; use Tymon\JWTAuth\Claims\NotBefore; -use Tymon\JWTAuth\Claims\Collection; -use Tymon\JWTAuth\Claims\Expiration; -use Tymon\JWTAuth\Validators\PayloadValidator; -use Tymon\JWTAuth\Claims\Factory as ClaimFactory; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; +use Tymon\JWTAuth\Factory; +use Tymon\JWTAuth\Options; +use Tymon\JWTAuth\Payload; class FactoryTest extends AbstractTestCase { - /** - * @var \Mockery\MockInterface|\Tymon\JWTAuth\Claims\Factory - */ - protected $claimFactory; - - /** - * @var \Mockery\MockInterface|\Tymon\JWTAuth\Validators\PayloadValidator - */ - protected $validator; - - /** - * @var \Tymon\JWTAuth\Factory - */ - protected $factory; - - public function setUp() - { - parent::setUp(); - - $this->claimFactory = Mockery::mock(ClaimFactory::class); - $this->validator = Mockery::mock(PayloadValidator::class); - $this->factory = new Factory($this->claimFactory, $this->validator); - } - /** @test */ public function it_should_return_a_payload_when_passing_an_array_of_claims() { - $expTime = $this->testNowTimestamp + 3600; - - // these are added from default claims - $this->claimFactory->shouldReceive('make')->twice()->with('iss')->andReturn(new Issuer('/foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('exp')->andReturn(new Expiration($expTime)); - $this->claimFactory->shouldReceive('make')->twice()->with('jti')->andReturn(new JwtId('foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('nbf')->andReturn(new NotBefore(123)); - $this->claimFactory->shouldReceive('make')->twice()->with('iat')->andReturn(new IssuedAt(123)); - - // custom claims that override - $this->claimFactory->shouldReceive('get')->twice()->with('sub', 1)->andReturn(new Subject(1)); - $this->claimFactory->shouldReceive('get')->twice()->with('jti', 'foo')->andReturn(new JwtId('foo')); - $this->claimFactory->shouldReceive('get')->twice()->with('nbf', 123)->andReturn(new NotBefore(123)); - $this->claimFactory->shouldReceive('get')->twice()->with('iat', 123)->andReturn(new IssuedAt(123)); - - $this->claimFactory->shouldReceive('getTTL')->andReturn(60); - - // once - $claims = $this->factory->customClaims([ - 'sub' => 1, - 'jti' => 'foo', - 'iat' => 123, - 'nbf' => 123, - ])->buildClaimsCollection(); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($claims); - - // twice - $payload = $this->factory->claims(['sub' => 1, 'jti' => 'foo', 'iat' => 123, 'nbf' => 123])->make(); - - $this->assertSame($payload->get('sub'), 1); - $this->assertSame($payload->get('iat'), 123); - $this->assertSame($payload['exp'], $expTime); - $this->assertSame($payload['jti'], 'foo'); + $payload = Factory::make([ + JwtId::NAME, // auto generated + IssuedAt::NAME, // auto generated + NotBefore::NAME, // auto generated + Subject::NAME => 1, + 'foo' => 'bar', + ]); + + $this->assertSame($payload->get(Subject::NAME), 1); + $this->assertSame($payload(IssuedAt::NAME), $this->testNowTimestamp); + $this->assertSame($payload(NotBefore::NAME), $this->testNowTimestamp); + $this->assertSame($payload['foo'], 'bar'); $this->assertInstanceOf(Payload::class, $payload); + $this->assertInstanceOf(Subject::class, $payload->getInternal(Subject::NAME)); + $this->assertInstanceOf(IssuedAt::class, $payload->getInternal(IssuedAt::NAME)); + $this->assertInstanceOf(JwtId::class, $payload->getInternal(JwtId::NAME)); + $this->assertInstanceOf(NotBefore::class, $payload->getInternal(NotBefore::NAME)); + $this->assertInstanceOf(Custom::class, $payload->getInternal('foo')); } /** @test */ - public function it_should_return_a_payload_when_chaining_claim_methods() + public function it_should_return_a_payload_when_passing_an_array_of_claims_with_values() { - $this->claimFactory->shouldReceive('get')->twice()->with('sub', 1)->andReturn(new Subject(1)); - $this->claimFactory->shouldReceive('get')->twice()->with('foo', 'baz')->andReturn(new Custom('foo', 'baz')); - - $this->claimFactory->shouldReceive('make')->twice()->with('iss')->andReturn(new Issuer('/foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('exp')->andReturn(new Expiration($this->testNowTimestamp + 3600)); - $this->claimFactory->shouldReceive('make')->twice()->with('iat')->andReturn(new IssuedAt($this->testNowTimestamp)); - $this->claimFactory->shouldReceive('make')->twice()->with('jti')->andReturn(new JwtId('foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('nbf')->andReturn(new NotBefore($this->testNowTimestamp)); - - $this->claimFactory->shouldReceive('getTTL')->andReturn(60); - - // once - $claims = $this->factory->sub(1)->foo('baz')->buildClaimsCollection(); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($claims); - - // twice - $payload = $this->factory->sub(1)->foo('baz')->make(); - - $this->assertSame($payload['sub'], 1); - $this->assertSame($payload->get('jti'), 'foo'); - $this->assertSame($payload->get('foo'), 'baz'); + $payload = Factory::make([ + JwtId::NAME => 'foo', + IssuedAt::NAME => $this->testNowTimestamp - 3600, + Issuer::NAME => 'example.com', + Subject::NAME => 1, + 'foo' => 'bar', + ]); + + $this->assertSame($payload->get(Subject::NAME), 1); + $this->assertSame($payload->get(JwtId::NAME), 'foo'); + $this->assertSame($payload(IssuedAt::NAME), $this->testNowTimestamp - 3600); + $this->assertSame($payload['foo'], 'bar'); + $this->assertSame($payload[Issuer::NAME], 'example.com'); $this->assertInstanceOf(Payload::class, $payload); + $this->assertInstanceOf(Subject::class, $payload->getInternal(Subject::NAME)); + $this->assertInstanceOf(IssuedAt::class, $payload->getInternal(IssuedAt::NAME)); + $this->assertInstanceOf(JwtId::class, $payload->getInternal(JwtId::NAME)); + $this->assertInstanceOf(Issuer::class, $payload->getInternal(Issuer::NAME)); + $this->assertInstanceOf(Custom::class, $payload->getInternal('foo')); } /** @test */ - public function it_should_return_a_payload_when_passing_miltidimensional_array_as_custom_claim_to_make_method() + public function it_should_run_a_custom_validator_and_throw_exception() { - // these are added from default claims - $this->claimFactory->shouldReceive('make')->twice()->with('iss')->andReturn(new Issuer('/foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('exp')->andReturn(new Expiration($this->testNowTimestamp + 3600)); - $this->claimFactory->shouldReceive('make')->twice()->with('jti')->andReturn(new JwtId('foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('nbf')->andReturn(new NotBefore(123)); - $this->claimFactory->shouldReceive('make')->twice()->with('iat')->andReturn(new IssuedAt(123)); - - // custom claims that override - $this->claimFactory->shouldReceive('get')->twice()->with('sub', 1)->andReturn(new Subject(1)); - $this->claimFactory->shouldReceive('get')->twice()->with('foo', ['bar' => [0, 0, 0]])->andReturn(new Custom('foo', ['bar' => [0, 0, 0]])); - - $this->claimFactory->shouldReceive('getTTL')->andReturn(60); - - // once - $claims = $this->factory->sub(1)->foo(['bar' => [0, 0, 0]])->buildClaimsCollection(); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($claims); - - // twice - $payload = $this->factory->sub(1)->foo(['bar' => [0, 0, 0]])->make(); - - $this->assertSame($payload->get('sub'), 1); - $this->assertSame($payload->get('jti'), 'foo'); - $this->assertSame($payload->get('foo'), ['bar' => [0, 0, 0]]); - $this->assertSame($payload->get('foo.bar'), [0, 0, 0]); - - $this->assertInstanceOf(Payload::class, $payload); - } - - /** @test */ - public function it_should_exclude_the_exp_claim_when_setting_ttl_to_null() - { - // these are added from default claims - $this->claimFactory->shouldReceive('make')->twice()->with('iss')->andReturn(new Issuer('/foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('jti')->andReturn(new JwtId('foo')); - $this->claimFactory->shouldReceive('make')->twice()->with('nbf')->andReturn(new NotBefore(123)); - $this->claimFactory->shouldReceive('make')->twice()->with('iat')->andReturn(new IssuedAt(123)); - - // custom claims that override - $this->claimFactory->shouldReceive('get')->twice()->with('sub', 1)->andReturn(new Subject(1)); - - $this->claimFactory->shouldReceive('setTTL')->with(null)->andReturn($this->claimFactory); - $this->claimFactory->shouldReceive('getTTL')->andReturn(null); - - // once - $claims = $this->factory->setTTL(null)->sub(1)->buildClaimsCollection(); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($claims); - - // twice - $payload = $this->factory->setTTL(null)->sub(1)->make(); - - $this->assertNull($payload->get('exp')); - - $this->assertInstanceOf(Payload::class, $payload); - } - - /** @test */ - public function it_should_exclude_claims_from_previous_payloads() - { - $fooClaim = new Custom('foo', 'bar'); - $barClaim = new Custom('baz', 'qux'); - - $this->claimFactory->shouldReceive('getTTL')->andReturn(60); - $this->claimFactory->shouldReceive('get')->with('foo', 'bar')->twice()->andReturn($fooClaim); - $this->claimFactory->shouldReceive('get')->with('baz', 'qux')->twice()->andReturn($barClaim); - $this->validator->shouldReceive('setRefreshFlow->check')->once()->andReturn(new Collection([$fooClaim, $barClaim])); - - $payload = $this->factory->setDefaultClaims([]) - ->customClaims([ - 'foo' => 'bar', - 'baz' => 'qux', - ])->make(); - - $this->assertSame($payload->get('foo'), 'bar'); - $this->assertSame($payload->get('baz'), 'qux'); - - $this->validator->shouldReceive('setRefreshFlow->check')->once()->andReturn(new Collection([$fooClaim])); - - $payload = $this->factory->setDefaultClaims([])->customClaims(['foo' => 'bar'])->make(true); - - $this->assertSame($payload->get('foo'), 'bar'); - $this->assertFalse($payload->hasKey('baz')); - } - - /** @test */ - public function it_should_set_the_default_claims() - { - $this->factory->setDefaultClaims(['sub', 'iat']); - - $this->assertSame($this->factory->getDefaultClaims(), ['sub', 'iat']); - } - - /** @test */ - public function it_should_get_payload_with_a_predefined_collection_of_claims() - { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp + 3600), - new NotBefore($this->testNowTimestamp), - new IssuedAt($this->testNowTimestamp), - new JwtId('foo'), - ]; - - $collection = Collection::make($claims); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - - $payload = $this->factory->withClaims($collection); - - $this->assertInstanceOf(Payload::class, $payload); - $this->assertSame($payload->get('sub'), 1); + $this->expectException(TokenInvalidException::class); + $this->expectExceptionMessage('Validation failed for claim [foo]'); + + Factory::make([ + JwtId::NAME => 'foo', + IssuedAt::NAME => $this->testNowTimestamp - 3600, + Issuer::NAME => 'example.com', + Subject::NAME => 1, + 'foo' => 'bar', + ], new Options([ + 'validators' => [ + // This will fail as the value is `bar` + 'foo' => fn ($value) => $value === 'baz', + ], + ])); } /** @test */ - public function it_should_get_the_validator() + public function it_should_not_run_a_custom_validator_for_a_non_existent_claim() { - $this->assertInstanceOf(PayloadValidator::class, $this->factory->validator()); + Factory::make([ + JwtId::NAME => 'foo', + IssuedAt::NAME => $this->testNowTimestamp - 3600, + Issuer::NAME => 'example.com', + Subject::NAME => 1, + 'foo' => 'bar', + ], new Options([ + 'validators' => [ + // The `bar` claim does not exist + 'bar' => fn ($value) => $value === 'baz', + ], + ])); } } diff --git a/tests/Http/ParserTest.php b/tests/Http/ParserTest.php index ffc3c41cb..3c4c05147 100644 --- a/tests/Http/ParserTest.php +++ b/tests/Http/ParserTest.php @@ -11,18 +11,18 @@ namespace Tymon\JWTAuth\Test\Http; -use Mockery; use Illuminate\Http\Request; use Illuminate\Routing\Route; use Illuminate\Support\Facades\Crypt; -use Tymon\JWTAuth\Http\Parser\Parser; -use Tymon\JWTAuth\Http\Parser\Cookies; -use Tymon\JWTAuth\Test\AbstractTestCase; +use Mockery; use Tymon\JWTAuth\Http\Parser\AuthHeaders; +use Tymon\JWTAuth\Http\Parser\Cookies; use Tymon\JWTAuth\Http\Parser\InputSource; +use Tymon\JWTAuth\Http\Parser\LumenRouteParams; +use Tymon\JWTAuth\Http\Parser\Parser; use Tymon\JWTAuth\Http\Parser\QueryString; use Tymon\JWTAuth\Http\Parser\RouteParams; -use Tymon\JWTAuth\Http\Parser\LumenRouteParams; +use Tymon\JWTAuth\Test\AbstractTestCase; class ParserTest extends AbstractTestCase { @@ -35,10 +35,10 @@ public function it_should_return_the_token_from_the_authorization_header() $parser = new Parser($request); $parser->setChain([ - new QueryString, - new InputSource, - new AuthHeaders, - new RouteParams, + 'query' => new QueryString(), + 'input' => new InputSource(), + 'header' => new AuthHeaders(), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -54,12 +54,14 @@ public function it_should_return_the_token_from_the_prefixed_authentication_head $parser = new Parser($request); $parser->setChain([ - new QueryString, - new InputSource, - (new AuthHeaders)->setHeaderPrefix('Custom'), - new RouteParams, + 'query' => new QueryString(), + 'input' => new InputSource(), + 'header' => new AuthHeaders(), + 'route' => new RouteParams(), ]); + $parser->get('header')->setHeaderPrefix('Custom'); + $this->assertSame($parser->parseToken(), 'foobar'); $this->assertTrue($parser->hasToken()); } @@ -73,10 +75,10 @@ public function it_should_return_the_token_from_the_custom_authentication_header $parser = new Parser($request); $parser->setChain([ - new QueryString, - new InputSource, - (new AuthHeaders)->setHeaderName('custom_authorization'), - new RouteParams, + 'query' => new QueryString(), + 'input' => new InputSource(), + 'header' => (new AuthHeaders())->setHeaderName('custom_authorization'), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -93,10 +95,10 @@ public function it_should_return_the_token_from_the_alt_authorization_headers() $request2->server->set('REDIRECT_HTTP_AUTHORIZATION', 'Bearer foobarbaz'); $parser = new Parser($request1, [ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -114,10 +116,10 @@ public function it_should_return_the_token_from_query_string() $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -131,10 +133,10 @@ public function it_should_return_the_token_from_the_custom_query_string() $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - (new QueryString)->setKey('custom_token_key'), - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => (new QueryString())->setKey('custom_token_key'), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -144,14 +146,22 @@ public function it_should_return_the_token_from_the_custom_query_string() /** @test */ public function it_should_return_the_token_from_the_query_string_not_the_input_source() { - $request = Request::create('foo?token=foobar', 'POST', [], [], [], [], json_encode(['token' => 'foobarbaz'])); + $request = Request::create( + 'foo?token=foobar', + 'POST', + [], + [], + [], + [], + json_encode(['token' => 'foobarbaz']) + ); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -161,14 +171,22 @@ public function it_should_return_the_token_from_the_query_string_not_the_input_s /** @test */ public function it_should_return_the_token_from_the_custom_query_string_not_the_custom_input_source() { - $request = Request::create('foo?custom_token_key=foobar', 'POST', [], [], [], [], json_encode(['custom_token_key' => 'foobarbaz'])); + $request = Request::create( + 'foo?custom_token_key=foobar', + 'POST', + [], + [], + [], + [], + json_encode(['custom_token_key' => 'foobarbaz']) + ); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - (new QueryString)->setKey('custom_token_key'), - (new InputSource)->setKey('custom_token_key'), - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => (new QueryString())->setKey('custom_token_key'), + 'input' => (new InputSource())->setKey('custom_token_key'), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -178,15 +196,23 @@ public function it_should_return_the_token_from_the_custom_query_string_not_the_ /** @test */ public function it_should_return_the_token_from_input_source() { - $request = Request::create('foo', 'POST', [], [], [], [], json_encode(['token' => 'foobar'])); + $request = Request::create( + 'foo', + 'POST', + [], + [], + [], + [], + json_encode(['token' => 'foobar']) + ); $request->headers->set('Content-Type', 'application/json'); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -196,15 +222,23 @@ public function it_should_return_the_token_from_input_source() /** @test */ public function it_should_return_the_token_from_the_custom_input_source() { - $request = Request::create('foo', 'POST', [], [], [], [], json_encode(['custom_token_key' => 'foobar'])); + $request = Request::create( + 'foo', + 'POST', + [], + [], + [], + [], + json_encode(['custom_token_key' => 'foobar']) + ); $request->headers->set('Content-Type', 'application/json'); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - (new InputSource)->setKey('custom_token_key'), - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => (new InputSource())->setKey('custom_token_key'), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -218,10 +252,10 @@ public function it_should_return_the_token_from_an_unencrypted_cookie() $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), new Cookies(false), ]); @@ -241,10 +275,10 @@ public function it_should_return_the_token_from_a_crypted_cookie() $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), new Cookies(true), ]); @@ -261,16 +295,14 @@ public function it_should_return_the_token_from_a_crypted_cookie() public function it_should_return_the_token_from_route() { $request = Request::create('foo', 'GET', ['foo' => 'bar']); - $request->setRouteResolver(function () { - return $this->getRouteMock('foobar'); - }); + $request->setRouteResolver(fn () => $this->getRouteMock('foobar')); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -281,16 +313,14 @@ public function it_should_return_the_token_from_route() public function it_should_return_the_token_from_route_with_a_custom_param() { $request = Request::create('foo', 'GET', ['foo' => 'bar']); - $request->setRouteResolver(function () { - return $this->getRouteMock('foobar', 'custom_route_param'); - }); + $request->setRouteResolver(fn () => $this->getRouteMock('foobar', 'custom_route_param')); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - (new RouteParams)->setKey('custom_route_param'), + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => (new RouteParams())->setKey('custom_route_param'), ]); $this->assertSame($parser->parseToken(), 'foobar'); @@ -307,10 +337,10 @@ public function it_should_ignore_routeless_requests() $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertNull($parser->parseToken()); @@ -321,16 +351,18 @@ public function it_should_ignore_routeless_requests() public function it_should_ignore_lumen_request_arrays() { $request = Request::create('foo', 'GET', ['foo' => 'bar']); - $request->setRouteResolver(function () { - return [false, ['uses' => 'someController'], ['token' => 'foobar']]; - }); + $request->setRouteResolver(fn () => [ + false, + ['uses' => 'someController'], + ['token' => 'foobar'], + ]); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertNull($parser->parseToken()); @@ -341,16 +373,18 @@ public function it_should_ignore_lumen_request_arrays() public function it_should_accept_lumen_request_arrays_with_special_class() { $request = Request::create('foo', 'GET', ['foo' => 'bar']); - $request->setRouteResolver(function () { - return [false, ['uses' => 'someController'], ['token' => 'foo.bar.baz']]; - }); + $request->setRouteResolver(fn () => [ + false, + ['uses' => 'someController'], + ['token' => 'foo.bar.baz'], + ]); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new LumenRouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + new LumenRouteParams(), ]); $this->assertSame($parser->parseToken(), 'foo.bar.baz'); @@ -361,16 +395,14 @@ public function it_should_accept_lumen_request_arrays_with_special_class() public function it_should_return_null_if_no_token_in_request() { $request = Request::create('foo', 'GET', ['foo' => 'bar']); - $request->setRouteResolver(function () { - return $this->getRouteMock(); - }); + $request->setRouteResolver(fn () => $this->getRouteMock()); $parser = new Parser($request); $parser->setChain([ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]); $this->assertNull($parser->parseToken()); @@ -381,10 +413,10 @@ public function it_should_return_null_if_no_token_in_request() public function it_should_retrieve_the_chain() { $chain = [ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]; $parser = new Parser(Mockery::mock(Request::class)); @@ -397,10 +429,10 @@ public function it_should_retrieve_the_chain() public function it_should_retrieve_the_chain_with_alias() { $chain = [ - new AuthHeaders, - new QueryString, - new InputSource, - new RouteParams, + 'header' => new AuthHeaders(), + 'query' => new QueryString(), + 'input' => new InputSource(), + 'route' => new RouteParams(), ]; /* @var \Illuminate\Http\Request $request */ @@ -415,12 +447,14 @@ public function it_should_retrieve_the_chain_with_alias() /** @test */ public function it_should_set_the_cookie_key() { - $cookies = (new Cookies)->setKey('test'); + $cookies = (new Cookies())->setKey('test'); $this->assertInstanceOf(Cookies::class, $cookies); } - protected function getRouteMock($expectedParameterValue = null, $expectedParameterName = 'token') - { + protected function getRouteMock( + $expectedParameterValue = null, + $expectedParameterName = 'token' + ) { return Mockery::mock(Route::class) ->shouldReceive('parameter') ->with($expectedParameterName) diff --git a/tests/JWTAuthTest.php b/tests/JWTAuthTest.php deleted file mode 100644 index c7c2114cf..000000000 --- a/tests/JWTAuthTest.php +++ /dev/null @@ -1,343 +0,0 @@ - - * - * 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 stdClass; -use Tymon\JWTAuth\Token; -use Tymon\JWTAuth\Factory; -use Tymon\JWTAuth\JWTAuth; -use Tymon\JWTAuth\Manager; -use Tymon\JWTAuth\Payload; -use Illuminate\Http\Request; -use Tymon\JWTAuth\Http\Parser\Parser; -use Tymon\JWTAuth\Test\Stubs\UserStub; -use Tymon\JWTAuth\Exceptions\JWTException; -use Tymon\JWTAuth\Contracts\Providers\Auth; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; - -class JWTAuthTest extends AbstractTestCase -{ - /** - * @var \Mockery\MockInterface|\Tymon\JWTAuth\Manager - */ - protected $manager; - - /** - * @var \Mockery\MockInterface|\Tymon\JWTAuth\Contracts\Providers\Auth - */ - protected $auth; - - /** - * @var \Mockery\MockInterface|\Tymon\JWTAuth\Http\Parser\Parser - */ - protected $parser; - - /** - * @var \Tymon\JWTAuth\JWTAuth - */ - protected $jwtAuth; - - public function setUp() - { - $this->manager = Mockery::mock(Manager::class); - $this->auth = Mockery::mock(Auth::class); - $this->parser = Mockery::mock(Parser::class); - $this->jwtAuth = new JWTAuth($this->manager, $this->auth, $this->parser); - } - - /** @test */ - public function it_should_return_a_token_when_passing_a_user() - { - $payloadFactory = Mockery::mock(Factory::class); - $payloadFactory->shouldReceive('make')->andReturn(Mockery::mock(Payload::class)); - - $this->manager - ->shouldReceive('getPayloadFactory->customClaims') - ->once() - ->with(['sub' => 1, 'prv' => sha1('Tymon\JWTAuth\Test\Stubs\UserStub'), 'foo' => 'bar', 'role' => 'admin']) - ->andReturn($payloadFactory); - - $this->manager->shouldReceive('encode->get')->once()->andReturn('foo.bar.baz'); - - $token = $this->jwtAuth->fromUser(new UserStub); - - $this->assertSame($token, 'foo.bar.baz'); - } - - /** @test */ - public function it_should_pass_provider_check_if_hash_matches() - { - $payloadFactory = Mockery::mock(Factory::class); - $payloadFactory->shouldReceive('make')->andReturn(Mockery::mock(Payload::class)); - $payloadFactory->shouldReceive('get') - ->with('prv') - ->andReturn(sha1('Tymon\JWTAuth\Test\Stubs\UserStub')); - - $this->manager->shouldReceive('decode')->once()->andReturn($payloadFactory); - - $this->assertTrue($this->jwtAuth->setToken('foo.bar.baz')->checkSubjectModel('Tymon\JWTAuth\Test\Stubs\UserStub')); - } - - /** @test */ - public function it_should_pass_provider_check_if_hash_matches_when_provider_is_null() - { - $payloadFactory = Mockery::mock(Factory::class); - $payloadFactory->shouldReceive('make')->andReturn(Mockery::mock(Payload::class)); - $payloadFactory->shouldReceive('get') - ->with('prv') - ->andReturnNull(); - - $this->manager->shouldReceive('decode')->once()->andReturn($payloadFactory); - - $this->assertTrue($this->jwtAuth->setToken('foo.bar.baz')->checkSubjectModel('Tymon\JWTAuth\Test\Stubs\UserStub')); - } - - /** @test */ - public function it_should_not_pass_provider_check_if_hash_not_match() - { - $payloadFactory = Mockery::mock(Factory::class); - $payloadFactory->shouldReceive('make')->andReturn(Mockery::mock(Payload::class)); - $payloadFactory->shouldReceive('get') - ->with('prv') - ->andReturn(sha1('Tymon\JWTAuth\Test\Stubs\UserStub1')); - - $this->manager->shouldReceive('decode')->once()->andReturn($payloadFactory); - - $this->assertFalse($this->jwtAuth->setToken('foo.bar.baz')->checkSubjectModel('Tymon\JWTAuth\Test\Stubs\UserStub')); - } - - /** @test */ - public function it_should_return_a_token_when_passing_valid_credentials_to_attempt_method() - { - $payloadFactory = Mockery::mock(Factory::class); - $payloadFactory->shouldReceive('make')->andReturn(Mockery::mock(Payload::class)); - - $this->manager - ->shouldReceive('getPayloadFactory->customClaims') - ->once() - ->with(['sub' => 1, 'prv' => sha1('Tymon\JWTAuth\Test\Stubs\UserStub'), 'foo' => 'bar', 'role' => 'admin']) - ->andReturn($payloadFactory); - - $this->manager->shouldReceive('encode->get')->once()->andReturn('foo.bar.baz'); - - $this->auth->shouldReceive('byCredentials')->once()->andReturn(true); - $this->auth->shouldReceive('user')->once()->andReturn(new UserStub); - - $token = $this->jwtAuth->attempt(['foo' => 'bar']); - - $this->assertSame($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(['foo' => 'bar']); - - $this->assertFalse($token); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage A token is required - */ - public function it_should_throw_an_exception_when_not_providing_a_token() - { - $this->jwtAuth->toUser(); - } - - /** @test */ - public function it_should_return_the_owning_user_from_a_token_containing_an_existing_user() - { - $payload = Mockery::mock(Payload::class); - $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->setToken('foo.bar.baz')->customClaims(['foo' => 'bar'])->authenticate(); - - $this->assertSame($user->id, 1); - } - - /** @test */ - public function it_should_return_false_when_passing_a_token_not_containing_an_existing_user() - { - $payload = Mockery::mock(Payload::class); - $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->setToken('foo.bar.baz')->authenticate(); - - $this->assertFalse($user); - } - - /** @test */ - public function it_should_refresh_a_token() - { - $newToken = Mockery::mock(Token::class); - $newToken->shouldReceive('get')->once()->andReturn('baz.bar.foo'); - - $this->manager->shouldReceive('customClaims->refresh')->once()->andReturn($newToken); - - $result = $this->jwtAuth->setToken('foo.bar.baz')->refresh(); - - $this->assertSame($result, 'baz.bar.foo'); - } - - /** @test */ - public function it_should_invalidate_a_token() - { - $token = new Token('foo.bar.baz'); - - $this->manager->shouldReceive('invalidate')->once()->with($token, false)->andReturn(true); - - $this->jwtAuth->setToken($token)->invalidate(); - } - - /** @test */ - public function it_should_force_invalidate_a_token_forever() - { - $token = new Token('foo.bar.baz'); - - $this->manager->shouldReceive('invalidate')->once()->with($token, true)->andReturn(true); - - $this->jwtAuth->setToken($token)->invalidate(true); - } - - /** @test */ - public function it_should_retrieve_the_token_from_the_request() - { - $this->parser->shouldReceive('parseToken')->andReturn('foo.bar.baz'); - - $this->assertInstanceOf(Token::class, $this->jwtAuth->parseToken()->getToken()); - $this->assertEquals($this->jwtAuth->getToken(), 'foo.bar.baz'); - } - - /** @test */ - public function it_should_get_the_authenticated_user() - { - $manager = $this->jwtAuth->manager(); - $this->assertInstanceOf(Manager::class, $manager); - } - - /** @test */ - public function it_should_return_false_if_the_token_is_invalid() - { - $this->parser->shouldReceive('parseToken')->andReturn('foo.bar.baz'); - $this->manager->shouldReceive('decode')->once()->andThrow(new TokenInvalidException); - - $this->assertFalse($this->jwtAuth->parseToken()->check()); - } - - /** @test */ - public function it_should_return_true_if_the_token_is_valid() - { - $payload = Mockery::mock(Payload::class); - - $this->parser->shouldReceive('parseToken')->andReturn('foo.bar.baz'); - $this->manager->shouldReceive('decode')->once()->andReturn($payload); - - $this->assertTrue($this->jwtAuth->parseToken()->check()); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage The token could not be parsed from the request - */ - public function it_should_throw_an_exception_when_token_not_present_in_request() - { - $this->parser->shouldReceive('parseToken')->andReturn(false); - - $this->jwtAuth->parseToken(); - } - - /** @test */ - public function it_should_return_false_when_no_token_is_set() - { - $this->parser->shouldReceive('parseToken')->andReturn(false); - - $this->assertNull($this->jwtAuth->getToken()); - } - - /** @test */ - public function it_should_magically_call_the_manager() - { - $this->manager->shouldReceive('getBlacklist')->andReturn(new stdClass); - - $blacklist = $this->jwtAuth->manager()->getBlacklist(); - - $this->assertInstanceOf(stdClass::class, $blacklist); - } - - /** @test */ - public function it_should_set_the_request() - { - $request = Request::create('/foo', 'GET', ['token' => 'some.random.token']); - - $this->parser->shouldReceive('setRequest')->once()->with($request); - $this->parser->shouldReceive('parseToken')->andReturn('some.random.token'); - - $token = $this->jwtAuth->setRequest($request)->getToken(); - - $this->assertEquals('some.random.token', $token); - } - - /** @test */ - public function it_should_unset_the_token() - { - $this->parser->shouldReceive('parseToken')->andThrow(new JWTException); - $token = new Token('foo.bar.baz'); - $this->jwtAuth->setToken($token); - - $this->assertSame($this->jwtAuth->getToken(), $token); - $this->jwtAuth->unsetToken(); - $this->assertNull($this->jwtAuth->getToken()); - } - - /** @test */ - public function it_should_get_the_manager_instance() - { - $manager = $this->jwtAuth->manager(); - $this->assertInstanceOf(Manager::class, $manager); - } - - /** @test */ - public function it_should_get_the_parser_instance() - { - $parser = $this->jwtAuth->parser(); - $this->assertInstanceOf(Parser::class, $parser); - } - - /** @test */ - public function it_should_get_a_claim_value() - { - $payload = Mockery::mock(Payload::class); - $payload->shouldReceive('get')->once()->with('sub')->andReturn(1); - - $this->manager->shouldReceive('decode')->once()->andReturn($payload); - - $this->assertSame($this->jwtAuth->setToken('foo.bar.baz')->getClaim('sub'), 1); - } -} diff --git a/tests/JWTGuardTest.php b/tests/JWTGuardTest.php index d2fbed9d8..3a39d83c4 100644 --- a/tests/JWTGuardTest.php +++ b/tests/JWTGuardTest.php @@ -11,14 +11,24 @@ namespace Tymon\JWTAuth\Test; +use Illuminate\Auth\EloquentUserProvider; +use Illuminate\Http\Request; +use Illuminate\Support\Testing\Fakes\EventFake; use Mockery; +use Tymon\JWTAuth\Builder; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Events\JWTAttempt; +use Tymon\JWTAuth\Events\JWTInvalidate; +use Tymon\JWTAuth\Events\JWTLogin; +use Tymon\JWTAuth\Events\JWTLogout; +use Tymon\JWTAuth\Events\JWTRefresh; +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\UserNotDefinedException; use Tymon\JWTAuth\JWT; -use Tymon\JWTAuth\Factory; -use Tymon\JWTAuth\Payload; use Tymon\JWTAuth\JWTGuard; -use Illuminate\Http\Request; -use Illuminate\Auth\EloquentUserProvider; +use Tymon\JWTAuth\Payload; use Tymon\JWTAuth\Test\Stubs\LaravelUserStub; +use Tymon\JWTAuth\Token; class JWTGuardTest extends AbstractTestCase { @@ -37,48 +47,65 @@ class JWTGuardTest extends AbstractTestCase */ protected $guard; - public function setUp() + /** + * @var \Illuminate\Support\Testing\Fakes\EventFake|\Mockery\MockInterface + */ + protected $events; + + public function setUp(): void { parent::setUp(); $this->jwt = Mockery::mock(JWT::class); $this->provider = Mockery::mock(EloquentUserProvider::class); - $this->guard = new JWTGuard($this->jwt, $this->provider, Request::create('/foo', 'GET')); + + $this->events = Mockery::mock(EventFake::class); + + $this->guard = new JWTGuard( + $this->jwt, + $this->provider, + Request::create('/foo', 'GET'), + $this->events + ); + $this->guard->useResponsable(false); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_get_the_request() { $this->assertInstanceOf(Request::class, $this->guard->getRequest()); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_get_the_authenticated_user_if_a_valid_token_is_provided() { - $payload = Mockery::mock(Payload::class); - $payload->shouldReceive('offsetGet')->once()->with('sub')->andReturn(1); + $payload = Mockery::mock(Payload::class) + ->shouldReceive('offsetGet') + ->once() + ->with(Subject::NAME) + ->andReturn(1) + ->getMock(); $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->once()->andReturn('foo.bar.baz'); - $this->jwt->shouldReceive('check')->once()->with(true)->andReturn($payload); + $this->jwt->shouldReceive('getToken') + ->once() + ->andReturn(new Token('foo.bar.baz')); + $this->jwt->shouldReceive('check') + ->once() + ->with(true) + ->andReturn($payload); $this->jwt->shouldReceive('checkSubjectModel') - ->once() - ->with('\Tymon\JWTAuth\Test\Stubs\LaravelUserStub') - ->andReturn(true); + ->once() + ->with(LaravelUserStub::class, $payload) + ->andReturn(true); $this->provider->shouldReceive('getModel') - ->once() - ->andReturn('\Tymon\JWTAuth\Test\Stubs\LaravelUserStub'); + ->once() + ->andReturn(LaravelUserStub::class); $this->provider->shouldReceive('retrieveById') - ->once() - ->with(1) - ->andReturn((object) ['id' => 1]); + ->once() + ->with(1) + ->andReturn((object) ['id' => 1]); $this->assertSame(1, $this->guard->user()->id); @@ -90,30 +117,36 @@ public function it_should_get_the_authenticated_user_if_a_valid_token_is_provide $this->assertSame(1, $this->guard->userOrFail()->id); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_get_the_authenticated_user_if_a_valid_token_is_provided_and_not_throw_an_exception() { - $payload = Mockery::mock(Payload::class); - $payload->shouldReceive('offsetGet')->once()->with('sub')->andReturn(1); + $payload = Mockery::mock(Payload::class) + ->shouldReceive('offsetGet') + ->once() + ->with(Subject::NAME) + ->andReturn(1) + ->getMock(); $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->once()->andReturn('foo.bar.baz'); - $this->jwt->shouldReceive('check')->once()->with(true)->andReturn($payload); + $this->jwt->shouldReceive('getToken') + ->once() + ->andReturn(new Token('foo.bar.baz')); + $this->jwt->shouldReceive('check') + ->once() + ->with(true) + ->andReturn($payload); $this->jwt->shouldReceive('checkSubjectModel') - ->once() - ->with('\Tymon\JWTAuth\Test\Stubs\LaravelUserStub') - ->andReturn(true); + ->once() + ->with('\Tymon\JWTAuth\Test\Stubs\LaravelUserStub', $payload) + ->andReturn(true); $this->provider->shouldReceive('getModel') - ->once() - ->andReturn('\Tymon\JWTAuth\Test\Stubs\LaravelUserStub'); + ->once() + ->andReturn('\Tymon\JWTAuth\Test\Stubs\LaravelUserStub'); $this->provider->shouldReceive('retrieveById') - ->once() - ->with(1) - ->andReturn((object) ['id' => 1]); + ->once() + ->with(1) + ->andReturn((object) ['id' => 1]); $this->assertSame(1, $this->guard->userOrFail()->id); @@ -122,15 +155,16 @@ public function it_should_get_the_authenticated_user_if_a_valid_token_is_provide $this->assertTrue($this->guard->check()); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_return_null_if_an_invalid_token_is_provided() { $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->twice()->andReturn('invalid.token.here'); - $this->jwt->shouldReceive('check')->twice()->andReturn(false); + $this->jwt->shouldReceive('getToken') + ->twice() + ->andReturn(new Token('invalid.token.here')); + $this->jwt->shouldReceive('check') + ->twice() + ->andReturn(false); $this->jwt->shouldReceive('getPayload->get')->never(); $this->provider->shouldReceive('retrieveById')->never(); @@ -138,14 +172,11 @@ public function it_should_return_null_if_an_invalid_token_is_provided() $this->assertFalse($this->guard->check()); // twice } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_return_null_if_no_token_is_provided() { $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->andReturn(false); + $this->jwt->shouldReceive('getToken')->andReturn(null); $this->jwt->shouldReceive('check')->never(); $this->jwt->shouldReceive('getPayload->get')->never(); $this->provider->shouldReceive('retrieveById')->never(); @@ -154,17 +185,19 @@ public function it_should_return_null_if_no_token_is_provided() $this->assertFalse($this->guard->check()); } - /** - * @test - * @group laravel-5.2 - * @expectedException \Tymon\JWTAuth\Exceptions\UserNotDefinedException - * @expectedExceptionMessage An error occurred - */ + /** @test */ public function it_should_throw_an_exception_if_an_invalid_token_is_provided() { + $this->expectException(UserNotDefinedException::class); + $this->expectExceptionMessage('User not defined'); + $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->twice()->andReturn('invalid.token.here'); - $this->jwt->shouldReceive('check')->twice()->andReturn(false); + $this->jwt->shouldReceive('getToken') + ->twice() + ->andReturn(new Token('invalid.token.here')); + $this->jwt->shouldReceive('check') + ->twice() + ->andReturn(false); $this->jwt->shouldReceive('getPayload->get')->never(); $this->provider->shouldReceive('retrieveById')->never(); @@ -172,16 +205,14 @@ public function it_should_throw_an_exception_if_an_invalid_token_is_provided() $this->guard->userOrFail(); // twice, throws the exception } - /** - * @test - * @group laravel-5.2 - * @expectedException \Tymon\JWTAuth\Exceptions\UserNotDefinedException - * @expectedExceptionMessage An error occurred - */ + /** @test */ public function it_should_throw_an_exception_if_no_token_is_provided() { + $this->expectException(UserNotDefinedException::class); + $this->expectExceptionMessage('User not defined'); + $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->andReturn(false); + $this->jwt->shouldReceive('getToken')->andReturn(null); $this->jwt->shouldReceive('check')->never(); $this->jwt->shouldReceive('getPayload->get')->never(); $this->provider->shouldReceive('retrieveById')->never(); @@ -190,300 +221,321 @@ public function it_should_throw_an_exception_if_no_token_is_provided() $this->guard->userOrFail(); // throws the exception } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_return_a_token_if_credentials_are_ok_and_user_is_found() { $credentials = ['foo' => 'bar', 'baz' => 'bob']; - $user = new LaravelUserStub; + $user = new LaravelUserStub(); + + $this->events->shouldReceive('dispatch')->times(2); + $this->events->shouldReceive('assertDispatched')->times(2); $this->provider->shouldReceive('retrieveByCredentials') - ->once() - ->with($credentials) - ->andReturn($user); + ->once() + ->with($credentials) + ->andReturn($user); $this->provider->shouldReceive('validateCredentials') - ->once() - ->with($user, $credentials) - ->andReturn(true); + ->once() + ->with($user, $credentials) + ->andReturn(true); $this->jwt->shouldReceive('fromUser') - ->once() - ->with($user) - ->andReturn('foo.bar.baz'); + ->once() + ->with($user) + ->andReturn($token = new Token('foo.bar.baz')); $this->jwt->shouldReceive('setToken') - ->once() - ->with('foo.bar.baz') - ->andReturnSelf(); + ->once() + ->with($token) + ->andReturnSelf(); $this->jwt->shouldReceive('claims') - ->once() - ->with(['foo' => 'bar']) - ->andReturnSelf(); + ->once() + ->with(['foo' => 'bar']) + ->andReturnSelf(); - $token = $this->guard->claims(['foo' => 'bar'])->attempt($credentials); + $jwt = $this->guard->claims(['foo' => 'bar'])->attempt($credentials); + $this->events->assertDispatched(JWTAttempt::class, 1); + $this->events->assertDispatched(JWTLogin::class, 1); $this->assertSame($this->guard->getLastAttempted(), $user); - $this->assertSame($token, 'foo.bar.baz'); + $this->assertTrue($jwt->matches($token)); + $this->assertSame((string) $jwt, 'foo.bar.baz'); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_return_true_if_credentials_are_ok_and_user_is_found_when_choosing_not_to_login() { $credentials = ['foo' => 'bar', 'baz' => 'bob']; - $user = new LaravelUserStub; + $user = new LaravelUserStub(); + + $this->events->shouldReceive('dispatch')->times(2); + $this->events->shouldReceive('assertDispatched')->once(); $this->provider->shouldReceive('retrieveByCredentials') - ->twice() - ->with($credentials) - ->andReturn($user); + ->twice() + ->with($credentials) + ->andReturn($user); $this->provider->shouldReceive('validateCredentials') - ->twice() - ->with($user, $credentials) - ->andReturn(true); + ->twice() + ->with($user, $credentials) + ->andReturn(true); $this->assertTrue($this->guard->attempt($credentials, false)); // once $this->assertTrue($this->guard->validate($credentials)); // twice + $this->events->assertDispatched(JWTAttempt::class, 2); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_return_false_if_credentials_are_invalid() { $credentials = ['foo' => 'bar', 'baz' => 'bob']; - $user = new LaravelUserStub; + $user = new LaravelUserStub(); + + $this->events->shouldReceive('dispatch')->once(); + $this->events->shouldReceive('assertDispatched')->once(); $this->provider->shouldReceive('retrieveByCredentials') - ->once() - ->with($credentials) - ->andReturn($user); + ->once() + ->with($credentials) + ->andReturn($user); $this->provider->shouldReceive('validateCredentials') - ->once() - ->with($user, $credentials) - ->andReturn(false); + ->once() + ->with($user, $credentials) + ->andReturn(false); $this->assertFalse($this->guard->attempt($credentials)); + $this->events->assertDispatched(JWTAttempt::class, 1); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_magically_call_the_jwt_instance() { - $this->jwt->shouldReceive('factory')->andReturn(Mockery::mock(Factory::class)); - $this->assertInstanceOf(Factory::class, $this->guard->factory()); + $this->jwt->shouldReceive('builder')->andReturn(Mockery::mock(Builder::class)); + $this->assertInstanceOf(Builder::class, $this->guard->builder()); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_logout_the_user_by_invalidating_the_token() { $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->once()->andReturn(true); - $this->jwt->shouldReceive('invalidate')->once()->andReturn(true); + $this->jwt->shouldReceive('getToken') + ->once() + ->andReturn(new Token('foo.bar.baz')); $this->jwt->shouldReceive('unsetToken')->once(); + $this->jwt->shouldReceive('invalidate') + ->once() + ->andReturnSelf(); + + $this->events->shouldReceive('dispatch')->once(); + $this->events->shouldReceive('assertDispatched')->once(); $this->guard->logout(); + + $this->events->assertDispatched(JWTLogout::class, 1); $this->assertNull($this->guard->getUser()); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_refresh_the_token() { $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->once()->andReturn(true); - $this->jwt->shouldReceive('refresh')->once()->andReturn('foo.bar.baz'); - - $this->assertSame($this->guard->refresh(), 'foo.bar.baz'); + $this->jwt->shouldReceive('getToken') + ->twice() + ->andReturn(new Token('baz.bar.foo')); + $this->jwt->shouldReceive('refresh') + ->twice() + ->andReturn($token = new Token('foo.bar.baz')); + + $this->events->shouldReceive('dispatch')->times(2); + $this->events->shouldReceive('assertDispatched')->once(); + + $this->assertTrue($token->matches($this->guard->refresh())); // once + $this->assertSame((string) $this->guard->refresh(), 'foo.bar.baz'); // twice + $this->events->assertDispatched(JWTRefresh::class, 2); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_invalidate_the_token() { $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->once()->andReturn(true); - $this->jwt->shouldReceive('invalidate')->once()->andReturn(true); - - $this->assertTrue($this->guard->invalidate()); + $this->jwt->shouldReceive('getToken') + ->once() + ->andReturn(new Token('foo.bar.baz')); + $this->jwt->shouldReceive('invalidate') + ->once() + ->andReturnSelf(); + + $this->events->shouldReceive('dispatch')->once(); + $this->events->shouldReceive('assertDispatched')->once(); + + $this->guard->invalidate(); + $this->events->assertDispatched(JWTInvalidate::class, 1); } - /** - * @test - * @group laravel-5.2 - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage Token could not be parsed from the request. - */ + /** @test */ public function it_should_throw_an_exception_if_there_is_no_token_present_when_required() { + $this->expectException(JWTException::class); + $this->expectExceptionMessage('Token could not be parsed from the request.'); + $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->once()->andReturn(false); + $this->jwt->shouldReceive('getToken') + ->once() + ->andReturn(null); $this->jwt->shouldReceive('refresh')->never(); $this->guard->refresh(); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_generate_a_token_by_id() { - $user = new LaravelUserStub; + $user = new LaravelUserStub(); $this->provider->shouldReceive('retrieveById') - ->once() - ->with(1) - ->andReturn($user); + ->once() + ->with(1) + ->andReturn($user); $this->jwt->shouldReceive('fromUser') - ->once() - ->with($user) - ->andReturn('foo.bar.baz'); + ->once() + ->with($user) + ->andReturn($token = new Token('foo.bar.baz')); - $this->assertSame('foo.bar.baz', $this->guard->tokenById(1)); + $this->assertSame($token, $this->guard->tokenById(1)); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_not_generate_a_token_by_id() { $this->provider->shouldReceive('retrieveById') - ->once() - ->with(1) - ->andReturn(null); + ->once() + ->with(1) + ->andReturn(null); $this->assertNull($this->guard->tokenById(1)); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_authenticate_the_user_by_credentials_and_return_true_if_valid() { $credentials = ['foo' => 'bar', 'baz' => 'bob']; - $user = new LaravelUserStub; + $user = new LaravelUserStub(); $this->provider->shouldReceive('retrieveByCredentials') - ->once() - ->with($credentials) - ->andReturn($user); + ->once() + ->with($credentials) + ->andReturn($user); $this->provider->shouldReceive('validateCredentials') - ->once() - ->with($user, $credentials) - ->andReturn(true); + ->once() + ->with($user, $credentials) + ->andReturn(true); + + $this->events->shouldReceive('dispatch')->once(); + $this->events->shouldReceive('assertDispatched')->once(); $this->assertTrue($this->guard->once($credentials)); + $this->events->assertDispatched(JWTAttempt::class, 1); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_attempt_to_authenticate_the_user_by_credentials_and_return_false_if_invalid() { $credentials = ['foo' => 'bar', 'baz' => 'bob']; - $user = new LaravelUserStub; + $user = new LaravelUserStub(); $this->provider->shouldReceive('retrieveByCredentials') - ->once() - ->with($credentials) - ->andReturn($user); + ->once() + ->with($credentials) + ->andReturn($user); $this->provider->shouldReceive('validateCredentials') - ->once() - ->with($user, $credentials) - ->andReturn(false); + ->once() + ->with($user, $credentials) + ->andReturn(false); + + $this->events->shouldReceive('dispatch')->once(); + $this->events->shouldReceive('assertDispatched')->once(); $this->assertFalse($this->guard->once($credentials)); + $this->events->assertDispatched(JWTAttempt::class, 1); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_authenticate_the_user_by_id_and_return_boolean() { - $user = new LaravelUserStub; + $user = new LaravelUserStub(); $this->provider->shouldReceive('retrieveById') - ->twice() - ->with(1) - ->andReturn($user); + ->twice() + ->with(1) + ->andReturn($user); $this->assertTrue($this->guard->onceUsingId(1)); // once $this->assertTrue($this->guard->byId(1)); // twice } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_not_authenticate_the_user_by_id_and_return_false() { $this->provider->shouldReceive('retrieveById') - ->twice() - ->with(1) - ->andReturn(null); + ->twice() + ->with(1) + ->andReturn(null); $this->assertFalse($this->guard->onceUsingId(1)); // once $this->assertFalse($this->guard->byId(1)); // twice } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_create_a_token_from_a_user_object() { - $user = new LaravelUserStub; + $user = new LaravelUserStub(); $this->jwt->shouldReceive('fromUser') - ->once() - ->with($user) - ->andReturn('foo.bar.baz'); + ->once() + ->with($user) + ->andReturn($token = new Token('foo.bar.baz')); $this->jwt->shouldReceive('setToken') - ->once() - ->with('foo.bar.baz') - ->andReturnSelf(); + ->once() + ->with($token) + ->andReturnSelf(); - $token = $this->guard->login($user); + $this->events->shouldReceive('dispatch')->once(); + $this->events->shouldReceive('assertDispatched')->once(); - $this->assertSame('foo.bar.baz', $token); + $jwt = $this->guard->login($user); + + $this->events->assertDispatched(JWTLogin::class, 1); + $this->assertTrue($jwt->matches($token)); + $this->assertSame('foo.bar.baz', (string) $jwt); } - /** - * @test - * @group laravel-5.2 - */ + /** @test */ public function it_should_get_the_payload() { - $this->jwt->shouldReceive('setRequest')->andReturn($this->jwt); - $this->jwt->shouldReceive('getToken')->once()->andReturn('foo.bar.baz'); - $this->jwt->shouldReceive('getPayload')->once()->andReturn(Mockery::mock(Payload::class)); + $this->jwt->shouldReceive('setRequest')->andReturnSelf(); + $this->jwt->shouldReceive('getToken') + ->once() + ->andReturn(new Token('foo.bar.baz')); + $this->jwt->shouldReceive('payload') + ->once() + ->andReturn(Mockery::mock(Payload::class)); + $this->assertInstanceOf(Payload::class, $this->guard->payload()); } + + /** @test */ + public function it_should_be_macroable() + { + $this->guard->macro('foo', fn () => 'bar'); + + $this->assertEquals('bar', $this->guard->foo()); + } } diff --git a/tests/JWTTest.php b/tests/JWTTest.php new file mode 100644 index 000000000..247be2be4 --- /dev/null +++ b/tests/JWTTest.php @@ -0,0 +1,283 @@ + + * + * 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\Http\Request; +use Mockery; +use Tymon\JWTAuth\Blacklist; +use Tymon\JWTAuth\Builder; +use Tymon\JWTAuth\Claims\HashedSubject; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; +use Tymon\JWTAuth\Http\Parser\Parser; +use Tymon\JWTAuth\JWT; +use Tymon\JWTAuth\Manager; +use Tymon\JWTAuth\Payload; +use Tymon\JWTAuth\Test\Stubs\UserStub; +use Tymon\JWTAuth\Token; + +class JWTTest extends AbstractTestCase +{ + /** + * @var \Mockery\MockInterface|\Tymon\JWTAuth\Builder + */ + protected $builder; + + /** + * @var \Mockery\MockInterface|\Tymon\JWTAuth\Manager + */ + protected $manager; + + /** + * @var \Mockery\MockInterface|\Tymon\JWTAuth\Http\Parser\Parser + */ + protected $parser; + + /** + * @var \Tymon\JWTAuth\JWT + */ + protected $jwt; + + public function setUp(): void + { + $this->builder = Mockery::spy(Builder::class); + $this->manager = Mockery::spy(Manager::class); + $this->parser = Mockery::spy(Parser::class); + $this->jwt = new JWT($this->builder, $this->manager, $this->parser); + } + + /** @test */ + public function it_should_return_a_token_when_passing_a_user() + { + $this->manager->shouldReceive('tokenForSubject') + ->once() + ->with($user = new UserStub, ['foo' => 'bar']) + ->andReturn($token = new Token('foo.bar.baz')); + + $jwt = $this->jwt->claims(['foo' => 'bar'])->fromUser($user); + + $this->assertSame($jwt, $token); + $this->assertSame((string) $jwt, 'foo.bar.baz'); + } + + /** @test */ + public function it_should_pass_hash_check_if_hash_matches() + { + $hash = sha1(UserStub::class); + + $payload = Mockery::mock(Payload::class)->shouldReceive('offsetExists') + ->with(HashedSubject::NAME) + ->andReturn(true) + ->getMock(); + + $payload->shouldReceive('get') + ->with(HashedSubject::NAME) + ->andReturn($hash); + + $this->builder->shouldReceive('hashSubjectModel') + ->once() + ->with(UserStub::class) + ->andReturn($hash); + + $this->assertTrue( + $this->jwt->setToken('foo.bar.baz')->checkSubjectModel(UserStub::class, $payload) + ); + } + + /** @test */ + public function it_should_pass_provider_check_if_hash_matches_when_hashed_subject_is_null() + { + $payload = Mockery::mock(Payload::class)->shouldReceive('get') + ->with(HashedSubject::NAME) + ->andReturn(null) + ->getMock(); + + $this->assertTrue( + $this->jwt->setToken('foo.bar.baz') + ->checkSubjectModel('Tymon\JWTAuth\Test\Stubs\UserStub', $payload) + ); + } + + /** @test */ + public function it_should_not_pass_provider_check_if_hash_not_match() + { + $payload = Mockery::mock(Payload::class)->shouldReceive('get') + ->with(HashedSubject::NAME) + ->andReturn(sha1('Tymon\JWTAuth\Test\Stubs\UserStub1')) + ->getMock(); + + $this->assertFalse( + $this->jwt->setToken('foo.bar.baz') + ->checkSubjectModel('Tymon\JWTAuth\Test\Stubs\UserStub', $payload) + ); + } + + /** @test */ + public function it_should_refresh_a_token() + { + $this->manager->shouldReceive('refresh', 60) + ->once() + ->andReturn($token = new Token('baz.bar.foo')); + + $result = $this->jwt->setToken('foo.bar.baz')->refresh(); + + $this->assertSame($result, $token); + $this->assertSame((string) $result, 'baz.bar.foo'); + } + + /** @test */ + public function it_should_invalidate_a_token() + { + $token = new Token('foo.bar.baz'); + + $this->manager->shouldReceive('invalidate') + ->once() + ->with($token)->andReturn(true); + + $this->jwt->setToken($token)->invalidate(); + } + + /** @test */ + public function it_should_retrieve_the_token_from_the_request() + { + $this->parser->shouldReceive('parseToken')->andReturn('foo.bar.baz'); + + $this->assertInstanceOf(Token::class, $this->jwt->parseToken()->getToken()); + $this->assertEquals($this->jwt->getToken(), 'foo.bar.baz'); + } + + /** @test */ + public function it_should_get_the_authenticated_user() + { + $manager = $this->jwt->manager(); + $this->assertInstanceOf(Manager::class, $manager); + } + + /** @test */ + public function it_should_return_false_if_the_token_is_invalid() + { + $this->parser->shouldReceive('parseToken')->andReturn('foo.bar.baz'); + $this->manager->shouldReceive('decode') + ->once() + ->andThrow(new TokenInvalidException); + + $this->assertFalse($this->jwt->parseToken()->check()); + } + + /** @test */ + public function it_should_return_true_if_the_token_is_valid() + { + $payload = Mockery::mock(Payload::class); + + $this->parser->shouldReceive('parseToken')->andReturn('foo.bar.baz'); + $this->manager->shouldReceive('decode') + ->once() + ->andReturn($payload); + + $this->assertTrue($this->jwt->parseToken()->check()); + } + + /** @test */ + public function it_should_throw_an_exception_when_token_not_present_in_request() + { + $this->expectException(JWTException::class); + $this->expectExceptionMessage('The token could not be parsed from the request'); + + $this->parser->shouldReceive('parseToken')->andReturn(false); + + $this->jwt->parseToken(); + } + + /** @test */ + public function it_should_return_false_when_no_token_is_set() + { + $this->parser->shouldReceive('parseToken')->andReturn(false); + + $this->assertNull($this->jwt->getToken()); + } + + /** @test */ + public function it_should_magically_call_the_manager() + { + $this->manager->shouldReceive('getBlacklist')->andReturn(Mockery::mock(Blacklist::class)); + + $blacklist = $this->jwt->manager()->getBlacklist(); + + $this->assertInstanceOf(Blacklist::class, $blacklist); + } + + /** @test */ + public function it_should_set_the_request() + { + $request = Request::create('/foo', 'GET', ['token' => 'some.random.token']); + + $this->parser->shouldReceive('setRequest') + ->once() + ->with($request); + $this->parser->shouldReceive('parseToken')->andReturn('some.random.token'); + + $token = $this->jwt->setRequest($request)->getToken(); + + $this->assertEquals('some.random.token', $token); + } + + /** @test */ + public function it_should_unset_the_token() + { + $this->parser->shouldReceive('parseToken')->andThrow(new JWTException); + $token = new Token('foo.bar.baz'); + $this->jwt->setToken($token); + + $this->assertSame($this->jwt->getToken(), $token); + $this->jwt->unsetToken(); + $this->assertNull($this->jwt->getToken()); + } + + /** @test */ + public function it_should_register_a_custom_claim_validator() + { + $this->builder->shouldReceive('setCustomValidator') + ->with('foo', Mockery::type('callable')) + ->once(); + + $this->jwt->registerCustomValidator('foo', fn ($value) => $value !== 'bar'); + } + + /** @test */ + public function it_should_get_the_manager_instance() + { + $this->assertInstanceOf(Manager::class, $this->jwt->manager()); + } + + /** @test */ + public function it_should_get_the_parser_instance() + { + $this->assertInstanceOf(Parser::class, $this->jwt->parser()); + } + + /** @test */ + public function it_should_get_a_claim_value() + { + $payload = Mockery::mock(Payload::class); + $payload->shouldReceive('get') + ->once() + ->with(Subject::NAME) + ->andReturn(1); + + $this->manager->shouldReceive('decode') + ->once() + ->andReturn($payload); + + $this->assertSame($this->jwt->setToken('foo.bar.baz')->getClaim(Subject::NAME), 1); + } +} diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php index ba1ce3dcd..3d95be32d 100644 --- a/tests/ManagerTest.php +++ b/tests/ManagerTest.php @@ -12,20 +12,22 @@ namespace Tymon\JWTAuth\Test; use Mockery; -use Tymon\JWTAuth\Token; -use Tymon\JWTAuth\Factory; -use Tymon\JWTAuth\Manager; -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\Builder; +use Tymon\JWTAuth\Claims\Expiration; use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; use Tymon\JWTAuth\Claims\NotBefore; -use Tymon\JWTAuth\Claims\Collection; -use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Claims\Subject; use Tymon\JWTAuth\Contracts\Providers\JWT; -use Tymon\JWTAuth\Validators\PayloadValidator; +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\TokenBlacklistedException; +use Tymon\JWTAuth\Factory; +use Tymon\JWTAuth\Manager; +use Tymon\JWTAuth\Options; +use Tymon\JWTAuth\Payload; +use Tymon\JWTAuth\Token; class ManagerTest extends AbstractTestCase { @@ -45,44 +47,40 @@ class ManagerTest extends AbstractTestCase protected $factory; /** - * @var \Tymon\JWTAuth\Manager + * @var \Mockery\MockInterface|\Tymon\JWTAuth\Builder */ - protected $manager; + protected $builder; /** - * @var \Mockery\MockInterface + * @var \Tymon\JWTAuth\Manager */ - protected $validator; + protected $manager; - public function setUp() + public function setUp(): void { parent::setUp(); $this->jwt = Mockery::mock(JWT::class); $this->blacklist = Mockery::mock(Blacklist::class); - $this->factory = Mockery::mock(Factory::class); - $this->manager = new Manager($this->jwt, $this->blacklist, $this->factory); - $this->validator = Mockery::mock(PayloadValidator::class); + $this->builder = Mockery::mock(Builder::class); + $this->manager = new Manager($this->jwt, $this->blacklist, $this->builder); } /** @test */ public function it_should_encode_a_payload() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - $payload = new Payload($collection, $this->validator); - - $this->jwt->shouldReceive('encode')->with($payload->toArray())->andReturn('foo.bar.baz'); + $this->jwt->shouldReceive('token') + ->with($payload) + ->andReturn(new Token('foo.bar.baz')); $token = $this->manager->encode($payload); @@ -92,28 +90,30 @@ public function it_should_encode_a_payload() /** @test */ public function it_should_decode_a_token() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - $collection = Collection::make($claims); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - $payload = new Payload($collection, $this->validator); + ]); $token = new Token('foo.bar.baz'); + $options = new Options(); - $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); + $this->jwt->shouldReceive('payload') + ->once() + ->with($token, $options) + ->andReturn($payload); - $this->factory->shouldReceive('setRefreshFlow')->andReturn($this->factory); - $this->factory->shouldReceive('customClaims')->andReturn($this->factory); - $this->factory->shouldReceive('make')->andReturn($payload); + $this->blacklist->shouldReceive('has') + ->with($payload) + ->andReturn(false); - $this->blacklist->shouldReceive('has')->with($payload)->andReturn(false); + $this->builder->shouldReceive('getOptions') + ->once() + ->andReturn($options); $payload = $this->manager->decode($token); @@ -121,34 +121,36 @@ public function it_should_decode_a_token() $this->assertSame($payload->count(), 6); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenBlacklistedException - * @expectedExceptionMessage The token has been blacklisted - */ + /** @test */ public function it_should_throw_exception_when_token_is_blacklisted() { - $claims = [ + $this->expectException(TokenBlacklistedException::class); + $this->expectExceptionMessage('The token has been blacklisted'); + + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - $payload = new Payload($collection, $this->validator); $token = new Token('foo.bar.baz'); + $options = new Options(); - $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); + $this->jwt->shouldReceive('payload') + ->once() + ->with($token, $options) + ->andReturn($payload); - $this->factory->shouldReceive('setRefreshFlow')->andReturn($this->factory); - $this->factory->shouldReceive('customClaims')->with($payload->toArray())->andReturn($this->factory); - $this->factory->shouldReceive('make')->andReturn($payload); + $this->blacklist->shouldReceive('has') + ->with($payload) + ->andReturn(true); - $this->blacklist->shouldReceive('has')->with($payload)->andReturn(true); + $this->builder->shouldReceive('getOptions') + ->once() + ->andReturn($options); $this->manager->decode($token); } @@ -156,33 +158,50 @@ public function it_should_throw_exception_when_token_is_blacklisted() /** @test */ public function it_should_refresh_a_token() { - $claims = [ + $payload = Factory::make([ new Subject(1), - new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp - 3600), - new NotBefore($this->testNowTimestamp), + new Issuer('example.com'), + new Expiration($this->testNowTimestamp + 3600), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - $payload = new Payload($collection, $this->validator); $token = new Token('foo.bar.baz'); - - $this->jwt->shouldReceive('decode')->twice()->with('foo.bar.baz')->andReturn($payload->toArray()); - $this->jwt->shouldReceive('encode')->with($payload->toArray())->andReturn('baz.bar.foo'); - - $this->factory->shouldReceive('setRefreshFlow')->with(true)->andReturn($this->factory); - $this->factory->shouldReceive('customClaims')->andReturn($this->factory); - $this->factory->shouldReceive('make')->andReturn($payload); - - $this->blacklist->shouldReceive('has')->with($payload)->andReturn(false); - $this->blacklist->shouldReceive('add')->once()->with($payload); + $options = new Options(); + + $this->jwt->shouldReceive('payload') + ->twice() + ->with($token, $options) + ->andReturn($payload); + + $this->jwt->shouldReceive('token') + ->once() + ->with(Mockery::type(Payload::class)) + ->andReturn(new Token('baz.bar.foo')); + + $this->blacklist->shouldReceive('has') + ->with($payload) + ->andReturn(false); + $this->blacklist->shouldReceive('add') + ->once() + ->with($payload); + + $this->builder->shouldReceive('getOptions') + ->twice() + ->andReturn($options); + + $this->builder->shouldReceive('buildRefreshClaims') + ->once() + ->with($payload) + ->andReturn($claims = $payload->toArray()); + + $this->builder->shouldReceive('make') + ->once() + ->with($claims) + ->andReturn($payload); $token = $this->manager->refresh($token); - // $this->assertArrayHasKey('ref', $payload); $this->assertInstanceOf(Token::class, $token); $this->assertEquals('baz.bar.foo', $token); } @@ -190,81 +209,49 @@ public function it_should_refresh_a_token() /** @test */ public function it_should_invalidate_a_token() { - $claims = [ + $payload = Factory::make([ new Subject(1), new Issuer('http://example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), - ]; - $collection = Collection::make($claims); + ]); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - $payload = new Payload($collection, $this->validator); $token = new Token('foo.bar.baz'); + $options = new Options(); - $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); + $this->jwt->shouldReceive('payload') + ->once() + ->with($token, $options) + ->andReturn($payload); - $this->factory->shouldReceive('setRefreshFlow')->andReturn($this->factory); - $this->factory->shouldReceive('customClaims')->with($payload->toArray())->andReturn($this->factory); - $this->factory->shouldReceive('make')->andReturn($payload); + $this->blacklist->shouldReceive('has') + ->with($payload) + ->andReturn(false); - $this->blacklist->shouldReceive('has')->with($payload)->andReturn(false); + $this->blacklist->shouldReceive('add') + ->with($payload) + ->andReturn(true); - $this->blacklist->shouldReceive('add')->with($payload)->andReturn(true); + $this->builder->shouldReceive('getOptions') + ->once() + ->andReturn($options); $this->manager->invalidate($token); } /** @test */ - public function it_should_force_invalidate_a_token_forever() - { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp + 3600), - new NotBefore($this->testNowTimestamp), - new IssuedAt($this->testNowTimestamp), - new JwtId('foo'), - ]; - $collection = Collection::make($claims); - - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - $payload = new Payload($collection, $this->validator); - $token = new Token('foo.bar.baz'); - - $this->jwt->shouldReceive('decode')->once()->with('foo.bar.baz')->andReturn($payload->toArray()); - - $this->factory->shouldReceive('setRefreshFlow')->andReturn($this->factory); - $this->factory->shouldReceive('customClaims')->with($payload->toArray())->andReturn($this->factory); - $this->factory->shouldReceive('make')->andReturn($payload); - - $this->blacklist->shouldReceive('has')->with($payload)->andReturn(false); - - $this->blacklist->shouldReceive('addForever')->with($payload)->andReturn(true); - - $this->manager->invalidate($token, true); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage You must have the blacklist enabled to invalidate a token. - */ public function it_should_throw_an_exception_when_enable_blacklist_is_set_to_false() { + $this->expectException(JWTException::class); + $this->expectExceptionMessage('You must have the blacklist enabled to invalidate a token.'); + $token = new Token('foo.bar.baz'); $this->manager->setBlacklistEnabled(false)->invalidate($token); } - /** @test */ - public function it_should_get_the_payload_factory() - { - $this->assertInstanceOf(Factory::class, $this->manager->getPayloadFactory()); - } - /** @test */ public function it_should_get_the_jwt_provider() { @@ -276,4 +263,10 @@ public function it_should_get_the_blacklist() { $this->assertInstanceOf(Blacklist::class, $this->manager->getBlacklist()); } + + /** @test */ + public function it_should_get_the_builder() + { + $this->assertInstanceOf(Builder::class, $this->manager->builder()); + } } diff --git a/tests/Middleware/AbstractMiddlewareTest.php b/tests/Middleware/AbstractMiddlewareTest.php deleted file mode 100644 index 3b9f290cc..000000000 --- a/tests/Middleware/AbstractMiddlewareTest.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Test\Middleware; - -use Mockery; -use Tymon\JWTAuth\JWTAuth; -use Illuminate\Http\Request; -use Tymon\JWTAuth\Test\AbstractTestCase; - -abstract class AbstractMiddlewareTest extends AbstractTestCase -{ - /** - * @var \Mockery\MockInterface|\Tymon\JWTAuth\JWTAuth - */ - protected $auth; - - /** - * @var \Mockery\MockInterface|\Illuminate\Http\Request - */ - protected $request; - - public function setUp() - { - parent::setUp(); - - $this->auth = Mockery::mock(JWTAuth::class); - $this->request = Mockery::mock(Request::class); - } -} diff --git a/tests/Middleware/AuthenticateAndRenewTest.php b/tests/Middleware/AuthenticateAndRenewTest.php deleted file mode 100644 index 5a17a4d78..000000000 --- a/tests/Middleware/AuthenticateAndRenewTest.php +++ /dev/null @@ -1,89 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Test\Middleware; - -use Mockery; -use Illuminate\Http\Response; -use Tymon\JWTAuth\Http\Parser\Parser; -use Tymon\JWTAuth\Test\Stubs\UserStub; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; -use Tymon\JWTAuth\Http\Middleware\AuthenticateAndRenew; - -class AuthenticateAndRenewTest extends AbstractMiddlewareTest -{ - /** - * @var \Tymon\JWTAuth\Http\Middleware\Authenticate|\Tymon\JWTAuth\Http\Middleware\AuthenticateAndRenew - */ - protected $middleware; - - public function setUp() - { - parent::setUp(); - - $this->middleware = new AuthenticateAndRenew($this->auth); - } - - /** @test */ - public function it_should_authenticate_a_user_and_return_a_new_token() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - $this->auth->shouldReceive('parser')->andReturn($parser); - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - - $this->auth->shouldReceive('parseToken->authenticate')->once()->andReturn(new UserStub); - - $this->auth->shouldReceive('refresh')->once()->andReturn('foo.bar.baz'); - - $response = $this->middleware->handle($this->request, function () { - return new Response; - }); - - $this->assertSame($response->headers->get('authorization'), 'Bearer foo.bar.baz'); - } - - /** - * @test - * @expectedException \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public function it_should_throw_an_unauthorized_exception_if_token_not_provided() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(false); - - $this->auth->shouldReceive('parser')->andReturn($parser); - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - - $this->middleware->handle($this->request, function () { - // - }); - } - - /** - * @test - * @expectedException \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public function it_should_throw_an_unauthorized_exception_if_token_invalid() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->authenticate')->once()->andThrow(new TokenInvalidException); - - $this->middleware->handle($this->request, function () { - // - }); - } -} diff --git a/tests/Middleware/AuthenticateTest.php b/tests/Middleware/AuthenticateTest.php deleted file mode 100644 index 30b3cc27b..000000000 --- a/tests/Middleware/AuthenticateTest.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Test\Middleware; - -use Mockery; -use Tymon\JWTAuth\Http\Parser\Parser; -use Tymon\JWTAuth\Test\Stubs\UserStub; -use Tymon\JWTAuth\Http\Middleware\Authenticate; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; - -class AuthenticateTest extends AbstractMiddlewareTest -{ - /** - * @var \Tymon\JWTAuth\Http\Middleware\Authenticate - */ - protected $middleware; - - public function setUp() - { - parent::setUp(); - - $this->middleware = new Authenticate($this->auth); - } - - /** @test */ - public function it_should_authenticate_a_user() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->authenticate')->once()->andReturn(new UserStub); - - $this->middleware->handle($this->request, function () { - // - }); - } - - /** - * @test - * @expectedException \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public function it_should_throw_an_unauthorized_exception_if_token_not_provided() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(false); - - $this->auth->shouldReceive('parser')->andReturn($parser); - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - - $this->middleware->handle($this->request, function () { - // - }); - } - - /** - * @test - * @expectedException \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public function it_should_throw_an_unauthorized_exception_if_token_invalid() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->authenticate')->once()->andThrow(new TokenInvalidException); - - $this->middleware->handle($this->request, function () { - // - }); - } - - /** - * @test - * @expectedException \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public function it_should_throw_an_unauthorized_exception_if_user_not_found() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->authenticate')->once()->andReturn(false); - - $this->middleware->handle($this->request, function () { - // - }); - } -} diff --git a/tests/Middleware/CheckTest.php b/tests/Middleware/CheckTest.php deleted file mode 100644 index 232beb8f5..000000000 --- a/tests/Middleware/CheckTest.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Test\Middleware; - -use Mockery; -use Tymon\JWTAuth\Http\Parser\Parser; -use Tymon\JWTAuth\Test\Stubs\UserStub; -use Tymon\JWTAuth\Http\Middleware\Check; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; - -class CheckTest extends AbstractMiddlewareTest -{ - /** - * @var \Tymon\JWTAuth\Http\Middleware\Check - */ - protected $middleware; - - public function setUp() - { - parent::setUp(); - - $this->middleware = new Check($this->auth); - } - - /** @test */ - public function it_should_authenticate_a_user_if_a_token_is_present() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->authenticate')->once()->andReturn(new UserStub); - - $this->middleware->handle($this->request, function () { - // - }); - } - - /** @test */ - public function it_should_unset_the_exception_if_a_token_is_present() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->authenticate')->once()->andThrow(new TokenInvalidException); - - $this->middleware->handle($this->request, function () { - // - }); - } - - /** @test */ - public function it_should_do_nothing_if_a_token_is_not_present() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(false); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->authenticate')->never(); - - $this->middleware->handle($this->request, function () { - // - }); - } -} diff --git a/tests/Middleware/RefreshTokenTest.php b/tests/Middleware/RefreshTokenTest.php deleted file mode 100644 index d5e46a159..000000000 --- a/tests/Middleware/RefreshTokenTest.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tymon\JWTAuth\Test\Middleware; - -use Mockery; -use Illuminate\Http\Response; -use Tymon\JWTAuth\Http\Parser\Parser; -use Tymon\JWTAuth\Http\Middleware\RefreshToken; -use Tymon\JWTAuth\Exceptions\TokenInvalidException; - -class RefreshTokenTest extends AbstractMiddlewareTest -{ - /** - * @var \Tymon\JWTAuth\Http\Middleware\RefreshToken - */ - protected $middleware; - - public function setUp() - { - parent::setUp(); - - $this->middleware = new RefreshToken($this->auth); - } - - /** @test */ - public function it_should_refresh_a_token() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->refresh')->once()->andReturn('foo.bar.baz'); - - $response = $this->middleware->handle($this->request, function () { - return new Response; - }); - - $this->assertSame($response->headers->get('authorization'), 'Bearer foo.bar.baz'); - } - - /** - * @test - * @expectedException \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public function it_should_throw_an_unauthorized_exception_if_token_not_provided() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(false); - - $this->auth->shouldReceive('parser')->andReturn($parser); - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - - $this->middleware->handle($this->request, function () { - // - }); - } - - /** - * @test - * @expectedException \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public function it_should_throw_an_unauthorized_exception_if_token_invalid() - { - $parser = Mockery::mock(Parser::class); - $parser->shouldReceive('hasToken')->once()->andReturn(true); - - $this->auth->shouldReceive('parser')->andReturn($parser); - - $this->auth->parser()->shouldReceive('setRequest')->once()->with($this->request)->andReturn($this->auth->parser()); - $this->auth->shouldReceive('parseToken->refresh')->once()->andThrow(new TokenInvalidException); - - $this->middleware->handle($this->request, function () { - // - }); - } -} diff --git a/tests/PayloadTest.php b/tests/PayloadTest.php index ac8b13cfe..9da1c9ed5 100644 --- a/tests/PayloadTest.php +++ b/tests/PayloadTest.php @@ -11,18 +11,18 @@ namespace Tymon\JWTAuth\Test; -use Mockery; -use Tymon\JWTAuth\Payload; -use Tymon\JWTAuth\Claims\Claim; -use Tymon\JWTAuth\Claims\JwtId; -use Tymon\JWTAuth\Claims\Issuer; -use Tymon\JWTAuth\Claims\Subject; +use BadMethodCallException; use Tymon\JWTAuth\Claims\Audience; -use Tymon\JWTAuth\Claims\IssuedAt; -use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Claim; use Tymon\JWTAuth\Claims\Collection; use Tymon\JWTAuth\Claims\Expiration; -use Tymon\JWTAuth\Validators\PayloadValidator; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Exceptions\PayloadException; +use Tymon\JWTAuth\Payload; class PayloadTest extends AbstractTestCase { @@ -36,7 +36,7 @@ class PayloadTest extends AbstractTestCase */ protected $payload; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -57,77 +57,69 @@ private function getTestPayload(array $extraClaims = []) new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), new JwtId('foo'), + ...$extraClaims, ]; - if ($extraClaims) { - $claims = array_merge($claims, $extraClaims); - } - $collection = Collection::make($claims); - $this->validator = Mockery::mock(PayloadValidator::class); - $this->validator->shouldReceive('setRefreshFlow->check')->andReturn($collection); - - return new Payload($collection, $this->validator); + return new Payload($collection); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\PayloadException - * @expectedExceptionMessage The payload is immutable - */ + /** @test */ public function it_should_throw_an_exception_when_trying_to_add_to_the_payload() { + $this->expectException(PayloadException::class); + $this->expectExceptionMessage('The payload is immutable'); + $this->payload['foo'] = 'bar'; } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\PayloadException - * @expectedExceptionMessage The payload is immutable - */ + /** @test */ public function it_should_throw_an_exception_when_trying_to_remove_a_key_from_the_payload() { + $this->expectException(PayloadException::class); + $this->expectExceptionMessage('The payload is immutable'); + unset($this->payload['foo']); } /** @test */ public function it_should_cast_the_payload_to_a_string_as_json() { - $this->assertSame((string) $this->payload, json_encode($this->payload->get(), JSON_UNESCAPED_SLASHES)); - $this->assertJsonStringEqualsJsonString((string) $this->payload, json_encode($this->payload->get())); + $this->assertSame( + (string) $this->payload, + json_encode($this->payload->get(), JSON_UNESCAPED_SLASHES) + ); + $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->assertSame($this->payload['sub'], 1); - $this->assertArrayHasKey('exp', $this->payload); + $this->assertTrue(isset($this->payload[IssuedAt::NAME])); + $this->assertSame($this->payload[Subject::NAME], 1); + $this->assertArrayHasKey(Expiration::NAME, $this->payload); } /** @test */ public function it_should_get_properties_of_payload_via_get_method() { - $this->assertInternalType('array', $this->payload->get()); - $this->assertSame($this->payload->get('sub'), 1); - - $this->assertSame( - $this->payload->get(function () { - return 'jti'; - }), - 'foo' - ); + $this->assertIsArray($this->payload->get()); + $this->assertSame($this->payload->get(Subject::NAME), 1); + $this->assertSame($this->payload->get(JwtId::NAME), 'foo'); } /** @test */ public function it_should_get_multiple_properties_when_passing_an_array_to_the_get_method() { - $values = $this->payload->get(['sub', 'jti']); + $values = $this->payload->get([Subject::NAME, JwtId::NAME]); - list($sub, $jti) = $values; + [$sub, $jti] = $values; - $this->assertInternalType('array', $values); + $this->assertIsArray($values); $this->assertSame($sub, 1); $this->assertSame($jti, 'foo'); } @@ -156,9 +148,9 @@ public function it_should_invoke_the_instance_as_a_callable() { $payload = $this->payload; - $sub = $payload('sub'); - $jti = $payload('jti'); - $iss = $payload('iss'); + $sub = $payload(Subject::NAME); + $jti = $payload(JwtId::NAME); + $iss = $payload(Issuer::NAME); $this->assertSame($sub, 1); $this->assertSame($jti, 'foo'); @@ -167,13 +159,12 @@ public function it_should_invoke_the_instance_as_a_callable() $this->assertSame($payload(), $this->payload->toArray()); } - /** - * @test - * @expectedException \BadMethodCallException - * @expectedExceptionMessage The claim [getFoo] does not exist on the payload. - */ + /** @test */ public function it_should_throw_an_exception_when_magically_getting_a_property_that_does_not_exist() { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('The claim [Foo] does not exist on the payload.'); + $this->payload->getFoo(); } @@ -182,9 +173,9 @@ public function it_should_get_the_claims() { $claims = $this->payload->getClaims(); - $this->assertInstanceOf(Expiration::class, $claims['exp']); - $this->assertInstanceOf(JwtId::class, $claims['jti']); - $this->assertInstanceOf(Subject::class, $claims['sub']); + $this->assertInstanceOf(Expiration::class, $claims[Expiration::NAME]); + $this->assertInstanceOf(JwtId::class, $claims[JwtId::NAME]); + $this->assertInstanceOf(Subject::class, $claims[Subject::NAME]); $this->assertContainsOnlyInstancesOf(Claim::class, $claims); } @@ -192,7 +183,10 @@ public function it_should_get_the_claims() /** @test */ public function it_should_get_the_object_as_json() { - $this->assertJsonStringEqualsJsonString(json_encode($this->payload), $this->payload->toJson()); + $this->assertJsonStringEqualsJsonString( + json_encode($this->payload), + $this->payload->toJson() + ); } /** @test */ @@ -206,7 +200,7 @@ public function it_should_count_the_claims() public function it_should_match_values() { $values = $this->payload->toArray(); - $values['sub'] = (string) $values['sub']; + $values[Subject::NAME] = (string) $values[Subject::NAME]; $this->assertTrue($this->payload->matches($values)); } @@ -230,7 +224,7 @@ public function it_should_not_match_empty_values() public function it_should_not_match_values() { $values = $this->payload->toArray(); - $values['sub'] = 'dummy_subject'; + $values[Subject::NAME] = 'dummy_subject'; $this->assertFalse($this->payload->matches($values)); } @@ -239,7 +233,7 @@ public function it_should_not_match_values() public function it_should_not_match_strict_values() { $values = $this->payload->toArray(); - $values['sub'] = (string) $values['sub']; + $values[Subject::NAME] = (string) $values[Subject::NAME]; $this->assertFalse($this->payload->matchesStrict($values)); $this->assertFalse($this->payload->matches($values, true)); diff --git a/tests/Providers/Auth/IlluminateTest.php b/tests/Providers/Auth/IlluminateTest.php deleted file mode 100644 index 1ffad8a8b..000000000 --- a/tests/Providers/Auth/IlluminateTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * 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 Illuminate\Contracts\Auth\Guard; -use Tymon\JWTAuth\Test\AbstractTestCase; -use Tymon\JWTAuth\Providers\Auth\Illuminate as Auth; - -class IlluminateTest extends AbstractTestCase -{ - /** - * @var \Mockery\MockInterface|\Illuminate\Contracts\Auth\Guard - */ - protected $authManager; - - /** - * @var \Tymon\JWTAuth\Providers\Auth\Illuminate - */ - protected $auth; - - public function setUp() - { - parent::setUp(); - - $this->authManager = Mockery::mock(Guard::class); - $this->auth = new Auth($this->authManager); - } - - /** @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_return_the_currently_authenticated_user() - { - $this->authManager->shouldReceive('user')->once()->andReturn((object) ['id' => 1]); - $this->assertSame($this->auth->user()->id, 1); - } -} diff --git a/tests/Providers/Auth/SentinelTest.php b/tests/Providers/Auth/SentinelTest.php deleted file mode 100644 index 9b2702a39..000000000 --- a/tests/Providers/Auth/SentinelTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * 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 Cartalyst\Sentinel\Sentinel; -use Tymon\JWTAuth\Test\AbstractTestCase; -use Tymon\JWTAuth\Test\Stubs\SentinelStub; -use Tymon\JWTAuth\Providers\Auth\Sentinel as Auth; - -class SentinelTest extends AbstractTestCase -{ - /** - * @var \Mockery\MockInterface|\Cartalyst\Sentinel\Sentinel - */ - protected $sentinel; - - /** - * @var \Tymon\JWTAuth\Providers\Auth\Sentinel - */ - protected $auth; - - public function setUp() - { - parent::setUp(); - - $this->sentinel = Mockery::mock(Sentinel::class); - $this->auth = new Auth($this->sentinel); - } - - /** @test */ - public function it_should_return_true_if_credentials_are_valid() - { - $this->sentinel->shouldReceive('stateless')->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() - { - $stub = new SentinelStub; - $this->sentinel->shouldReceive('getUserRepository->findById')->once()->with(123)->andReturn($stub); - $this->sentinel->shouldReceive('setUser')->once()->with($stub); - - $this->assertTrue($this->auth->byId(123)); - } - - /** @test */ - public function it_should_return_false_if_user_is_not_found() - { - $this->sentinel->shouldReceive('getUserRepository->findById')->once()->with(321)->andReturn(false); - $this->sentinel->shouldReceive('setUser')->never(); - - $this->assertFalse($this->auth->byId(321)); - } - - /** @test */ - public function it_should_return_the_currently_authenticated_user() - { - $this->sentinel->shouldReceive('getUser')->once()->andReturn(new SentinelStub); - $this->assertSame($this->auth->user()->getUserId(), 123); - } -} diff --git a/tests/Providers/JWT/LcobucciTest.php b/tests/Providers/JWT/LcobucciTest.php index 2ecb5d1eb..2ba4c5bf2 100644 --- a/tests/Providers/JWT/LcobucciTest.php +++ b/tests/Providers/JWT/LcobucciTest.php @@ -11,14 +11,20 @@ namespace Tymon\JWTAuth\Test\Providers\JWT; -use Mockery; use Exception; -use Lcobucci\JWT\Parser; +use InvalidArgumentException; use Lcobucci\JWT\Builder; +use Lcobucci\JWT\Parser; use Lcobucci\JWT\Signer\Key; -use InvalidArgumentException; -use Tymon\JWTAuth\Test\AbstractTestCase; +use Mockery; +use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Exceptions\JWTException; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; use Tymon\JWTAuth\Providers\JWT\Lcobucci; +use Tymon\JWTAuth\Test\AbstractTestCase; class LcobucciTest extends AbstractTestCase { @@ -33,11 +39,11 @@ class LcobucciTest extends AbstractTestCase protected $builder; /** - * @var \Tymon\JWTAuth\Providers\JWT\Namshi + * @var \Tymon\JWTAuth\Providers\JWT\Lcobucci */ protected $provider; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -48,30 +54,50 @@ public function setUp() /** @test */ public function it_should_return_the_token_when_passing_a_valid_payload_to_encode() { - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->builder->shouldReceive('unsign')->once()->andReturnSelf(); + $payload = [ + Subject::NAME => 1, + Expiration::NAME => $this->testNowTimestamp + 3600, + IssuedAt::NAME => $this->testNowTimestamp, + Issuer::NAME => '/foo', + ]; + + $this->builder->shouldReceive('unsign') + ->once() + ->andReturnSelf(); $this->builder->shouldReceive('set')->times(count($payload)); - $this->builder->shouldReceive('sign')->once()->with(Mockery::any(), 'secret'); - $this->builder->shouldReceive('getToken')->once()->andReturn('foo.bar.baz'); + $this->builder->shouldReceive('sign') + ->once() + ->with(Mockery::any(), 'secret'); + $this->builder->shouldReceive('getToken') + ->once() + ->andReturn('foo.bar.baz'); $token = $this->getProvider('secret', 'HS256')->encode($payload); $this->assertSame('foo.bar.baz', $token); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage Could not create token: - */ + /** @test */ public function it_should_throw_an_invalid_exception_when_the_payload_could_not_be_encoded() { - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->builder->shouldReceive('unsign')->once()->andReturnSelf(); + $this->expectException(JWTException::class); + $this->expectExceptionMessage('Could not create token:'); + + $payload = [ + Subject::NAME => 1, + Expiration::NAME => $this->testNowTimestamp, + IssuedAt::NAME => $this->testNowTimestamp, + Issuer::NAME => '/foo', + ]; + + $this->builder->shouldReceive('unsign') + ->once() + ->andReturnSelf(); $this->builder->shouldReceive('set')->times(count($payload)); - $this->builder->shouldReceive('sign')->once()->with(Mockery::any(), 'secret')->andThrow(new Exception); + $this->builder->shouldReceive('sign') + ->once() + ->with(Mockery::any(), 'secret') + ->andThrow(new Exception()); $this->getProvider('secret', 'HS256')->encode($payload); } @@ -79,37 +105,57 @@ public function it_should_throw_an_invalid_exception_when_the_payload_could_not_ /** @test */ public function it_should_return_the_payload_when_passing_a_valid_token_to_decode() { - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->parser->shouldReceive('parse')->once()->with('foo.bar.baz')->andReturn(Mockery::self()); - $this->parser->shouldReceive('verify')->once()->with(Mockery::any(), 'secret')->andReturn(true); - $this->parser->shouldReceive('getClaims')->once()->andReturn($payload); + $payload = [ + Subject::NAME => 1, + Expiration::NAME => $this->testNowTimestamp + 3600, + IssuedAt::NAME => $this->testNowTimestamp, + Issuer::NAME => '/foo', + ]; + + $this->parser->shouldReceive('parse') + ->once() + ->with('foo.bar.baz') + ->andReturn(Mockery::self()); + $this->parser->shouldReceive('verify') + ->once() + ->with(Mockery::any(), 'secret') + ->andReturn(true); + $this->parser->shouldReceive('getClaims') + ->once() + ->andReturn($payload); $this->assertSame($payload, $this->getProvider('secret', 'HS256')->decode('foo.bar.baz')); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenInvalidException - * @expectedExceptionMessage Token Signature could not be verified. - */ + /** @test */ public function it_should_throw_a_token_invalid_exception_when_the_token_could_not_be_decoded_due_to_a_bad_signature() { - $this->parser->shouldReceive('parse')->once()->with('foo.bar.baz')->andReturn(Mockery::self()); - $this->parser->shouldReceive('verify')->once()->with(Mockery::any(), 'secret')->andReturn(false); + $this->expectException(TokenInvalidException::class); + $this->expectExceptionMessage('Token Signature could not be verified.'); + + $this->parser->shouldReceive('parse') + ->once() + ->with('foo.bar.baz') + ->andReturn(Mockery::self()); + $this->parser->shouldReceive('verify') + ->once() + ->with(Mockery::any(), 'secret') + ->andReturn(false); $this->parser->shouldReceive('getClaims')->never(); $this->getProvider('secret', 'HS256')->decode('foo.bar.baz'); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenInvalidException - * @expectedExceptionMessage Could not decode token: - */ + /** @test */ public function it_should_throw_a_token_invalid_exception_when_the_token_could_not_be_decoded() { - $this->parser->shouldReceive('parse')->once()->with('foo.bar.baz')->andThrow(new InvalidArgumentException); + $this->expectException(TokenInvalidException::class); + $this->expectExceptionMessage('Could not decode token:'); + + $this->parser->shouldReceive('parse') + ->once() + ->with('foo.bar.baz') + ->andThrow(new InvalidArgumentException()); $this->parser->shouldReceive('verify')->never(); $this->parser->shouldReceive('getClaims')->never(); @@ -119,60 +165,71 @@ public function it_should_throw_a_token_invalid_exception_when_the_token_could_n /** @test */ public function it_should_generate_a_token_when_using_an_rsa_algorithm() { - $provider = $this->getProvider( - 'does_not_matter', - 'RS256', - ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] - ); - - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->builder->shouldReceive('unsign')->once()->andReturnSelf(); + $provider = $this->getProvider('does_not_matter', 'RS256', [ + 'private' => $this->getDummyPrivateKey(), + 'public' => $this->getDummyPublicKey(), + ]); + + $payload = [ + Subject::NAME => 1, + Expiration::NAME => $this->testNowTimestamp + 3600, + IssuedAt::NAME => $this->testNowTimestamp, + Issuer::NAME => '/foo', + ]; + + $this->builder->shouldReceive('unsign') + ->once() + ->andReturnSelf(); $this->builder->shouldReceive('set')->times(count($payload)); - $this->builder->shouldReceive('sign')->once()->with(Mockery::any(), Mockery::type(Key::class)); - $this->builder->shouldReceive('getToken')->once()->andReturn('foo.bar.baz'); + $this->builder->shouldReceive('sign') + ->once() + ->with(Mockery::any(), Mockery::type(Key::class)); + $this->builder->shouldReceive('getToken') + ->once() + ->andReturn('foo.bar.baz'); $token = $provider->encode($payload); $this->assertSame('foo.bar.baz', $token); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage The given algorithm could not be found - */ + /** @test */ public function it_should_throw_a_exception_when_the_algorithm_passed_is_invalid() { + $this->expectException(JWTException::class); + $this->expectExceptionMessage('The given algorithm could not be found'); + $this->parser->shouldReceive('parse')->never(); $this->parser->shouldReceive('verify')->never(); $this->getProvider('secret', 'AlgorithmWrong')->decode('foo.bar.baz'); } - /** - * @test - */ + /** @test */ public function it_should_return_the_public_key() { $provider = $this->getProvider( 'does_not_matter', 'RS256', - $keys = ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] + $keys = [ + 'private' => $this->getDummyPrivateKey(), + 'public' => $this->getDummyPublicKey(), + ] ); $this->assertSame($keys['public'], $provider->getPublicKey()); } - /** - * @test - */ + /** @test */ public function it_should_return_the_keys() { $provider = $this->getProvider( 'does_not_matter', 'RS256', - $keys = ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] + $keys = [ + 'private' => $this->getDummyPrivateKey(), + 'public' => $this->getDummyPublicKey(), + ] ); $this->assertSame($keys, $provider->getKeys()); diff --git a/tests/Providers/JWT/NamshiTest.php b/tests/Providers/JWT/NamshiTest.php deleted file mode 100644 index d0d8f9be9..000000000 --- a/tests/Providers/JWT/NamshiTest.php +++ /dev/null @@ -1,224 +0,0 @@ - - * - * 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 Exception; -use Namshi\JOSE\JWS; -use InvalidArgumentException; -use Tymon\JWTAuth\Providers\JWT\Namshi; -use Tymon\JWTAuth\Test\AbstractTestCase; - -class NamshiTest extends AbstractTestCase -{ - /** - * @var \Mockery\MockInterface - */ - protected $jws; - - /** - * @var \Tymon\JWTAuth\Providers\JWT\Namshi - */ - protected $provider; - - public function setUp() - { - parent::setUp(); - - $this->jws = Mockery::mock(JWS::class); - } - - /** @test */ - public function it_should_return_the_token_when_passing_a_valid_payload_to_encode() - { - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->jws->shouldReceive('setPayload')->once()->with($payload)->andReturn(Mockery::self()); - $this->jws->shouldReceive('sign')->once()->with('secret', null)->andReturn(Mockery::self()); - $this->jws->shouldReceive('getTokenString')->once()->andReturn('foo.bar.baz'); - - $token = $this->getProvider('secret', 'HS256')->encode($payload); - - $this->assertSame('foo.bar.baz', $token); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage Could not create token: - */ - public function it_should_throw_an_invalid_exception_when_the_payload_could_not_be_encoded() - { - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->jws->shouldReceive('setPayload')->once()->with($payload)->andReturn(Mockery::self()); - $this->jws->shouldReceive('sign')->andThrow(new Exception); - - $this->getProvider('secret', 'HS256')->encode($payload); - } - - /** @test */ - public function it_should_return_the_payload_when_passing_a_valid_token_to_decode() - { - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->jws->shouldReceive('load')->once()->with('foo.bar.baz', false)->andReturn(Mockery::self()); - $this->jws->shouldReceive('verify')->once()->with('secret', 'HS256')->andReturn(true); - $this->jws->shouldReceive('getPayload')->andReturn($payload); - - $this->assertSame($payload, $this->getProvider('secret', 'HS256')->decode('foo.bar.baz')); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenInvalidException - * @expectedExceptionMessage Token Signature could not be verified. - */ - public function it_should_throw_a_token_invalid_exception_when_the_token_could_not_be_decoded_due_to_a_bad_signature() - { - $this->jws->shouldReceive('load')->once()->with('foo.bar.baz', false)->andReturn(Mockery::self()); - $this->jws->shouldReceive('verify')->once()->with('secret', 'HS256')->andReturn(false); - $this->jws->shouldReceive('getPayload')->never(); - - $this->getProvider('secret', 'HS256')->decode('foo.bar.baz'); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenInvalidException - * @expectedExceptionMessage Could not decode token: - */ - public function it_should_throw_a_token_invalid_exception_when_the_token_could_not_be_decoded() - { - $this->jws->shouldReceive('load')->once()->with('foo.bar.baz', false)->andThrow(new InvalidArgumentException); - $this->jws->shouldReceive('verify')->never(); - $this->jws->shouldReceive('getPayload')->never(); - - $this->getProvider('secret', 'HS256')->decode('foo.bar.baz'); - } - - /** @test */ - public function it_should_generate_a_token_when_using_an_rsa_algorithm() - { - $provider = $this->getProvider( - 'does_not_matter', - 'RS256', - ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] - ); - - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->jws->shouldReceive('setPayload')->once()->with($payload)->andReturn(Mockery::self()); - $this->jws->shouldReceive('sign')->once()->with($this->getDummyPrivateKey(), null)->andReturn(Mockery::self()); - $this->jws->shouldReceive('getTokenString')->once()->andReturn('foo.bar.baz'); - - $token = $provider->encode($payload); - - $this->assertSame('foo.bar.baz', $token); - } - - /** @test */ - public function it_should_generate_a_token_when_using_an_ecdsa_algorithm() - { - $provider = $this->getProvider( - 'does_not_matter', - 'ES256', - ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] - ); - - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->jws->shouldReceive('setPayload')->once()->with($payload)->andReturn(Mockery::self()); - $this->jws->shouldReceive('sign')->once()->with($this->getDummyPrivateKey(), null)->andReturn(Mockery::self()); - $this->jws->shouldReceive('getTokenString')->once()->andReturn('foo.bar.baz'); - - $token = $provider->encode($payload); - - $this->assertSame('foo.bar.baz', $token); - } - - /** @test */ - public function it_should_decode_a_token_when_using_an_rsa_algorithm() - { - $provider = $this->getProvider( - 'does_not_matter', - 'RS256', - ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] - ); - - $payload = ['sub' => 1, 'exp' => $this->testNowTimestamp + 3600, 'iat' => $this->testNowTimestamp, 'iss' => '/foo']; - - $this->jws->shouldReceive('setPayload')->once()->with($payload)->andReturn(Mockery::self()); - $this->jws->shouldReceive('sign')->once()->with($this->getDummyPrivateKey(), null)->andReturn(Mockery::self()); - $this->jws->shouldReceive('getTokenString')->once()->andReturn('foo.bar.baz'); - - $token = $provider->encode($payload); - - $this->assertSame('foo.bar.baz', $token); - } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\JWTException - * @expectedExceptionMessage The given algorithm could not be found - */ - public function it_should_throw_a_exception_when_the_algorithm_passed_is_invalid() - { - $this->jws->shouldReceive('load')->once()->with('foo.bar.baz', false)->andReturn(Mockery::self()); - $this->jws->shouldReceive('verify')->with('secret', 'AlgorithmWrong')->andReturn(true); - - $this->getProvider('secret', 'AlgorithmWrong')->decode('foo.bar.baz'); - } - - /** - * @test - */ - public function it_should_return_the_public_key() - { - $provider = $this->getProvider( - 'does_not_matter', - 'RS256', - $keys = ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] - ); - - $this->assertSame($keys['public'], $provider->getPublicKey()); - } - - /** - * @test - */ - public function it_should_return_the_keys() - { - $provider = $this->getProvider( - 'does_not_matter', - 'RS256', - $keys = ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()] - ); - - $this->assertSame($keys, $provider->getKeys()); - } - - public function getProvider($secret, $algo, array $keys = []) - { - return new Namshi($this->jws, $secret, $algo, $keys); - } - - public function getDummyPrivateKey() - { - return file_get_contents(__DIR__.'/../Keys/id_rsa'); - } - - public function getDummyPublicKey() - { - return file_get_contents(__DIR__.'/../Keys/id_rsa.pub'); - } -} diff --git a/tests/Providers/JWT/ProviderTest.php b/tests/Providers/JWT/ProviderTest.php index 89a1018e9..be4942d05 100644 --- a/tests/Providers/JWT/ProviderTest.php +++ b/tests/Providers/JWT/ProviderTest.php @@ -21,7 +21,7 @@ class ProviderTest extends AbstractTestCase */ protected $provider; - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/Providers/Storage/IlluminateTest.php b/tests/Providers/Storage/IlluminateTest.php index 80455be3c..e985f4f5a 100644 --- a/tests/Providers/Storage/IlluminateTest.php +++ b/tests/Providers/Storage/IlluminateTest.php @@ -11,11 +11,11 @@ namespace Tymon\JWTAuth\Test\Providers\Storage; +use Illuminate\Contracts\Cache\Repository; use Mockery; +use Tymon\JWTAuth\Providers\Storage\Illuminate as Storage; use Tymon\JWTAuth\Test\AbstractTestCase; -use Illuminate\Contracts\Cache\Repository; use Tymon\JWTAuth\Test\Stubs\TaggedStorage; -use Tymon\JWTAuth\Providers\Storage\Illuminate as Storage; class IlluminateTest extends AbstractTestCase { @@ -29,7 +29,7 @@ class IlluminateTest extends AbstractTestCase */ protected $storage; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -40,7 +40,9 @@ public function setUp() /** @test */ public function it_should_add_the_item_to_storage() { - $this->cache->shouldReceive('put')->with('foo', 'bar', 10)->once(); + $this->cache->shouldReceive('put') + ->with('foo', 'bar', 10) + ->once(); $this->storage->add('foo', 'bar', 10); } @@ -48,7 +50,9 @@ public function it_should_add_the_item_to_storage() /** @test */ public function it_should_add_the_item_to_storage_forever() { - $this->cache->shouldReceive('forever')->with('foo', 'bar')->once(); + $this->cache->shouldReceive('forever') + ->with('foo', 'bar') + ->once(); $this->storage->forever('foo', 'bar'); } @@ -56,7 +60,10 @@ public function it_should_add_the_item_to_storage_forever() /** @test */ public function it_should_get_an_item_from_storage() { - $this->cache->shouldReceive('get')->with('foo')->once()->andReturn(['foo' => 'bar']); + $this->cache->shouldReceive('get') + ->with('foo') + ->once() + ->andReturn(['foo' => 'bar']); $this->assertSame(['foo' => 'bar'], $this->storage->get('foo')); } @@ -64,15 +71,19 @@ public function it_should_get_an_item_from_storage() /** @test */ public function it_should_remove_the_item_from_storage() { - $this->cache->shouldReceive('forget')->with('foo')->once()->andReturn(true); + $this->cache->shouldReceive('forget') + ->with('foo') + ->once(); - $this->assertTrue($this->storage->destroy('foo')); + $this->storage->destroy('foo'); } /** @test */ public function it_should_remove_all_items_from_storage() { - $this->cache->shouldReceive('flush')->withNoArgs()->once(); + $this->cache->shouldReceive('flush') + ->withNoArgs() + ->once(); $this->storage->flush(); } @@ -89,14 +100,19 @@ private function emulateTags() { $this->storage = new TaggedStorage($this->cache); - $this->cache->shouldReceive('tags')->with('tymon.jwt')->once()->andReturn(Mockery::self()); + $this->cache->shouldReceive('tags') + ->with('tymon.jwt') + ->once() + ->andReturn(Mockery::self()); } /** @test */ public function it_should_add_the_item_to_tagged_storage() { $this->emulateTags(); - $this->cache->shouldReceive('put')->with('foo', 'bar', 10)->once(); + $this->cache->shouldReceive('put') + ->with('foo', 'bar', 10) + ->once(); $this->storage->add('foo', 'bar', 10); } @@ -105,7 +121,9 @@ public function it_should_add_the_item_to_tagged_storage() public function it_should_add_the_item_to_tagged_storage_forever() { $this->emulateTags(); - $this->cache->shouldReceive('forever')->with('foo', 'bar')->once(); + $this->cache->shouldReceive('forever') + ->with('foo', 'bar') + ->once(); $this->storage->forever('foo', 'bar'); } @@ -114,7 +132,10 @@ public function it_should_add_the_item_to_tagged_storage_forever() public function it_should_get_an_item_from_tagged_storage() { $this->emulateTags(); - $this->cache->shouldReceive('get')->with('foo')->once()->andReturn(['foo' => 'bar']); + $this->cache->shouldReceive('get') + ->with('foo') + ->once() + ->andReturn(['foo' => 'bar']); $this->assertSame(['foo' => 'bar'], $this->storage->get('foo')); } @@ -123,16 +144,20 @@ public function it_should_get_an_item_from_tagged_storage() public function it_should_remove_the_item_from_tagged_storage() { $this->emulateTags(); - $this->cache->shouldReceive('forget')->with('foo')->once()->andReturn(true); + $this->cache->shouldReceive('forget') + ->with('foo') + ->once(); - $this->assertTrue($this->storage->destroy('foo')); + $this->storage->destroy('foo'); } /** @test */ public function it_should_remove_all_tagged_items_from_storage() { $this->emulateTags(); - $this->cache->shouldReceive('flush')->withNoArgs()->once(); + $this->cache->shouldReceive('flush') + ->withNoArgs() + ->once(); $this->storage->flush(); } diff --git a/tests/Stubs/JWTProviderStub.php b/tests/Stubs/JWTProviderStub.php index ef47d9136..58541240a 100644 --- a/tests/Stubs/JWTProviderStub.php +++ b/tests/Stubs/JWTProviderStub.php @@ -18,8 +18,24 @@ class JWTProviderStub extends Provider /** * {@inheritdoc} */ - protected function isAsymmetric() + protected function isAsymmetric(): bool { return false; } + + /** + * Create a JSON Web Token. + */ + public function encode(array $payload): string + { + return 'foo.bar.baz'; + } + + /** + * Decode a JSON Web Token. + */ + public function decode(string $token): array + { + return ['foo' => 'bar']; + } } diff --git a/tests/Stubs/LaravelUserStub.php b/tests/Stubs/LaravelUserStub.php index 616ddbfd2..97ca3e9f9 100644 --- a/tests/Stubs/LaravelUserStub.php +++ b/tests/Stubs/LaravelUserStub.php @@ -11,8 +11,8 @@ namespace Tymon\JWTAuth\Test\Stubs; -use Tymon\JWTAuth\Contracts\JWTSubject; use Illuminate\Contracts\Auth\Authenticatable; +use Tymon\JWTAuth\Contracts\JWTSubject; class LaravelUserStub extends UserStub implements Authenticatable, JWTSubject { diff --git a/tests/Stubs/SentinelStub.php b/tests/Stubs/SentinelStub.php deleted file mode 100644 index 51517db6d..000000000 --- a/tests/Stubs/SentinelStub.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * 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 Cartalyst\Sentinel\Users\UserInterface; - -class SentinelStub implements UserInterface -{ - public function getUserId() - { - return 123; - } - - public function getUserLogin() - { - return 'foo'; - } - - public function getUserLoginName() - { - return 'bar'; - } - - public function getUserPassword() - { - return 'baz'; - } -} diff --git a/tests/Stubs/TaggedStorage.php b/tests/Stubs/TaggedStorage.php index e1b73c987..80671171a 100644 --- a/tests/Stubs/TaggedStorage.php +++ b/tests/Stubs/TaggedStorage.php @@ -20,5 +20,5 @@ class TaggedStorage extends Storage // aren't defined in the interface. Getting those conditionals to behave as expected // would be a lot of finicky work compared to verifying their functionality by hand. // So instead we'll just set this value manually... - protected $supportsTags = true; + protected ?bool $supportsTags = true; } diff --git a/tests/TokenTest.php b/tests/TokenTest.php index ca44ce7fb..034f7b870 100644 --- a/tests/TokenTest.php +++ b/tests/TokenTest.php @@ -20,7 +20,7 @@ class TokenTest extends AbstractTestCase */ protected $token; - public function setUp() + public function setUp(): void { parent::setUp(); @@ -36,6 +36,6 @@ public function it_should_return_the_token_when_casting_to_a_string() /** @test */ public function it_should_return_the_token_when_calling_get_method() { - $this->assertInternalType('string', $this->token->get()); + $this->assertIsString($this->token->get()); } } diff --git a/tests/Validators/PayloadValidatorTest.php b/tests/Validators/PayloadValidatorTest.php index 3166de2b6..8c038e666 100644 --- a/tests/Validators/PayloadValidatorTest.php +++ b/tests/Validators/PayloadValidatorTest.php @@ -11,36 +11,28 @@ namespace Tymon\JWTAuth\Test\Validators; -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\Collection; use Tymon\JWTAuth\Claims\Expiration; +use Tymon\JWTAuth\Claims\IssuedAt; +use Tymon\JWTAuth\Claims\Issuer; +use Tymon\JWTAuth\Claims\JwtId; +use Tymon\JWTAuth\Claims\NotBefore; +use Tymon\JWTAuth\Claims\Subject; +use Tymon\JWTAuth\Exceptions\InvalidClaimException; +use Tymon\JWTAuth\Exceptions\TokenExpiredException; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; +use Tymon\JWTAuth\Options; use Tymon\JWTAuth\Test\AbstractTestCase; use Tymon\JWTAuth\Validators\PayloadValidator; class PayloadValidatorTest extends AbstractTestCase { - /** - * @var \Tymon\JWTAuth\Validators\PayloadValidator - */ - protected $validator; - - public function setUp() - { - parent::setUp(); - - $this->validator = new PayloadValidator; - } - /** @test */ public function it_should_return_true_when_providing_a_valid_payload() { $claims = [ new Subject(1), - new Issuer('http://example.com'), + new Issuer('example.com'), new Expiration($this->testNowTimestamp + 3600), new NotBefore($this->testNowTimestamp), new IssuedAt($this->testNowTimestamp), @@ -49,19 +41,18 @@ public function it_should_return_true_when_providing_a_valid_payload() $collection = Collection::make($claims); - $this->assertTrue($this->validator->isValid($collection)); + $this->assertTrue(PayloadValidator::isValid($collection)); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenExpiredException - * @expectedExceptionMessage Token has expired - */ + /** @test */ public function it_should_throw_an_exception_when_providing_an_expired_payload() { + $this->expectException(TokenExpiredException::class); + $this->expectExceptionMessage('Token has expired'); + $claims = [ new Subject(1), - new Issuer('http://example.com'), + new Issuer('example.com'), new Expiration($this->testNowTimestamp - 1440), new NotBefore($this->testNowTimestamp - 3660), new IssuedAt($this->testNowTimestamp - 3660), @@ -70,19 +61,18 @@ public function it_should_throw_an_exception_when_providing_an_expired_payload() $collection = Collection::make($claims); - $this->validator->check($collection); + PayloadValidator::check($collection); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\InvalidClaimException - * @expectedExceptionMessage Invalid value provided for claim [nbf] - */ + /** @test */ public function it_should_throw_an_exception_when_providing_an_invalid_nbf_claim() { + $this->expectException(TokenInvalidException::class); + $this->expectExceptionMessage('Not Before (nbf) timestamp cannot be in the future'); + $claims = [ new Subject(1), - new Issuer('http://example.com'), + new Issuer('example.com'), new Expiration($this->testNowTimestamp + 1440), new NotBefore($this->testNowTimestamp + 3660), new IssuedAt($this->testNowTimestamp - 3660), @@ -91,19 +81,18 @@ public function it_should_throw_an_exception_when_providing_an_invalid_nbf_claim $collection = Collection::make($claims); - $this->validator->check($collection); + PayloadValidator::check($collection); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\InvalidClaimException - * @expectedExceptionMessage Invalid value provided for claim [iat] - */ + /** @test */ public function it_should_throw_an_exception_when_providing_an_invalid_iat_claim() { + $this->expectException(InvalidClaimException::class); + $this->expectExceptionMessage('Invalid value provided for claim [iat]'); + $claims = [ new Subject(1), - new Issuer('http://example.com'), + new Issuer('example.com'), new Expiration($this->testNowTimestamp + 1440), new NotBefore($this->testNowTimestamp - 3660), new IssuedAt($this->testNowTimestamp + 3660), @@ -112,33 +101,28 @@ public function it_should_throw_an_exception_when_providing_an_invalid_iat_claim $collection = Collection::make($claims); - $this->validator->check($collection); + PayloadValidator::check($collection); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenInvalidException - * @expectedExceptionMessage JWT payload does not contain the required claims - */ + /** @test */ public function it_should_throw_an_exception_when_providing_an_invalid_payload() { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - ]; + $this->expectException(TokenInvalidException::class); + $this->expectExceptionMessage('JWT does not contain the required claims'); + + $claims = [new Subject(1), new Issuer('http://example.com')]; $collection = Collection::make($claims); - $this->validator->check($collection); + PayloadValidator::check($collection, new Options(['required_claims' => ['foo']])); } - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\InvalidClaimException - * @expectedExceptionMessage Invalid value provided for claim [exp] - */ + /** @test */ public function it_should_throw_an_exception_when_providing_an_invalid_expiry() { + $this->expectException(InvalidClaimException::class); + $this->expectExceptionMessage('Invalid value provided for claim [exp]'); + $claims = [ new Subject(1), new Issuer('http://example.com'), @@ -150,78 +134,20 @@ public function it_should_throw_an_exception_when_providing_an_invalid_expiry() $collection = Collection::make($claims); - $this->validator->check($collection); + PayloadValidator::check($collection); } /** @test */ public function it_should_set_the_required_claims() { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - ]; - - $collection = Collection::make($claims); - - $this->assertTrue($this->validator->setRequiredClaims(['iss', 'sub'])->isValid($collection)); - } - - /** @test */ - public function it_should_check_the_token_in_the_refresh_context() - { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp - 1000), - new NotBefore($this->testNowTimestamp), - new IssuedAt($this->testNowTimestamp - 2600), // this is LESS than the refresh ttl at 1 hour - new JwtId('foo'), - ]; - - $collection = Collection::make($claims); - - $this->assertTrue( - $this->validator->setRefreshFlow()->setRefreshTTL(60)->isValid($collection) - ); - } - - /** @test */ - public function it_should_return_true_if_the_refresh_ttl_is_null() - { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp - 1000), - new NotBefore($this->testNowTimestamp), - new IssuedAt($this->testNowTimestamp - 2600), // this is LESS than the refresh ttl at 1 hour - new JwtId('foo'), - ]; + $claims = [new Subject(1), new Issuer('http://example.com')]; $collection = Collection::make($claims); $this->assertTrue( - $this->validator->setRefreshFlow()->setRefreshTTL(null)->isValid($collection) + PayloadValidator::isValid($collection, new Options([ + 'required_claims' => [Issuer::NAME, Subject::NAME], + ])) ); } - - /** - * @test - * @expectedException \Tymon\JWTAuth\Exceptions\TokenExpiredException - * @expectedExceptionMessage Token has expired and can no longer be refreshed - */ - public function it_should_throw_an_exception_if_the_token_cannot_be_refreshed() - { - $claims = [ - new Subject(1), - new Issuer('http://example.com'), - new Expiration($this->testNowTimestamp), - new NotBefore($this->testNowTimestamp), - new IssuedAt($this->testNowTimestamp - 5000), // this is MORE than the refresh ttl at 1 hour, so is invalid - new JwtId('foo'), - ]; - - $collection = Collection::make($claims); - - $this->validator->setRefreshFlow()->setRefreshTTL(60)->check($collection); - } } diff --git a/tests/Validators/TokenValidatorTest.php b/tests/Validators/TokenValidatorTest.php index 657fd6198..2842888e5 100644 --- a/tests/Validators/TokenValidatorTest.php +++ b/tests/Validators/TokenValidatorTest.php @@ -11,40 +11,16 @@ namespace Tymon\JWTAuth\Test\Validators; +use Tymon\JWTAuth\Exceptions\TokenInvalidException; use Tymon\JWTAuth\Test\AbstractTestCase; use Tymon\JWTAuth\Validators\TokenValidator; class TokenValidatorTest extends AbstractTestCase { - /** - * @var \Tymon\JWTAuth\Validators\TokenValidator - */ - protected $validator; - - public function setUp() - { - parent::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')); - } - - public function dataProviderMalformedTokens() - { - return [ - ['one.two.'], - ['.two.'], - ['.two.three'], - ['one..three'], - ['..'], - [' . . '], - [' one . two . three '], - ]; + $this->assertTrue(TokenValidator::isValid('one.two.three')); } /** @@ -55,7 +31,7 @@ public function dataProviderMalformedTokens() */ public function it_should_return_false_when_providing_a_malformed_token($token) { - $this->assertFalse($this->validator->isValid($token)); + $this->assertFalse(TokenValidator::isValid($token)); } /** @@ -63,21 +39,18 @@ public function it_should_return_false_when_providing_a_malformed_token($token) * @dataProvider \Tymon\JWTAuth\Test\Validators\TokenValidatorTest::dataProviderMalformedTokens * * @param string $token - * @expectedException \Tymon\JWTAuth\Exceptions\TokenInvalidException - * @expectedExceptionMessage Malformed token */ public function it_should_throw_an_exception_when_providing_a_malformed_token($token) { - $this->validator->check($token); + $this->expectException(TokenInvalidException::class); + $this->expectExceptionMessage('Malformed token'); + + TokenValidator::check($token); } public function dataProviderTokensWithWrongSegmentsNumber() { - return [ - ['one.two'], - ['one.two.three.four'], - ['one.two.three.four.five'], - ]; + return [['one.two'], ['one.two.three.four'], ['one.two.three.four.five']]; } /** @@ -86,9 +59,10 @@ public function dataProviderTokensWithWrongSegmentsNumber() * * @param string $token */ - public function it_should_return_false_when_providing_a_token_with_wrong_segments_number($token) - { - $this->assertFalse($this->validator->isValid($token)); + public function it_should_return_false_when_providing_a_token_with_wrong_segments_number( + $token + ) { + $this->assertFalse(TokenValidator::isValid($token)); } /** @@ -96,11 +70,26 @@ public function it_should_return_false_when_providing_a_token_with_wrong_segment * @dataProvider \Tymon\JWTAuth\Test\Validators\TokenValidatorTest::dataProviderTokensWithWrongSegmentsNumber * * @param string $token - * @expectedException \Tymon\JWTAuth\Exceptions\TokenInvalidException - * @expectedExceptionMessage Wrong number of segments */ - public function it_should_throw_an_exception_when_providing_a_malformed_token_with_wrong_segments_number($token) + public function it_should_throw_an_exception_when_providing_a_malformed_token_with_wrong_segments_number( + $token + ) { + $this->expectException(TokenInvalidException::class); + $this->expectExceptionMessage('Wrong number of segments'); + + TokenValidator::check($token); + } + + public function dataProviderMalformedTokens() { - $this->validator->check($token); + return [ + ['one.two.'], + ['.two.'], + ['.two.three'], + ['one..three'], + ['..'], + [' . . '], + [' one . two . three '], + ]; } }