diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php index 3ce0002..5e35c16 100644 --- a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -20,6 +20,7 @@ public function up() $table->string('token', 64)->unique(); $table->text('abilities')->nullable(); $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); $table->timestamps(); }); } diff --git a/src/Guard.php b/src/Guard.php index 3884451..3b70e85 100644 --- a/src/Guard.php +++ b/src/Guard.php @@ -65,6 +65,8 @@ public function __invoke(Request $request) if (! $accessToken || ($this->expiration && $accessToken->created_at->lte(now()->subMinutes($this->expiration))) || + ($accessToken->expires_at && + $accessToken->expires_at->isPast()) || ! $this->hasValidProvider($accessToken->tokenable)) { return; } diff --git a/src/HasApiTokens.php b/src/HasApiTokens.php index 0b38629..b986834 100644 --- a/src/HasApiTokens.php +++ b/src/HasApiTokens.php @@ -2,6 +2,7 @@ namespace Laravel\Sanctum; +use Illuminate\Support\Carbon; use Illuminate\Support\Str; trait HasApiTokens @@ -39,14 +40,16 @@ public function tokenCan(string $ability) * * @param string $name * @param array $abilities + * @param Carbon|null $expires_at * @return \Laravel\Sanctum\NewAccessToken */ - public function createToken(string $name, array $abilities = ['*']) + public function createToken(string $name, array $abilities = ['*'], Carbon $expires_at = null) { $token = $this->tokens()->create([ 'name' => $name, 'token' => hash('sha256', $plainTextToken = Str::random(40)), 'abilities' => $abilities, + 'expires_at' => $expires_at, ]); return new NewAccessToken($token, $token->id.'|'.$plainTextToken); diff --git a/src/PersonalAccessToken.php b/src/PersonalAccessToken.php index 0d5df21..c9aa891 100644 --- a/src/PersonalAccessToken.php +++ b/src/PersonalAccessToken.php @@ -15,6 +15,7 @@ class PersonalAccessToken extends Model implements HasAbilities protected $casts = [ 'abilities' => 'json', 'last_used_at' => 'datetime', + 'expires_at' => 'datetime', ]; /** @@ -26,6 +27,7 @@ class PersonalAccessToken extends Model implements HasAbilities 'name', 'token', 'abilities', + 'expires_at', ]; /** diff --git a/tests/GuardTest.php b/tests/GuardTest.php index ae59345..081c8f2 100644 --- a/tests/GuardTest.php +++ b/tests/GuardTest.php @@ -120,6 +120,86 @@ public function test_authentication_with_token_fails_if_expired() $this->assertNull($user); } + public function test_authentication_with_token_fails_if_expires_at_has_passed() + { + $this->loadLaravelMigrations(['--database' => 'testbench']); + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $factory = Mockery::mock(AuthFactory::class); + + $guard = new Guard($factory, null, 'users'); + + $webGuard = Mockery::mock(stdClass::class); + + $factory->shouldReceive('guard') + ->with('web') + ->andReturn($webGuard); + + $webGuard->shouldReceive('user')->once()->andReturn(null); + + $request = Request::create('/', 'GET'); + $request->headers->set('Authorization', 'Bearer test'); + + $user = User::forceCreate([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + 'remember_token' => Str::random(10), + ]); + + $token = PersonalAccessToken::forceCreate([ + 'tokenable_id' => $user->id, + 'tokenable_type' => get_class($user), + 'name' => 'Test', + 'token' => hash('sha256', 'test'), + 'expires_at' => now()->subMinutes(60), + ]); + + $user = $guard->__invoke($request); + + $this->assertNull($user); + } + + public function test_authentication_with_token_succeeds_if_expires_at_not_passed() + { + $this->loadLaravelMigrations(['--database' => 'testbench']); + $this->artisan('migrate', ['--database' => 'testbench'])->run(); + + $factory = Mockery::mock(AuthFactory::class); + + $guard = new Guard($factory, null, 'users'); + + $webGuard = Mockery::mock(stdClass::class); + + $factory->shouldReceive('guard') + ->with('web') + ->andReturn($webGuard); + + $webGuard->shouldReceive('user')->once()->andReturn(null); + + $request = Request::create('/', 'GET'); + $request->headers->set('Authorization', 'Bearer test'); + + $user = User::forceCreate([ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + 'remember_token' => Str::random(10), + ]); + + $token = PersonalAccessToken::forceCreate([ + 'tokenable_id' => $user->id, + 'tokenable_type' => get_class($user), + 'name' => 'Test', + 'token' => hash('sha256', 'test'), + 'expires_at' => now()->addMinutes(60), + ]); + + $user = $guard->__invoke($request); + + $this->assertNull($user); + } + public function test_authentication_is_successful_with_token_if_no_session_present() { $this->loadLaravelMigrations(['--database' => 'testbench']); diff --git a/tests/HasApiTokensTest.php b/tests/HasApiTokensTest.php index 11e7cf6..517cc92 100644 --- a/tests/HasApiTokensTest.php +++ b/tests/HasApiTokensTest.php @@ -2,18 +2,20 @@ namespace Laravel\Sanctum\Tests; +use Illuminate\Support\Carbon; use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\PersonalAccessToken; use Laravel\Sanctum\TransientToken; -use PHPUnit\Framework\TestCase; +use Orchestra\Testbench\TestCase; class HasApiTokensTest extends TestCase { public function test_tokens_can_be_created() { $class = new ClassThatHasApiTokens; + $time = Carbon::now(); - $newToken = $class->createToken('test', ['foo']); + $newToken = $class->createToken('test', ['foo'], $time); [$id, $token] = explode('|', $newToken->plainTextToken); @@ -26,6 +28,11 @@ public function test_tokens_can_be_created() $newToken->accessToken->id, $id ); + + $this->assertEquals( + $time->toDateTimeString(), + $newToken->accessToken->expires_at->toDateTimeString() + ); } public function test_can_check_token_abilities()