From 67115751f4424c06d18afbe7b13dd762dca3386e Mon Sep 17 00:00:00 2001 From: Edie Lemoine Date: Fri, 22 Nov 2024 13:29:50 +0100 Subject: [PATCH] feat(logging): obfuscate authorization header in logs --- src/Api/Service/AbstractApiService.php | 35 ++++++++++--- .../Api/Service/AbstractApiServiceTest.php | 51 +++++++++++++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/Api/Service/AbstractApiService.php b/src/Api/Service/AbstractApiService.php index bd064899c..c54a3cfd0 100644 --- a/src/Api/Service/AbstractApiService.php +++ b/src/Api/Service/AbstractApiService.php @@ -54,14 +54,7 @@ public function doRequest( 'body' => $request->getBody(), ]; - $logContext = [ - 'request' => [ - 'uri' => $uri, - 'method' => $method, - 'headers' => $options['headers'], - 'body' => $options['body'] ? json_decode($options['body'], true) : null, - ], - ]; + $logContext = $this->createLogContext($uri, $method, $options); try { $response = $this->clientAdapter->doRequest($method, $uri, $options); @@ -128,4 +121,30 @@ protected function buildUri(RequestInterface $request): string return $url; } + + /** + * @param string $uri + * @param string $method + * @param array $options + * + * @return array[] + */ + private function createLogContext(string $uri, string $method, array $options): array + { + $headers = array_combine(array_map('strtolower', array_keys($options['headers'])), $options['headers']); + + // Obfuscate the authorization header if present + if (isset($headers['authorization'])) { + $headers['authorization'] = '***'; + } + + return [ + 'request' => [ + 'uri' => $uri, + 'method' => $method, + 'headers' => $headers, + 'body' => $options['body'] ? json_decode($options['body'], true) : null, + ], + ]; + } } diff --git a/tests/Unit/Api/Service/AbstractApiServiceTest.php b/tests/Unit/Api/Service/AbstractApiServiceTest.php index 443843faa..bfb9725fb 100644 --- a/tests/Unit/Api/Service/AbstractApiServiceTest.php +++ b/tests/Unit/Api/Service/AbstractApiServiceTest.php @@ -6,9 +6,12 @@ namespace MyParcelNL\Pdk\Api\Service; use MyParcelNL\Pdk\Account\Repository\ShopRepository; +use MyParcelNL\Pdk\Api\Contract\ApiServiceInterface; use MyParcelNL\Pdk\Api\Exception\ApiException; +use MyParcelNL\Pdk\Api\Request\Request; use MyParcelNL\Pdk\Base\Support\Collection; use MyParcelNL\Pdk\Facade\Pdk; +use MyParcelNL\Pdk\Logger\Contract\PdkLoggerInterface; use MyParcelNL\Pdk\Shipment\Model\Shipment; use MyParcelNL\Pdk\Shipment\Repository\ShipmentRepository; use MyParcelNL\Pdk\Tests\Api\Response\ExampleErrorNotFoundResponse; @@ -16,7 +19,11 @@ use MyParcelNL\Pdk\Tests\Api\Response\ExampleErrorUnprocessableEntityResponse; use MyParcelNL\Pdk\Tests\Api\Response\ExampleGetShipmentsResponse; use MyParcelNL\Pdk\Tests\Bootstrap\MockApi; +use MyParcelNL\Pdk\Tests\Bootstrap\TestBootstrapper; +use MyParcelNL\Pdk\Tests\Mocks\MockApiResponse; use MyParcelNL\Pdk\Tests\Uses\UsesMockPdkInstance; +use MyParcelNL\Sdk\src\Support\Arr; +use Psr\Log\LogLevel; use function MyParcelNL\Pdk\Tests\usesShared; usesShared(new UsesMockPdkInstance()); @@ -49,3 +56,47 @@ ->and($shipments->first()) ->toBeInstanceOf(Shipment::class); }); + +it('creates log context with obfuscated authorization header', function () { + TestBootstrapper::hasApiKey(); + + MockApi::enqueue(new ExampleGetShipmentsResponse()); + + /** @var \MyParcelNL\Pdk\Tests\Bootstrap\MockLogger $logger */ + $logger = Pdk::get(PdkLoggerInterface::class); + + /** @var \MyParcelNL\Pdk\Api\Contract\ApiServiceInterface $apiService */ + $apiService = Pdk::get(ApiServiceInterface::class); + + $request = new Request([ + 'headers' => [ + 'Authorization' => 'bearer this-is-some-fake-value', + 'Content-Type' => 'application/json', + ], + 'uri' => 'test', + 'method' => 'POST', + 'parameters' => [], + 'body' => json_encode(['test' => 'test']), + ]); + + $apiService->doRequest($request, MockApiResponse::class); + + $lastLog = Arr::last($logger->getLogs()); + + expect($lastLog['level']) + ->toBe(LogLevel::DEBUG) + ->and($lastLog['message']) + ->toBe('[PDK]: Successfully sent request') + ->and(array_keys($lastLog['context'])) + ->toEqual(['request', 'response']) + ->and(array_keys($lastLog['context']['request'])) + ->toEqual(['uri', 'method', 'headers', 'body']) + ->and(array_keys($lastLog['context']['response'])) + ->toEqual(['code', 'body']) + // Expect header keys to be normalized and authorization header to be hidden + ->and($lastLog['context']['request']['headers']) + ->toBe([ + 'authorization' => '***', + 'content-type' => 'application/json', + ]); +});