diff --git a/src/Passport.php b/src/Passport.php index 62984641..78099b1d 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -22,6 +22,11 @@ class Passport */ public static bool $validateKeyPermissions = false; + /** + * Indicates if the refresh token being used should be revoked when refreshing an access token. + */ + 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() 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 new file mode 100644 index 00000000..31140aef --- /dev/null +++ b/tests/Feature/RefreshTokenGrantTest.php @@ -0,0 +1,165 @@ + 'Create', + 'read' => 'Read', + 'update' => 'Update', + 'delete' => 'Delete', + ]); + + Passport::$revokeRefreshTokens = true; + + Passport::authorizationView(fn ($params) => $params); + } + + public function testRefreshingToken() + { + $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']); + $this->assertSame('Token has been revoked', $json['hint']); + } + + public function testRefreshingTokenWithoutRevoking() + { + Passport::$revokeRefreshTokens = false; + + $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(); + + $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(); + } +}