diff --git a/src/Client/OAuth2Client.php b/src/Client/OAuth2Client.php index dc31a98..53fc10f 100644 --- a/src/Client/OAuth2Client.php +++ b/src/Client/OAuth2Client.php @@ -236,8 +236,14 @@ public function revokeToken(string $token, string $tokenTypeHint = 'access_token 'client_id' => $this->clientId, ]; + $useBasic = false; if ($this->clientSecret !== null) { - $params['client_secret'] = $this->clientSecret; + $useBasic = in_array('client_secret_basic', $this->provider->tokenEndpointAuthMethodsSupported, true) + && !in_array('client_secret_post', $this->provider->tokenEndpointAuthMethodsSupported, true); + + if (!$useBasic) { + $params['client_secret'] = $this->clientSecret; + } } $body = $this->streamFactory->createStream(http_build_query($params)); @@ -247,6 +253,11 @@ public function revokeToken(string $token, string $tokenTypeHint = 'access_token ->withHeader('Accept', 'application/json') ->withBody($body); + if ($this->clientSecret !== null && $useBasic) { + $credentials = base64_encode(urlencode($this->clientId) . ':' . urlencode($this->clientSecret)); + $request = $request->withHeader('Authorization', 'Basic ' . $credentials); + } + $response = $this->httpClient->sendRequest($request); if ($response->getStatusCode() >= 400) { diff --git a/test/Client/OAuth2ClientTest.php b/test/Client/OAuth2ClientTest.php index 83d3c52..b12433a 100644 --- a/test/Client/OAuth2ClientTest.php +++ b/test/Client/OAuth2ClientTest.php @@ -40,6 +40,7 @@ protected function setUp(): void 'authorization_endpoint' => 'https://idp.example.org/authorize', 'token_endpoint' => 'https://idp.example.org/token', 'revocation_endpoint' => 'https://idp.example.org/revoke', + 'token_endpoint_auth_methods_supported' => ['client_secret_post'], ]); $this->stream = $this->createStub(StreamInterface::class); @@ -89,7 +90,7 @@ public function testRevokeTokenSendsPostToRevocationEndpoint(): void $this->makeClient($httpClient, $requestFactory)->revokeToken('my-access-token'); } - public function testRevokeTokenIncludesTokenInBody(): void + public function testRevokeTokenIncludesTokenInBodyWhenPostConfigured(): void { $requestFactory = $this->createStub(RequestFactoryInterface::class); $requestFactory->method('createRequest')->willReturn($this->request); @@ -237,4 +238,102 @@ public function testRevokeTokenThrowsOnErrorResponse(): void $this->expectException(OAuthException::class); $this->makeClient($httpClient, $requestFactory)->revokeToken('bad-token'); } + + public function testRevokeTokenUsesBasicAuthWhenConfigured(): void + { + $provider = ProviderConfig::fromArray([ + 'issuer' => 'https://idp.example.org', + 'authorization_endpoint' => 'https://idp.example.org/authorize', + 'token_endpoint' => 'https://idp.example.org/token', + 'revocation_endpoint' => 'https://idp.example.org/revoke', + 'token_endpoint_auth_methods_supported' => ['client_secret_basic'], + ]); + + $requestFactory = $this->createStub(RequestFactoryInterface::class); + $requestFactory->method('createRequest')->willReturn($this->request); + + $capturedBody = null; + $capturedAuth = null; + $this->request->method('withHeader') + ->willReturnCallback(function (string $name, string $value) use (&$capturedAuth): RequestInterface { + if ($name === 'Authorization') { + $capturedAuth = $value; + } + return $this->request; + }); + + $streamFactory = $this->createStub(StreamFactoryInterface::class); + $streamFactory->method('createStream') + ->willReturnCallback(function (string $body) use (&$capturedBody): StreamInterface { + $capturedBody = $body; + return $this->stream; + }); + + $response = $this->createStub(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + + $httpClient = $this->createStub(ClientInterface::class); + $httpClient->method('sendRequest')->willReturn($response); + + $client = new OAuth2Client( + provider: $provider, + clientId: 'test-client', + clientSecret: 'secret', + redirectUri: 'https://horde.example.org/callback', + httpClient: $httpClient, + requestFactory: $requestFactory, + streamFactory: $streamFactory, + ); + + $client->revokeToken('my-token'); + + self::assertStringNotContainsString('client_secret', $capturedBody); + self::assertStringStartsWith('Basic ', $capturedAuth); + self::assertSame( + 'Basic ' . base64_encode(urlencode('test-client') . ':' . urlencode('secret')), + $capturedAuth + ); + } + + public function testRevokeTokenUsesPostWhenBothMethodsSupported(): void + { + $provider = ProviderConfig::fromArray([ + 'issuer' => 'https://idp.example.org', + 'authorization_endpoint' => 'https://idp.example.org/authorize', + 'token_endpoint' => 'https://idp.example.org/token', + 'revocation_endpoint' => 'https://idp.example.org/revoke', + 'token_endpoint_auth_methods_supported' => ['client_secret_basic', 'client_secret_post'], + ]); + + $requestFactory = $this->createStub(RequestFactoryInterface::class); + $requestFactory->method('createRequest')->willReturn($this->request); + + $capturedBody = null; + $streamFactory = $this->createStub(StreamFactoryInterface::class); + $streamFactory->method('createStream') + ->willReturnCallback(function (string $body) use (&$capturedBody): StreamInterface { + $capturedBody = $body; + return $this->stream; + }); + + $response = $this->createStub(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + + $httpClient = $this->createStub(ClientInterface::class); + $httpClient->method('sendRequest')->willReturn($response); + + $client = new OAuth2Client( + provider: $provider, + clientId: 'test-client', + clientSecret: 'secret', + redirectUri: 'https://horde.example.org/callback', + httpClient: $httpClient, + requestFactory: $requestFactory, + streamFactory: $streamFactory, + ); + + $client->revokeToken('my-token'); + + self::assertStringContainsString('client_secret=secret', $capturedBody); + } }