├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── multiple-tokens-auth.php ├── database ├── factories │ ├── ApiTokenFactory.php │ └── UserFactory.php └── migrations │ └── 2019_12_29_134146_create_api_tokens_table.php ├── phpunit.xml ├── src ├── Jobs │ └── PurgeExpiredApiTokensJob.php ├── Models │ └── ApiToken.php ├── MultipleTokensAuthServiceProvider.php ├── MultipleTokensGuard.php └── Traits │ └── HasApiTokens.php └── tests ├── ApiTokenTest.php ├── HasApiTokensTest.php ├── MultipleTokensAuthGuardTest.php ├── PurgeExpiredApiTokensJobTest.php ├── TestCase.php └── User.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .idea 4 | .phpunit* 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Fredrik Livijn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important 2 | This was released before Laravel Sanctum. I would recommend using [Laravel Sanctum](https://laravel.com/docs/master/sanctum) instead. 3 | 4 | # multiple-tokens-auth 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/livijn/multiple-tokens-auth.svg?style=flat-square)](https://packagist.org/packages/livijn/multiple-tokens-auth) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/livijn/multiple-tokens-auth.svg?style=flat-square)](https://packagist.org/packages/livijn/multiple-tokens-auth) 7 | 8 | Adds the ability to use multiple tokens for the auth:api middleware. Useful if you want to allow a user to be logged in to your e.g. SPA, iOS app and android app at the same time. The default token driver only allows one token per user. 9 | 10 | It is possible to end up with a large table when using multiple tokens per user. Therefor we set an expiration date on the tokens. If possible, you should add the `PurgeExpiredApiTokensJob` to your Schedule as the *Step 6* describes. If not, you should somehow take care of the expired tokens. 11 | 12 | You may take a look at the example app [multiple-tokens-auth-testapp](https://github.com/Livijn/multiple-tokens-auth-testapp). 13 | 14 | ## Install 15 | 1. Install the package with composer: 16 | ```bash 17 | composer require livijn/multiple-tokens-auth 18 | ``` 19 | 20 | 2. Publish the `multiple-tokens-auth.php` config & migrations: 21 | ```bash 22 | php artisan vendor:publish --provider="Livijn\MultipleTokensAuth\MultipleTokensAuthServiceProvider" 23 | ``` 24 | > By default, the migration is shipped with the field `user_id` that has `unsignedBigInteger`. This needs to be manually changed if you use `uuid` in your User model. 25 | 26 | 3. Run the migrations: 27 | ```bash 28 | php artisan migrate 29 | ``` 30 | 31 | 4. Set the api guard driver to `multiple-tokens` in the file `config/auth.php`: 32 | ```php 33 | 'guards' => [ 34 | // ... 35 | 36 | 'api' => [ 37 | 'driver' => 'multiple-tokens', // <- Change this FROM token TO multiple-tokens 38 | 39 | // ... 40 | ], 41 | ], 42 | ``` 43 | 44 | 5. Add the `HasApiTokens` trait to your User model. 45 | ```php 46 | class User extends Authenticatable 47 | { 48 | use Notifiable, HasApiTokens; 49 | 50 | // ... 51 | } 52 | ``` 53 | 54 | 6. *(Optional)* Add the `PurgeExpiredApiTokensJob` to your Schedule at `Console/Kernel.php`. 55 | ```php 56 | protected function schedule(Schedule $schedule) 57 | { 58 | $schedule->job(PurgeExpiredApiTokensJob::class)->dailyAt('01:00'); 59 | } 60 | ``` 61 | 62 | ## Usage 63 | You can use this the same way as you would use the [default Laravel token based API authorization](https://laravel.com/docs/master/api-authentication). This package also supports [hashing](https://laravel.com/docs/master/api-authentication#hashing-tokens). 64 | 65 | ### Sign in 66 | When a user logs in, you should create a new api token by using the `generateApiToken` method. 67 | ```php 68 | $user = User::first(); 69 | $token = $user->generateApiToken(); // returns ltBKMC8zwnshLcrVh9W07IGuifysDqkyWRt6Z5szYJOrh1mnNPValkAtETj0vtPJdsfDQa4E3Yx0N3QU 70 | ``` 71 | 72 | ### Sign out 73 | When you want to log out a user, you can use the `logout` method on the Auth facade. This will delete the token that was used for the current request. 74 | ```php 75 | auth()->logout(); 76 | // or 77 | Auth::logout(); 78 | ``` 79 | 80 | ### Purging tokens 81 | To delete all tokens connected to a user, use the `purgeApiTokens` method. 82 | ```php 83 | $user = User::first(); 84 | $user->purgeApiTokens(); 85 | ``` 86 | 87 | ## Testing 88 | Run the tests with: 89 | 90 | ```bash 91 | vendor/bin/phpunit 92 | ``` 93 | 94 | ## Credits 95 | 96 | - [Fredrik Livijn](https://github.com/livijn) 97 | 98 | ## License 99 | 100 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 101 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livijn/multiple-tokens-auth", 3 | "description": "Adds the ability to use multiple tokens for the auth:api middleware.", 4 | "homepage": "https://github.com/livijn/multiple-tokens-auth", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Fredrik Livijn", 9 | "email": "ouff@live.se" 10 | } 11 | ], 12 | "require": { 13 | "php" : ">=7.2.0" 14 | }, 15 | "require-dev": { 16 | "orchestra/testbench": "^4.0", 17 | "phpunit/phpunit": "^8.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Livijn\\MultipleTokensAuth\\": "src" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Livijn\\MultipleTokensAuth\\Test\\": "tests" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Livijn\\MultipleTokensAuth\\MultipleTokensAuthServiceProvider" 33 | ] 34 | } 35 | }, 36 | "config": { 37 | "sort-packages": true 38 | }, 39 | "minimum-stability": "dev", 40 | "prefer-stable": true 41 | } 42 | -------------------------------------------------------------------------------- /config/multiple-tokens-auth.php: -------------------------------------------------------------------------------- 1 | 'api_tokens', 5 | 6 | 'token' => [ 7 | /* 8 | * Amount of days token should live 9 | * Value is in days. 10 | */ 11 | 'life_length' => 60, 12 | 13 | /* 14 | * Amount of days left of life when we should extend it 15 | * Value is in days. 16 | */ 17 | 'extend_life_at' => 10, 18 | 19 | /* 20 | * Amount of characters for tokens. 21 | */ 22 | 'char_length' => 80, 23 | ], 24 | 25 | /** 26 | * Set to true or false to enable/disable token hashing. 27 | * When set to null, it will default to the auth.guards.api.hash config var. 28 | */ 29 | 'hash' => null, 30 | ]; 31 | -------------------------------------------------------------------------------- /database/factories/ApiTokenFactory.php: -------------------------------------------------------------------------------- 1 | define(ApiToken::class, function (Faker $faker) { 8 | return [ 9 | 'user_id' => factory(User::class), 10 | 'token' => Str::random(64), 11 | 'expired_at' => now()->addDays(60), 12 | ]; 13 | }); 14 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker $faker) { 7 | return [ 8 | 'name' => $faker->name, 9 | 'email' => $faker->unique()->safeEmail, 10 | 'email_verified_at' => now(), 11 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 12 | 'remember_token' => Str::random(10), 13 | ]; 14 | }); 15 | -------------------------------------------------------------------------------- /database/migrations/2019_12_29_134146_create_api_tokens_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('user_id')->index(); 18 | $table->string('token', config('multiple-tokens-auth.token.char_length'))->collation('utf8mb4_bin')->unique(); 19 | $table->dateTime('expired_at'); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::drop(config('multiple-tokens-auth.table')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Jobs/PurgeExpiredApiTokensJob.php: -------------------------------------------------------------------------------- 1 | delete(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/ApiToken.php: -------------------------------------------------------------------------------- 1 | 'datetime' 15 | ]; 16 | 17 | protected $primaryKey = 'token'; 18 | 19 | public $incrementing = false; 20 | 21 | public function __construct(array $attributes = []) 22 | { 23 | parent::__construct($attributes); 24 | 25 | $this->setTable(config('multiple-tokens-auth.table')); 26 | } 27 | 28 | public function shouldExtendLife() 29 | { 30 | if ($this->hasExpired()) { 31 | return false; 32 | } 33 | 34 | return $this->expired_at->isBefore( 35 | now()->addDays(config('multiple-tokens-auth.token.extend_life_at')) 36 | ); 37 | } 38 | 39 | public function hasExpired() 40 | { 41 | return $this->expired_at->isPast(); 42 | } 43 | 44 | public function scopeWhereHasExpired(Builder $query) 45 | { 46 | return $query->whereDate('expired_at', '<', now()); 47 | } 48 | 49 | public function scopeWhereHasNotExpired(Builder $query) 50 | { 51 | return $query->whereDate('expired_at', '>=', now()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MultipleTokensAuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 12 | __DIR__ . '/../config/multiple-tokens-auth.php' => config_path('multiple-tokens-auth.php'), 13 | ], 'config'); 14 | 15 | $this->publishes([ 16 | __DIR__ . '/../database/migrations/' => database_path('migrations') 17 | ], 'migrations'); 18 | 19 | Auth::extend('multiple-tokens', function ($app, $name, array $config) { 20 | return new MultipleTokensGuard( 21 | Auth::createUserProvider($config['provider']), 22 | $app['request'], 23 | config()->get('multiple-tokens-auth.hash') ?? $config['hash'] ?? false 24 | ); 25 | }); 26 | } 27 | 28 | public function register() 29 | { 30 | $this->mergeConfigFrom( 31 | __DIR__.'/../config/multiple-tokens-auth.php', 32 | 'multiple-tokens-auth' 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MultipleTokensGuard.php: -------------------------------------------------------------------------------- 1 | hash = $hash; 31 | $this->request = $request; 32 | $this->provider = $provider; 33 | } 34 | 35 | public function user() 36 | { 37 | if (! is_null($this->user)) { 38 | return $this->user; 39 | } 40 | 41 | $token = $this->getTokenForRequest(); 42 | 43 | $apiToken = ApiToken::where('token', $this->hashedToken($token)) 44 | ->whereHasNotExpired() 45 | ->first(); 46 | 47 | if (is_null($apiToken)) { 48 | return $this->user = null; 49 | } 50 | 51 | if ($apiToken->shouldExtendLife()) { 52 | $apiToken->update([ 53 | 'expired_at' => now()->addDays(config('multiple-tokens-auth.token.life_length')), 54 | ]); 55 | } 56 | 57 | return $this->user = $this->provider->retrieveById($apiToken->user_id); 58 | } 59 | 60 | public function validate(array $credentials = []) 61 | { 62 | return ApiToken::where('token', $this->hashedToken($credentials)) 63 | ->whereHasNotExpired() 64 | ->exists(); 65 | } 66 | 67 | public function logout() 68 | { 69 | if ($this->guest() || ! $token = $this->getTokenForRequest()) { 70 | return; 71 | } 72 | 73 | ApiToken::where('token', $this->hashedToken($token))->delete(); 74 | 75 | $this->user = null; 76 | } 77 | 78 | private function getTokenForRequest() 79 | { 80 | $token = $this->request->query('api_token'); 81 | 82 | if (empty($token)) { 83 | $token = $this->request->input('api_token'); 84 | } 85 | 86 | if (empty($token)) { 87 | $token = $this->request->bearerToken(); 88 | } 89 | 90 | if (empty($token)) { 91 | $token = $this->request->getPassword(); 92 | } 93 | 94 | return $token; 95 | } 96 | 97 | private function hashedToken($token) 98 | { 99 | $token = is_array($token) ? $token['token'] : $token; 100 | 101 | return $this->hash ? hash('sha256', $token) : $token; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Traits/HasApiTokens.php: -------------------------------------------------------------------------------- 1 | hasMany(ApiToken::class); 12 | } 13 | 14 | public function generateApiToken() 15 | { 16 | $useHash = config('multiple-tokens-auth.hash') ?? config('auth.guards.api.hash', false); 17 | $unique = false; 18 | $token = null; 19 | $hashedToken = null; 20 | 21 | while (! $unique) { 22 | $token = Str::random(config('multiple-tokens-auth.token.char_length')); 23 | $hashedToken = $useHash 24 | ? hash('sha256', $token) 25 | : $token; 26 | 27 | $unique = ApiToken::where('token', $hashedToken)->exists() == false; 28 | } 29 | 30 | ApiToken::create([ 31 | 'user_id' => $this->getAuthIdentifier(), 32 | 'token' => $hashedToken, 33 | 'expired_at' => now()->addDays(config('multiple-tokens-auth.token.life_length')), 34 | ]); 35 | 36 | return $token; 37 | } 38 | 39 | public function purgeApiTokens() 40 | { 41 | $this->apiTokens()->delete(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/ApiTokenTest.php: -------------------------------------------------------------------------------- 1 | create(); 12 | 13 | $this->assertFalse($token->hasExpired()); 14 | 15 | $token->update([ 16 | 'expired_at' => now()->subDay(), 17 | ]); 18 | 19 | $this->assertTrue($token->hasExpired()); 20 | } 21 | 22 | /** @test It can determine if it should extend its life */ 23 | public function it_can_determine_if_it_should_extend_its_life() 24 | { 25 | $token = factory(ApiToken::class)->create([ 26 | 'expired_at' => now()->addDays(config('multiple-tokens-auth.token.extend_life_at') + 1), 27 | ]); 28 | 29 | $this->assertFalse($token->shouldExtendLife()); 30 | 31 | $token->update([ 32 | 'expired_at' => now()->addDays(config('multiple-tokens-auth.token.extend_life_at') - 1), 33 | ]); 34 | 35 | $this->assertTrue($token->shouldExtendLife()); 36 | 37 | $token->update([ 38 | 'expired_at' => now()->subDay(), 39 | ]); 40 | 41 | $this->assertFalse($token->shouldExtendLife()); 42 | } 43 | 44 | /** @test It can be scoped by whereHasExpired */ 45 | public function it_can_be_scoped_by_whereHasExpired() 46 | { 47 | $token = factory(ApiToken::class)->create(); 48 | 49 | $this->assertEquals(0, ApiToken::whereHasExpired()->count()); 50 | 51 | $token->update([ 52 | 'expired_at' => now()->subDay(), 53 | ]); 54 | 55 | $this->assertEquals(1, ApiToken::whereHasExpired()->count()); 56 | } 57 | 58 | /** @test It can be scoped by whereHasNotExpired */ 59 | public function it_can_be_scoped_by_whereHasNotExpired() 60 | { 61 | $token = factory(ApiToken::class)->create(); 62 | 63 | $this->assertEquals(1, ApiToken::whereHasNotExpired()->count()); 64 | 65 | $token->update([ 66 | 'expired_at' => now()->subDay(), 67 | ]); 68 | 69 | $this->assertEquals(0, ApiToken::whereHasNotExpired()->count()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/HasApiTokensTest.php: -------------------------------------------------------------------------------- 1 | create(); 14 | factory(ApiToken::class, 3)->create(['user_id' => $user->id]); 15 | 16 | $this->assertEquals(3, $user->apiTokens()->count()); 17 | } 18 | 19 | /** @test It can generate an api token without hash */ 20 | public function it_can_generate_an_api_token_without_hash() 21 | { 22 | $user = factory(User::class)->create(); 23 | 24 | $this->assertEquals(0, ApiToken::count()); 25 | $this->assertEquals(0, $user->apiTokens()->count()); 26 | 27 | $token = $user->generateApiToken(); 28 | 29 | $this->assertEquals(1, ApiToken::count()); 30 | $this->assertEquals(1, $user->apiTokens()->count()); 31 | $this->assertEquals($user->id, ApiToken::first()->user_id); 32 | $this->assertEquals($token, ApiToken::first()->token); 33 | $this->assertTrue(ApiToken::first()->expired_at->isSameDay(now()->addDays(config('multiple-tokens-auth.token.life_length')))); 34 | } 35 | 36 | /** @test It can generate an api token with hash */ 37 | public function it_can_generate_an_api_token_with_hash() 38 | { 39 | config()->set('auth.guards.api.hash', true); 40 | 41 | $user = factory(User::class)->create(); 42 | 43 | $this->assertEquals(0, ApiToken::count()); 44 | $this->assertEquals(0, $user->apiTokens()->count()); 45 | 46 | $token = $user->generateApiToken(); 47 | 48 | $this->assertEquals(1, ApiToken::count()); 49 | $this->assertEquals(1, $user->apiTokens()->count()); 50 | $this->assertEquals($user->id, ApiToken::first()->user_id); 51 | $this->assertEquals(hash('sha256', $token), ApiToken::first()->token); 52 | } 53 | 54 | /** @test It uses the hash config variable when generating a token */ 55 | public function it_uses_the_hash_config_variable_when_generating_a_token() 56 | { 57 | $user = factory(User::class)->create(); 58 | 59 | config()->set('multiple-tokens-auth.hash', null); 60 | $tokenOne = $user->generateApiToken(); 61 | 62 | config()->set('multiple-tokens-auth.hash', true); 63 | $tokenTwo = $user->generateApiToken(); 64 | 65 | config()->set('multiple-tokens-auth.hash', false); 66 | config()->set('auth.guards.api.hash', true); 67 | $tokenThree = $user->generateApiToken(); 68 | 69 | $this->assertEquals($tokenOne, ApiToken::first()->token); 70 | $this->assertEquals(hash('sha256', $tokenTwo), ApiToken::skip(1)->first()->token); 71 | $this->assertEquals($tokenThree, ApiToken::skip(2)->first()->token); 72 | } 73 | 74 | /** @test It can purge api tokens */ 75 | public function it_can_purge_api_tokens() 76 | { 77 | factory(ApiToken::class)->create(); 78 | $user = factory(User::class)->create(); 79 | $user->generateApiToken(); 80 | $user->generateApiToken(); 81 | 82 | $this->assertEquals(3, ApiToken::count()); 83 | 84 | $user->purgeApiTokens(); 85 | 86 | $this->assertEquals(1, ApiToken::count()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/MultipleTokensAuthGuardTest.php: -------------------------------------------------------------------------------- 1 | app['auth']->createUserProvider('users'), 17 | new Request(['api_token' => $token]), 18 | $hash 19 | ); 20 | } 21 | 22 | /** @test It can validate credentials without hash */ 23 | public function it_can_validate_credentials_without_hash() 24 | { 25 | $token = factory(ApiToken::class)->create(); 26 | $guard = $this->createGuard(); 27 | 28 | $this->assertFalse($guard->validate(['token' => 'some-random-token'])); 29 | $this->assertTrue($guard->validate(['token' => $token->token])); 30 | } 31 | 32 | /** @test It can validate credentials with hash */ 33 | public function it_can_validate_credentials_with_hash() 34 | { 35 | $token = Str::random(64); 36 | factory(ApiToken::class)->create(['token' => hash('sha256', $token)]); 37 | $guard = $this->createGuard(true); 38 | 39 | $this->assertFalse($guard->validate(['token' => 'some-random-token'])); 40 | $this->assertTrue($guard->validate(['token' => $token])); 41 | } 42 | 43 | /** @test It doesnt validate expired tokens */ 44 | public function it_doesnt_validate_expired_tokens() 45 | { 46 | $token = factory(ApiToken::class)->create(['expired_at' => now()->subDay()]); 47 | $guard = $this->createGuard(); 48 | 49 | $this->assertFalse($guard->validate(['token' => $token->token])); 50 | } 51 | 52 | /** @test It can get a user from a valid token */ 53 | public function it_can_get_a_user_from_a_valid_token() 54 | { 55 | $user = factory(User::class)->create(); 56 | $token = factory(ApiToken::class)->create(['user_id' => $user->id]); 57 | $guard = $this->createGuard(false, $token->token); 58 | 59 | $this->assertNotNull($guard->user()); 60 | $this->assertTrue($user->is($guard->user())); 61 | } 62 | 63 | /** @test It doesnt return a user if the token is expired */ 64 | public function it_doesnt_return_a_user_if_the_token_is_expired() 65 | { 66 | $token = factory(ApiToken::class)->create(['expired_at' => now()->subDay()]); 67 | $guard = $this->createGuard(false, $token->token); 68 | 69 | $this->assertNull($guard->user()); 70 | } 71 | 72 | /** @test It returns 401 if the token is invalid */ 73 | public function it_returns_401_if_the_token_is_invalid() 74 | { 75 | Route::get('multiple-tokens-auth/test-invalid-token', function () { 76 | return true; 77 | })->middleware('auth:api'); 78 | 79 | $request = $this->getJson('multiple-tokens-auth/test-invalid-token'); 80 | $request->assertStatus(401); 81 | 82 | $request = $this->getJson('multiple-tokens-auth/test-invalid-token', ['Authorization' => 'Bearer abc123']); 83 | $request->assertStatus(401); 84 | } 85 | 86 | /** @test It can get the user from a request */ 87 | public function it_can_get_the_user_from_a_request() 88 | { 89 | Route::get('multiple-tokens-auth/test-guard', function () { 90 | return ['user' => auth()->user()]; 91 | })->middleware('auth:api'); 92 | 93 | $user = factory(User::class)->create(); 94 | $token = $user->generateApiToken(); 95 | 96 | $request = $this->getJson('multiple-tokens-auth/test-guard', ['Authorization' => 'Bearer ' . $token]); 97 | $request->assertJson(['user' => [ 98 | 'id' => $user->id, 99 | ]]); 100 | } 101 | 102 | /** @test It can logout without hash */ 103 | public function it_can_logout_without_hash() 104 | { 105 | Route::get('multiple-tokens-auth/test-logout', function () { 106 | auth()->logout(); 107 | return null; 108 | })->middleware('auth:api'); 109 | 110 | factory(ApiToken::class)->create(); 111 | $user = factory(User::class)->create(); 112 | $tokenOne = $user->generateApiToken(); 113 | $tokenTwo = $user->generateApiToken(); 114 | 115 | $this->assertEquals(3, ApiToken::count()); 116 | $this->assertEquals(2, $user->apiTokens()->count()); 117 | 118 | $request = $this->getJson('multiple-tokens-auth/test-logout', ['Authorization' => 'Bearer ' . $tokenTwo]); 119 | $request->assertSuccessful(); 120 | 121 | $this->assertEquals(2, ApiToken::count()); 122 | $this->assertEquals(1, $user->apiTokens()->count()); 123 | $this->assertEquals($tokenOne, $user->apiTokens()->first()->token); 124 | 125 | $request = $this->getJson('multiple-tokens-auth/test-logout', ['Authorization' => 'Bearer ' . $tokenTwo]); 126 | $request->assertUnauthorized(); 127 | } 128 | 129 | /** @test It can logout with hash */ 130 | public function it_can_logout_with_hash() 131 | { 132 | config()->set('auth.guards.api.hash', true); 133 | 134 | Route::get('multiple-tokens-auth/test-logout', function () { 135 | auth()->logout(); 136 | return null; 137 | })->middleware('auth:api'); 138 | 139 | $user = factory(User::class)->create(); 140 | $token = $user->generateApiToken(); 141 | 142 | $this->assertEquals(1, ApiToken::count()); 143 | $this->assertEquals(1, $user->apiTokens()->count()); 144 | 145 | $request = $this->getJson('multiple-tokens-auth/test-logout', ['Authorization' => 'Bearer ' . $token]); 146 | $request->assertSuccessful(); 147 | 148 | $this->assertEquals(0, ApiToken::count()); 149 | $this->assertEquals(0, $user->apiTokens()->count()); 150 | } 151 | 152 | /** @test Logging out without a token doesnt delete any token */ 153 | public function logging_out_without_a_token_doesnt_delete_any_token() 154 | { 155 | $user = factory(User::class)->create(); 156 | $user->generateApiToken(); 157 | 158 | $this->assertEquals(1, ApiToken::count()); 159 | 160 | auth()->guard('api')->logout(); 161 | 162 | $this->assertEquals(1, ApiToken::count()); 163 | } 164 | 165 | /** @test Using a token that should is about to expire, updates its expiration date */ 166 | public function using_a_token_that_should_is_about_to_expire_updates_its_expiration_date() 167 | { 168 | Route::get('multiple-tokens-auth/test-update-expired_at', function () { 169 | return null; 170 | })->middleware('auth:api'); 171 | 172 | $user = factory(User::class)->create(); 173 | $token = factory(ApiToken::class)->create(['user_id' => $user->id, 'expired_at' => now()->addDay()]); 174 | 175 | $request = $this->getJson('multiple-tokens-auth/test-update-expired_at', ['Authorization' => 'Bearer ' . $token->token]); 176 | $request->assertSuccessful(); 177 | 178 | $this->assertTrue( 179 | $token->fresh()->expired_at->isSameDay(now()->addDays(config('multiple-tokens-auth.token.life_length'))) 180 | ); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/PurgeExpiredApiTokensJobTest.php: -------------------------------------------------------------------------------- 1 | create(['expired_at' => now()->subDay()]); 13 | $token = factory(ApiToken::class)->create(['expired_at' => now()->addDay()]); 14 | 15 | $this->assertEquals(2, ApiToken::count()); 16 | 17 | (new PurgeExpiredApiTokensJob())->handle(); 18 | 19 | $this->assertEquals(1, ApiToken::count()); 20 | $this->assertTrue(ApiToken::first()->is($token)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('auth.guards.api.driver', 'multiple-tokens'); 14 | config()->set('auth.providers.users.model', User::class); 15 | 16 | $this->loadLaravelMigrations(); 17 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 18 | $this->artisan('migrate'); 19 | 20 | $this->withFactories(__DIR__ . '/../database/factories'); 21 | } 22 | 23 | protected function getEnvironmentSetUp($app) 24 | { 25 | $app['config']->set('database.default', 'sqlite'); 26 | $app['config']->set('database.connections.sqlite', [ 27 | 'driver' => 'sqlite', 28 | 'database' => ':memory:', 29 | 'prefix' => '', 30 | ]); 31 | } 32 | 33 | protected function getPackageProviders($app) 34 | { 35 | return [ 36 | MultipleTokensAuthServiceProvider::class, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/User.php: -------------------------------------------------------------------------------- 1 |