From c720c1bd55cc19d83113b37bc311fbd86a5c7278 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 1 Oct 2024 02:00:24 +0330 Subject: [PATCH 1/6] new revoke refresh token config --- src/Passport.php | 5 +++++ src/PassportServiceProvider.php | 1 + 2 files changed, 6 insertions(+) diff --git a/src/Passport.php b/src/Passport.php index 503e650f..9b9c4b62 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -22,6 +22,11 @@ class Passport */ public static bool $validateKeyPermissions = false; + /** + * Indicates if the previous refresh token should be revoked and a new refresh token should be issued when refreshing access tokens. + */ + public static bool $revokeRefreshTokens = true; + /** * Indicates if the implicit grant type is enabled. */ diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index d2abfd45..c4f8d61f 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -120,6 +120,7 @@ protected function registerAuthorizationServer(): void $this->app->singleton(AuthorizationServer::class, function () { return tap($this->makeAuthorizationServer(), function (AuthorizationServer $server) { $server->setDefaultScope(Passport::$defaultScope); + $server->revokeRefreshTokens(Passport::$revokeRefreshTokens); $server->enableGrantType( $this->makeAuthCodeGrant(), Passport::tokensExpireIn() From 389a396b93a066898725947203255f6edbfcf5ad Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 1 Oct 2024 02:03:42 +0330 Subject: [PATCH 2/6] add feature tests --- tests/Feature/RefreshTokenGrantTest.php | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/Feature/RefreshTokenGrantTest.php diff --git a/tests/Feature/RefreshTokenGrantTest.php b/tests/Feature/RefreshTokenGrantTest.php new file mode 100644 index 00000000..0076cc52 --- /dev/null +++ b/tests/Feature/RefreshTokenGrantTest.php @@ -0,0 +1,71 @@ + $params); + } + + public function testRefreshingToken() + { + $this->assertArrayHasKey('refresh_token', $this->refreshToken()); + } + + public function testRefreshingTokenWithoutRevoking() + { + Passport::$revokeRefreshTokens = false; + + $this->assertArrayNotHasKey('refresh_token', $this->refreshToken()); + } + + private function refreshToken() + { + $client = ClientFactory::new()->create(); + + $this->actingAs(UserFactory::new()->create(), 'web'); + + $json = $this->get('/oauth/authorize?'.http_build_query([ + 'client_id' => $client->getKey(), + 'redirect_uri' => $redirect = $client->redirect_uris[0], + 'response_type' => 'code', + ]))->json(); + + $redirectUrl = $this->post('/oauth/authorize', ['auth_token' => $json['authToken']])->headers->get('Location'); + parse_str(parse_url($redirectUrl, PHP_URL_QUERY), $params); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'authorization_code', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'redirect_uri' => $redirect, + 'code' => $params['code'], + ])->json(); + + $response = $this->post('/oauth/token', [ + 'grant_type' => 'refresh_token', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'refresh_token' => $json['refresh_token'], + ]); + + $response->assertOk(); + $json = $response->json(); + $this->assertArrayHasKey('access_token', $json); + $this->assertSame(31536000, $json['expires_in']); + $this->assertSame('Bearer', $json['token_type']); + + return $json; + } +} From 261a4a384e7d6999f8e0198ce82e3e80174397c7 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 1 Oct 2024 12:59:21 +0330 Subject: [PATCH 3/6] add tests --- tests/Feature/RefreshTokenGrantTest.php | 61 ++++++++++++++++++------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/tests/Feature/RefreshTokenGrantTest.php b/tests/Feature/RefreshTokenGrantTest.php index 0076cc52..672981c8 100644 --- a/tests/Feature/RefreshTokenGrantTest.php +++ b/tests/Feature/RefreshTokenGrantTest.php @@ -2,6 +2,8 @@ namespace Laravel\Passport\Tests\Feature; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; use Laravel\Passport\Database\Factories\ClientFactory; use Laravel\Passport\Passport; use Orchestra\Testbench\Concerns\WithLaravelMigrations; @@ -20,14 +22,27 @@ protected function setUp(): void public function testRefreshingToken() { - $this->assertArrayHasKey('refresh_token', $this->refreshToken()); + $json = $this->refreshToken() + ->assertStatus(400) + ->json(); + + $this->assertSame('invalid_grant', $json['error']); + $this->assertSame('The refresh token is invalid.', $json['error_description']); + $this->assertSame('Token has been revoked', $json['hint']); } public function testRefreshingTokenWithoutRevoking() { Passport::$revokeRefreshTokens = false; - $this->assertArrayNotHasKey('refresh_token', $this->refreshToken()); + $json = $this->refreshToken() + ->assertOk() + ->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayHasKey('refresh_token', $json); + $this->assertSame(31536000, $json['expires_in']); + $this->assertSame('Bearer', $json['token_type']); } private function refreshToken() @@ -36,36 +51,50 @@ private function refreshToken() $this->actingAs(UserFactory::new()->create(), 'web'); - $json = $this->get('/oauth/authorize?'.http_build_query([ + $authToken = $this->get('/oauth/authorize?'.http_build_query([ 'client_id' => $client->getKey(), 'redirect_uri' => $redirect = $client->redirect_uris[0], 'response_type' => 'code', - ]))->json(); + ]))->json('authToken'); - $redirectUrl = $this->post('/oauth/authorize', ['auth_token' => $json['authToken']])->headers->get('Location'); + $redirectUrl = $this->post('/oauth/authorize', ['auth_token' => $authToken])->headers->get('Location'); parse_str(parse_url($redirectUrl, PHP_URL_QUERY), $params); - $json = $this->post('/oauth/token', [ + $oldToken = $this->post('/oauth/token', [ 'grant_type' => 'authorization_code', 'client_id' => $client->getKey(), 'client_secret' => $client->plainSecret, 'redirect_uri' => $redirect, 'code' => $params['code'], - ])->json(); + ])->assertOK()->json(); - $response = $this->post('/oauth/token', [ + $newToken = $this->post('/oauth/token', [ 'grant_type' => 'refresh_token', 'client_id' => $client->getKey(), 'client_secret' => $client->plainSecret, - 'refresh_token' => $json['refresh_token'], - ]); + 'refresh_token' => $oldToken['refresh_token'], + ])->assertOK()->json(); - $response->assertOk(); - $json = $response->json(); - $this->assertArrayHasKey('access_token', $json); - $this->assertSame(31536000, $json['expires_in']); - $this->assertSame('Bearer', $json['token_type']); + $this->assertArrayHasKey('access_token', $newToken); + $this->assertArrayHasKey('refresh_token', $newToken); + $this->assertSame(31536000, $newToken['expires_in']); + $this->assertSame('Bearer', $newToken['token_type']); + + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson())->middleware('auth:api'); + + $this->getJson('/foo', [ + 'Authorization' => $oldToken['token_type'].' '.$oldToken['access_token'] + ])->assertUnauthorized(); + + $this->getJson('/foo', [ + 'Authorization' => $newToken['token_type'].' '.$newToken['access_token'] + ])->assertOk(); - return $json; + return $this->post('/oauth/token', [ + 'grant_type' => 'refresh_token', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'refresh_token' => $oldToken['refresh_token'], + ]); } } From 928206b74a48d3e2baf43b056daa1c90c98e4b9b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 1 Oct 2024 13:01:33 +0330 Subject: [PATCH 4/6] formatting --- tests/Feature/RefreshTokenGrantTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/RefreshTokenGrantTest.php b/tests/Feature/RefreshTokenGrantTest.php index 672981c8..79c5764b 100644 --- a/tests/Feature/RefreshTokenGrantTest.php +++ b/tests/Feature/RefreshTokenGrantTest.php @@ -83,11 +83,11 @@ private function refreshToken() Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson())->middleware('auth:api'); $this->getJson('/foo', [ - 'Authorization' => $oldToken['token_type'].' '.$oldToken['access_token'] + 'Authorization' => $oldToken['token_type'].' '.$oldToken['access_token'], ])->assertUnauthorized(); $this->getJson('/foo', [ - 'Authorization' => $newToken['token_type'].' '.$newToken['access_token'] + 'Authorization' => $newToken['token_type'].' '.$newToken['access_token'], ])->assertOk(); return $this->post('/oauth/token', [ From 168e003760b081d477c4d59a811631945d2c1ecb Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 2 Oct 2024 12:13:45 +0330 Subject: [PATCH 5/6] formatting --- src/Passport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Passport.php b/src/Passport.php index 9b9c4b62..746b357a 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -23,7 +23,7 @@ class Passport public static bool $validateKeyPermissions = false; /** - * Indicates if the previous refresh token should be revoked and a new refresh token should be issued when refreshing access tokens. + * Indicates if the refresh token being used should be revoked when refreshing an access token. */ public static bool $revokeRefreshTokens = true; From afc24b278046bce26a0faf37948fe7165f754ae5 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 15 Oct 2024 21:13:38 +0330 Subject: [PATCH 6/6] add more tests --- .../AuthorizationCodeGrantWithPkceTest.php | 14 ++ tests/Feature/RefreshTokenGrantTest.php | 141 +++++++++++++----- 2 files changed, 117 insertions(+), 38 deletions(-) diff --git a/tests/Feature/AuthorizationCodeGrantWithPkceTest.php b/tests/Feature/AuthorizationCodeGrantWithPkceTest.php index 559a4084..16352f42 100644 --- a/tests/Feature/AuthorizationCodeGrantWithPkceTest.php +++ b/tests/Feature/AuthorizationCodeGrantWithPkceTest.php @@ -83,6 +83,8 @@ public function testIssueAccessToken() $this->assertSame('Bearer', $json['token_type']); $this->assertSame(31536000, $json['expires_in']); + $refreshToken = $json['refresh_token']; + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) ->middleware('auth:api'); @@ -91,6 +93,18 @@ public function testIssueAccessToken() $this->assertSame($client->getKey(), $json['oauth_client_id']); $this->assertEquals($user->getAuthIdentifier(), $json['oauth_user_id']); $this->assertSame(['create', 'read'], $json['oauth_scopes']); + + $newToken = $this->post('/oauth/token', [ + 'grant_type' => 'refresh_token', + 'client_id' => $client->getKey(), + 'refresh_token' => $refreshToken, + 'scope' => 'create read', + ])->assertOK()->json(); + + $this->assertArrayHasKey('access_token', $newToken); + $this->assertArrayHasKey('refresh_token', $newToken); + $this->assertSame(31536000, $newToken['expires_in']); + $this->assertSame('Bearer', $newToken['token_type']); } public function testRequireCodeChallenge() diff --git a/tests/Feature/RefreshTokenGrantTest.php b/tests/Feature/RefreshTokenGrantTest.php index 79c5764b..31140aef 100644 --- a/tests/Feature/RefreshTokenGrantTest.php +++ b/tests/Feature/RefreshTokenGrantTest.php @@ -4,6 +4,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; +use Laravel\Passport\Client; use Laravel\Passport\Database\Factories\ClientFactory; use Laravel\Passport\Passport; use Orchestra\Testbench\Concerns\WithLaravelMigrations; @@ -15,16 +16,58 @@ class RefreshTokenGrantTest extends PassportTestCase protected function setUp(): void { - PassportTestCase::setUp(); + parent::setUp(); + + Passport::tokensCan([ + 'create' => 'Create', + 'read' => 'Read', + 'update' => 'Update', + 'delete' => 'Delete', + ]); + + Passport::$revokeRefreshTokens = true; Passport::authorizationView(fn ($params) => $params); } public function testRefreshingToken() { - $json = $this->refreshToken() - ->assertStatus(400) - ->json(); + $client = ClientFactory::new()->create(); + + $oldToken = $this->getNewAccessToken($client); + + $newToken = $this->post('/oauth/token', [ + 'grant_type' => 'refresh_token', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'refresh_token' => $oldToken['refresh_token'], + 'scope' => 'read delete', + ])->assertOK()->json(); + + $this->assertArrayHasKey('access_token', $newToken); + $this->assertArrayHasKey('refresh_token', $newToken); + $this->assertSame(31536000, $newToken['expires_in']); + $this->assertSame('Bearer', $newToken['token_type']); + + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + ->middleware('auth:api'); + + $this->getJson('/foo', [ + 'Authorization' => $oldToken['token_type'].' '.$oldToken['access_token'], + ])->assertUnauthorized(); + + $json = $this->getJson('/foo', [ + 'Authorization' => $newToken['token_type'].' '.$newToken['access_token'], + ])->assertOk()->json(); + + $this->assertSame(['read', 'delete'], $json['oauth_scopes']); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'refresh_token', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'refresh_token' => $oldToken['refresh_token'], + ])->assertStatus(400)->json(); $this->assertSame('invalid_grant', $json['error']); $this->assertSame('The refresh token is invalid.', $json['error_description']); @@ -35,44 +78,16 @@ public function testRefreshingTokenWithoutRevoking() { Passport::$revokeRefreshTokens = false; - $json = $this->refreshToken() - ->assertOk() - ->json(); - - $this->assertArrayHasKey('access_token', $json); - $this->assertArrayHasKey('refresh_token', $json); - $this->assertSame(31536000, $json['expires_in']); - $this->assertSame('Bearer', $json['token_type']); - } - - private function refreshToken() - { $client = ClientFactory::new()->create(); - $this->actingAs(UserFactory::new()->create(), 'web'); - - $authToken = $this->get('/oauth/authorize?'.http_build_query([ - 'client_id' => $client->getKey(), - 'redirect_uri' => $redirect = $client->redirect_uris[0], - 'response_type' => 'code', - ]))->json('authToken'); - - $redirectUrl = $this->post('/oauth/authorize', ['auth_token' => $authToken])->headers->get('Location'); - parse_str(parse_url($redirectUrl, PHP_URL_QUERY), $params); - - $oldToken = $this->post('/oauth/token', [ - 'grant_type' => 'authorization_code', - 'client_id' => $client->getKey(), - 'client_secret' => $client->plainSecret, - 'redirect_uri' => $redirect, - 'code' => $params['code'], - ])->assertOK()->json(); + $oldToken = $this->getNewAccessToken($client); $newToken = $this->post('/oauth/token', [ 'grant_type' => 'refresh_token', 'client_id' => $client->getKey(), 'client_secret' => $client->plainSecret, 'refresh_token' => $oldToken['refresh_token'], + 'scope' => 'read delete', ])->assertOK()->json(); $this->assertArrayHasKey('access_token', $newToken); @@ -80,21 +95,71 @@ private function refreshToken() $this->assertSame(31536000, $newToken['expires_in']); $this->assertSame('Bearer', $newToken['token_type']); - Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson())->middleware('auth:api'); + Route::get('/foo', fn (Request $request) => $request->user()->token()->toJson()) + ->middleware('auth:api'); $this->getJson('/foo', [ 'Authorization' => $oldToken['token_type'].' '.$oldToken['access_token'], ])->assertUnauthorized(); - $this->getJson('/foo', [ + $json = $this->getJson('/foo', [ 'Authorization' => $newToken['token_type'].' '.$newToken['access_token'], ])->assertOk(); - return $this->post('/oauth/token', [ + $this->assertSame(['read', 'delete'], $json['oauth_scopes']); + + $json = $this->post('/oauth/token', [ 'grant_type' => 'refresh_token', 'client_id' => $client->getKey(), 'client_secret' => $client->plainSecret, 'refresh_token' => $oldToken['refresh_token'], - ]); + ])->assertOk()->json(); + + $this->assertArrayHasKey('access_token', $json); + $this->assertArrayHasKey('refresh_token', $json); + $this->assertSame(31536000, $json['expires_in']); + $this->assertSame('Bearer', $json['token_type']); + } + + public function testRefreshingTokenWithAdditionalScopes() + { + $client = ClientFactory::new()->create(); + + $oldToken = $this->getNewAccessToken($client); + + $json = $this->post('/oauth/token', [ + 'grant_type' => 'refresh_token', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'refresh_token' => $oldToken['refresh_token'], + 'scope' => 'create update', + ])->assertStatus(400)->json(); + + $this->assertSame('invalid_scope', $json['error']); + $this->assertSame('The requested scope is invalid, unknown, or malformed', $json['error_description']); + $this->assertSame('Check the `update` scope', $json['hint']); + } + + private function getNewAccessToken(Client $client) + { + $this->actingAs(UserFactory::new()->create(), 'web'); + + $authToken = $this->get('/oauth/authorize?'.http_build_query([ + 'client_id' => $client->getKey(), + 'redirect_uri' => $redirect = $client->redirect_uris[0], + 'response_type' => 'code', + 'scope' => 'create read delete', + ]))->assertOk()->json('authToken'); + + $redirectUrl = $this->post('/oauth/authorize', ['auth_token' => $authToken])->headers->get('Location'); + parse_str(parse_url($redirectUrl, PHP_URL_QUERY), $params); + + return $this->post('/oauth/token', [ + 'grant_type' => 'authorization_code', + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + 'redirect_uri' => $redirect, + 'code' => $params['code'], + ])->assertOK()->json(); } }