From 8d6ff05381bb89f8bde767c447822f46e5b4ba3f Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 29 Sep 2025 13:17:23 +0330 Subject: [PATCH 1/4] register token server --- composer.json | 8 +++++++- src/PassportServiceProvider.php | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 31a53496..2d9bc6a5 100644 --- a/composer.json +++ b/composer.json @@ -27,13 +27,19 @@ "illuminate/encryption": "^11.35|^12.0", "illuminate/http": "^11.35|^12.0", "illuminate/support": "^11.35|^12.0", - "league/oauth2-server": "^9.2", + "league/oauth2-server": "dev-master-token-revocation-introspection", "php-http/discovery": "^1.20", "phpseclib/phpseclib": "^3.0", "psr/http-factory-implementation": "*", "symfony/console": "^7.1", "symfony/psr-http-message-bridge": "^7.1" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/hafezdivandari/oauth2-server" + } + ], "require-dev": { "mockery/mockery": "^1.6", "orchestra/testbench": "^9.9|^10.0", diff --git a/src/PassportServiceProvider.php b/src/PassportServiceProvider.php index 121f055f..d4d47e0e 100644 --- a/src/PassportServiceProvider.php +++ b/src/PassportServiceProvider.php @@ -32,6 +32,7 @@ use League\OAuth2\Server\Grant\RefreshTokenGrant; use League\OAuth2\Server\ResourceServer; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use League\OAuth2\Server\TokenServer; class PassportServiceProvider extends ServiceProvider { @@ -116,6 +117,7 @@ public function register(): void $this->registerResponseBindings(); $this->registerAuthorizationServer(); $this->registerResourceServer(); + $this->registerTokenServer(); $this->registerGuard(); } @@ -278,6 +280,20 @@ protected function registerResourceServer(): void )); } + /** + * Register the token server. + */ + protected function registerTokenServer(): void + { + $this->app->singleton(TokenServer::class, fn ($container) => new TokenServer( + $container->make(Bridge\ClientRepository::class), + $container->make(Bridge\AccessTokenRepository::class), + $container->make(Bridge\RefreshTokenRepository::class), + $this->makeCryptKey('public'), + Passport::tokenEncryptionKey($container->make('encrypter')) + )); + } + /** * Create a CryptKey instance. */ From e2e5b936d629949ddbe29fe9e833e85f25e47cba Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 29 Sep 2025 13:18:12 +0330 Subject: [PATCH 2/4] add token revocation --- routes/web.php | 8 ++ .../Controllers/RevokeTokenController.php | 33 +++++ src/Passport.php | 5 + tests/Feature/TokenRevocationTest.php | 129 ++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 src/Http/Controllers/RevokeTokenController.php create mode 100644 tests/Feature/TokenRevocationTest.php diff --git a/routes/web.php b/routes/web.php index c6f79b51..094dc080 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,6 +29,14 @@ ]); } +if (Passport::$tokenRevocationEnabled) { + Route::post('/revoke', [ + 'uses' => 'RevokeTokenController', + 'as' => 'revoke', + 'middleware' => 'throttle', + ]); +} + $guard = config('passport.guard', null); Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function () { diff --git a/src/Http/Controllers/RevokeTokenController.php b/src/Http/Controllers/RevokeTokenController.php new file mode 100644 index 00000000..e8a9057a --- /dev/null +++ b/src/Http/Controllers/RevokeTokenController.php @@ -0,0 +1,33 @@ +withErrorHandling( + fn () => $this->convertResponse( + $this->server->respondToTokenRevocationRequest($psrRequest, $psrResponse) + ) + ); + } +} diff --git a/src/Passport.php b/src/Passport.php index e261e78a..c9b1a564 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -45,6 +45,11 @@ class Passport */ public static bool $passwordGrantEnabled = false; + /** + * Indicates if the token revocation is enabled. + */ + public static bool $tokenRevocationEnabled = true; + /** * The default scope. */ diff --git a/tests/Feature/TokenRevocationTest.php b/tests/Feature/TokenRevocationTest.php new file mode 100644 index 00000000..68039545 --- /dev/null +++ b/tests/Feature/TokenRevocationTest.php @@ -0,0 +1,129 @@ + 'Create', + 'read' => 'Read', + 'update' => 'Update', + 'delete' => 'Delete', + ]); + + Passport::authorizationView(fn ($params) => $params); + } + + public function testRevokeAccessToken() + { + $client = ClientFactory::new()->create(); + + $token = $this->requestToken($client); + + $requestData = [ + 'token' => $token['access_token'], + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + ]; + + $this->assertTrue($this->post('/oauth/introspect', $requestData)->assertOk()->json('active')); + + $this->post('/oauth/revoke', $requestData)->assertOk(); + + $this->assertFalse($this->post('/oauth/introspect', $requestData)->assertOk()->json('active')); + + $requestData['token'] = $token['refresh_token']; + + $this->assertTrue($this->post('/oauth/introspect', $requestData)->assertOk()->json('active')); + } + + public function testRevokeRefreshToken() + { + $client = ClientFactory::new()->create(); + + $token = $this->requestToken($client); + + $requestData = [ + 'token' => $token['refresh_token'], + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + ]; + + $this->assertTrue($this->post('/oauth/introspect', $requestData)->assertOk()->json('active')); + + $this->post('/oauth/revoke', $requestData)->assertOk(); + + $this->assertFalse($this->post('/oauth/introspect', $requestData)->assertOk()->json('active')); + + $requestData['token'] = $token['access_token']; + + $this->assertFalse($this->post('/oauth/introspect', $requestData)->assertOk()->json('active')); + } + + public function testInvalidClient(): void + { + $client1 = ClientFactory::new()->create(); + $client2 = ClientFactory::new()->create(); + + $token = $this->requestToken($client1); + + $this->post('/oauth/revoke', [ + 'token' => $token['access_token'], + 'client_id' => $client2->getKey(), + 'client_secret' => $client2->plainSecret, + ])->assertOk(); + + $this->post('/oauth/revoke', [ + 'token' => $token['refresh_token'], + 'client_id' => $client2->getKey(), + 'client_secret' => $client2->plainSecret, + ])->assertOk(); + + $this->assertTrue($this->post('/oauth/introspect', [ + 'token' => $token['access_token'], + 'client_id' => $client1->getKey(), + 'client_secret' => $client1->plainSecret, + ])->assertOk()->json('active')); + + $this->assertTrue($this->post('/oauth/introspect', [ + 'token' => $token['refresh_token'], + 'client_id' => $client1->getKey(), + 'client_secret' => $client1->plainSecret, + ])->assertOk()->json('active')); + } + + private function requestToken(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(); + } +} From 6ced533dcc45dda21e3894cb22964265a9abe67c Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 29 Sep 2025 13:18:55 +0330 Subject: [PATCH 3/4] add token introspection --- routes/web.php | 8 ++ .../Controllers/IntrospectTokenController.php | 33 +++++ src/Passport.php | 5 + tests/Feature/TokenIntrospectionTest.php | 127 ++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 src/Http/Controllers/IntrospectTokenController.php create mode 100644 tests/Feature/TokenIntrospectionTest.php diff --git a/routes/web.php b/routes/web.php index 094dc080..09003f98 100644 --- a/routes/web.php +++ b/routes/web.php @@ -37,6 +37,14 @@ ]); } +if (Passport::$tokenIntrospectionEnabled) { + Route::post('/introspect', [ + 'uses' => 'IntrospectTokenController', + 'as' => 'introspect', + 'middleware' => 'throttle', + ]); +} + $guard = config('passport.guard', null); Route::middleware(['web', $guard ? 'auth:'.$guard : 'auth'])->group(function () { diff --git a/src/Http/Controllers/IntrospectTokenController.php b/src/Http/Controllers/IntrospectTokenController.php new file mode 100644 index 00000000..b3e3c4ec --- /dev/null +++ b/src/Http/Controllers/IntrospectTokenController.php @@ -0,0 +1,33 @@ +withErrorHandling( + fn () => $this->convertResponse( + $this->server->respondToTokenIntrospectionRequest($psrRequest, $psrResponse) + ) + ); + } +} diff --git a/src/Passport.php b/src/Passport.php index c9b1a564..9f54285c 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -45,6 +45,11 @@ class Passport */ public static bool $passwordGrantEnabled = false; + /** + * Indicates if the token introspection is enabled. + */ + public static bool $tokenIntrospectionEnabled = true; + /** * Indicates if the token revocation is enabled. */ diff --git a/tests/Feature/TokenIntrospectionTest.php b/tests/Feature/TokenIntrospectionTest.php new file mode 100644 index 00000000..d5f67f15 --- /dev/null +++ b/tests/Feature/TokenIntrospectionTest.php @@ -0,0 +1,127 @@ + 'Create', + 'read' => 'Read', + 'update' => 'Update', + 'delete' => 'Delete', + ]); + + Passport::authorizationView(fn ($params) => $params); + } + + public function testIntrospectToken() + { + $client = ClientFactory::new()->create(); + $user = UserFactory::new()->create(); + + $token = $this->requestToken($user, $client); + + $json = $this->post('/oauth/introspect', [ + 'token' => $token['access_token'], + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + ])->assertOk()->json(); + + $jwt = JWT::decode($token['access_token'], new Key(file_get_contents(self::PUBLIC_KEY), 'RS256')); + + $this->assertTrue($json['active']); + $this->assertSame('create read delete', $json['scope']); + $this->assertSame($client->getKey(), $json['client_id']); + $this->assertSame('Bearer', $json['token_type']); + $this->assertEquals($user->getAuthIdentifier(), $json['sub']); + $this->assertArrayHasKey('aud', $json); + $this->assertSame($jwt->jti, $json['jti']); + $this->assertSame((int) $jwt->exp, $json['exp']); + $this->assertSame((int) $jwt->iat, $json['iat']); + $this->assertSame((int) $jwt->nbf, $json['nbf']); + + $json = $this->post('/oauth/introspect', [ + 'token' => $token['refresh_token'], + 'client_id' => $client->getKey(), + 'client_secret' => $client->plainSecret, + ])->assertOk()->json(); + + $this->assertTrue($json['active']); + $this->assertSame('create read delete', $json['scope']); + $this->assertSame($client->getKey(), $json['client_id']); + $this->assertEquals($user->getAuthIdentifier(), $json['sub']); + $this->assertArrayHasKey('jti', $json); + $this->assertEqualsWithDelta(31536000, $json['exp'] - time(), 5); + } + + public function testInvalidClient(): void + { + $client1 = ClientFactory::new()->create(); + $client2 = ClientFactory::new()->create(); + $user = UserFactory::new()->create(); + + $token = $this->requestToken($user, $client1); + + $this->assertFalse($this->post('/oauth/introspect', [ + 'token' => $token['access_token'], + 'client_id' => $client2->getKey(), + 'client_secret' => $client2->plainSecret, + ])->assertOk()->json('active')); + + $this->assertTrue($this->post('/oauth/introspect', [ + 'token' => $token['access_token'], + 'client_id' => $client1->getKey(), + 'client_secret' => $client1->plainSecret, + ])->assertOk()->json('active')); + + $this->assertFalse($this->post('/oauth/introspect', [ + 'token' => $token['refresh_token'], + 'client_id' => $client2->getKey(), + 'client_secret' => $client2->plainSecret, + ])->assertOk()->json('active')); + + $this->assertTrue($this->post('/oauth/introspect', [ + 'token' => $token['refresh_token'], + 'client_id' => $client1->getKey(), + 'client_secret' => $client1->plainSecret, + ])->assertOk()->json('active')); + } + + private function requestToken(Authenticatable $user, Client $client) + { + $this->actingAs($user, '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(); + } +} From 445676dad685168aae76aa5c94214eb53372fdce Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 10 Oct 2025 23:29:26 +0330 Subject: [PATCH 4/4] disabled by default --- src/Passport.php | 20 ++++++++++++++++++-- tests/Feature/TokenIntrospectionTest.php | 2 ++ tests/Feature/TokenRevocationTest.php | 3 +++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Passport.php b/src/Passport.php index 9f54285c..f4931d09 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -48,12 +48,12 @@ class Passport /** * Indicates if the token introspection is enabled. */ - public static bool $tokenIntrospectionEnabled = true; + public static bool $tokenIntrospectionEnabled = false; /** * Indicates if the token revocation is enabled. */ - public static bool $tokenRevocationEnabled = true; + public static bool $tokenRevocationEnabled = false; /** * The default scope. @@ -201,6 +201,22 @@ public static function enablePasswordGrant(): void static::$passwordGrantEnabled = true; } + /** + * Enable the token introspection. + */ + public static function enableTokenIntrospection(): void + { + static::$tokenIntrospectionEnabled = true; + } + + /** + * Enable the token revocation. + */ + public static function enableTokenRevocation(): void + { + static::$tokenRevocationEnabled = true; + } + /** * Set the default scope(s). Multiple scopes may be an array or specified delimited by spaces. * diff --git a/tests/Feature/TokenIntrospectionTest.php b/tests/Feature/TokenIntrospectionTest.php index d5f67f15..3050ab3b 100644 --- a/tests/Feature/TokenIntrospectionTest.php +++ b/tests/Feature/TokenIntrospectionTest.php @@ -17,6 +17,8 @@ class TokenIntrospectionTest extends PassportTestCase protected function setUp(): void { + Passport::enableTokenIntrospection(); + parent::setUp(); Passport::tokensCan([ diff --git a/tests/Feature/TokenRevocationTest.php b/tests/Feature/TokenRevocationTest.php index 68039545..c86344ce 100644 --- a/tests/Feature/TokenRevocationTest.php +++ b/tests/Feature/TokenRevocationTest.php @@ -14,6 +14,9 @@ class TokenRevocationTest extends PassportTestCase protected function setUp(): void { + Passport::enableTokenRevocation(); + Passport::enableTokenIntrospection(); + parent::setUp(); Passport::tokensCan([