diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa5a67..e3c625f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.1] + +- Redact log response for exchange and refresh methods. + ## [1.0.0] - **Breaking:** rename the verify result field `newIdTokenResponse` to `invalidTokenResponse`, matching the `exchangeUsingTokenExchange` and `adminGraphQLRequest` parameters. Update any code that reads this field: diff --git a/src/Internal/Exchange/ClientCredentials.php b/src/Internal/Exchange/ClientCredentials.php index 4f367c3..bb5458f 100644 --- a/src/Internal/Exchange/ClientCredentials.php +++ b/src/Internal/Exchange/ClientCredentials.php @@ -101,7 +101,9 @@ public static function exchange( $responseBody = (string) $response->getBody(); $responseData = json_decode($responseBody, true); - // Build response object for logging + // Build response object for logging. The token endpoint response + // carries the newly issued access token, so redact it before this + // goes into a log. $responseHeaders = []; foreach ($response->getHeaders() as $name => $values) { $responseHeaders[$name] = implode(', ', $values); @@ -109,7 +111,7 @@ public static function exchange( $resObj = [ 'status' => $statusCode, 'headers' => empty($responseHeaders) ? (object)[] : $responseHeaders, - 'body' => $responseBody + 'body' => \Shopify\App\Internal\Utils\Request::redactResponseBodyForLog($responseBody) ]; // Handle 200 success diff --git a/src/Internal/Exchange/TokenExchange.php b/src/Internal/Exchange/TokenExchange.php index bb1e577..cf3e6ed 100644 --- a/src/Internal/Exchange/TokenExchange.php +++ b/src/Internal/Exchange/TokenExchange.php @@ -289,7 +289,9 @@ public static function exchange( $responseBody = (string) $response->getBody(); $responseData = json_decode($responseBody, true); - // Build response object for logging + // Build response object for logging. The token endpoint + // response carries the newly issued credentials, so redact them + // before this goes into a log. $responseHeaders = []; foreach ($response->getHeaders() as $name => $values) { $responseHeaders[$name] = implode(', ', $values); @@ -297,7 +299,7 @@ public static function exchange( $resObj = [ 'status' => $statusCode, 'headers' => empty($responseHeaders) ? (object)[] : $responseHeaders, - 'body' => $responseBody + 'body' => \Shopify\App\Internal\Utils\Request::redactResponseBodyForLog($responseBody) ]; // Handle 200 success diff --git a/src/Internal/Utils/Request.php b/src/Internal/Utils/Request.php index 44eae94..bdecfe9 100644 --- a/src/Internal/Utils/Request.php +++ b/src/Internal/Utils/Request.php @@ -11,7 +11,22 @@ class Request { private const REDACTED = '[REDACTED]'; - private const SENSITIVE_BODY_FIELDS = ['client_secret', 'subject_token', 'refresh_token']; + private const SENSITIVE_BODY_FIELDS = [ + 'client_secret', + 'subject_token', + 'refresh_token', + 'access_token', + ]; + /** + * Fields an OAuth token endpoint returns that are reusable credentials. A + * debug log is a lower-trust sink than the app runtime, so these must never + * reach it. + */ + private const SENSITIVE_RESPONSE_BODY_FIELDS = [ + 'access_token', + 'refresh_token', + 'client_secret', + ]; private const SENSITIVE_HEADER_FIELDS = [ 'x-shopify-access-token', 'authorization', @@ -68,6 +83,47 @@ public static function redactForLog(array $req): array return $result; } + /** + * Redact issued OAuth credentials from a response body before logging it. + * + * A 200 from the OAuth token endpoint carries the access_token and + * refresh_token that were just issued. Both are reusable credentials, so + * logging the response verbatim would let anyone with log access replay + * them. Every other field is preserved so the log stays useful for + * debugging. + * + * The body is only re-encoded when something was actually redacted, so a + * body that carries no credentials keeps its original formatting. + * + * @param string $body The raw response body + * @return string The response body safe for logging + */ + public static function redactResponseBodyForLog(string $body): string + { + $parsed = json_decode($body, true); + + if (!is_array($parsed)) { + return $body; + } + + $modified = false; + foreach (self::SENSITIVE_RESPONSE_BODY_FIELDS as $field) { + if (array_key_exists($field, $parsed)) { + $parsed[$field] = self::REDACTED; + $modified = true; + } + } + + if (!$modified) { + return $body; + } + + $encoded = json_encode($parsed, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + // Fail closed: never fall back to the unredacted body. + return $encoded === false ? self::REDACTED : $encoded; + } + /** * Redact sensitive query parameter values in a URL string. * diff --git a/src/Version.php b/src/Version.php index ac65f9b..b6827ab 100644 --- a/src/Version.php +++ b/src/Version.php @@ -4,4 +4,4 @@ namespace Shopify\App; -const VERSION = '1.0.0'; +const VERSION = '1.0.1';