Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions src/Internal/Exchange/ClientCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,17 @@ 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);
}
$resObj = [
'status' => $statusCode,
'headers' => empty($responseHeaders) ? (object)[] : $responseHeaders,
'body' => $responseBody
'body' => \Shopify\App\Internal\Utils\Request::redactResponseBodyForLog($responseBody)
];

// Handle 200 success
Expand Down
6 changes: 4 additions & 2 deletions src/Internal/Exchange/TokenExchange.php
Original file line number Diff line number Diff line change
Expand Up @@ -289,15 +289,17 @@ 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);
}
$resObj = [
'status' => $statusCode,
'headers' => empty($responseHeaders) ? (object)[] : $responseHeaders,
'body' => $responseBody
'body' => \Shopify\App\Internal\Utils\Request::redactResponseBodyForLog($responseBody)
];

// Handle 200 success
Expand Down
58 changes: 57 additions & 1 deletion src/Internal/Utils/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.
*
Expand Down
2 changes: 1 addition & 1 deletion src/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

namespace Shopify\App;

const VERSION = '1.0.0';
const VERSION = '1.0.1';
Loading