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 `new_id_token_response` to `invalid_token_response`, matching the `exchange_using_token_exchange` and `admin_graphql_request` parameters. Update any code that reads this field:
Expand Down
2 changes: 1 addition & 1 deletion shopify_app/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

from __future__ import annotations

__version__ = "1.0.0"
__version__ = "1.0.1"
14 changes: 11 additions & 3 deletions shopify_app/exchange/client_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
RequestInput,
Res,
)
from ..utils import _get_user_agent, redact_http_log
from ..utils import _get_user_agent, redact_http_log, redact_http_response_body
from ..utils.http_client import AsyncHTTPClientContext, HTTPClientContext
from ._response_builders import build_network_error_response
from ._validation import validate_shop
Expand Down Expand Up @@ -86,8 +86,12 @@ def exchange_using_client_credentials(
response_headers = (
dict(response.headers) if hasattr(response, "headers") else {}
)
# The token endpoint response carries the newly issued
# credentials, so redact them before this goes into a log.
res_obj = Res(
status=status_code, body=response_body, headers=response_headers
status=status_code,
body=redact_http_response_body(response_body),
headers=response_headers,
)

# Handle 200 success
Expand Down Expand Up @@ -353,8 +357,12 @@ async def exchange_using_client_credentials_async(
response_headers = (
dict(response.headers) if hasattr(response, "headers") else {}
)
# The token endpoint response carries the newly issued
# credentials, so redact them before this goes into a log.
res_obj = Res(
status=status_code, body=response_body, headers=response_headers
status=status_code,
body=redact_http_response_body(response_body),
headers=response_headers,
)

# Handle 200 success
Expand Down
20 changes: 17 additions & 3 deletions shopify_app/exchange/token_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
TokenExchangeResult,
User,
)
from ..utils import _get_attr, _get_user_agent, _to_res, redact_http_log
from ..utils import (
_get_attr,
_get_user_agent,
_to_res,
redact_http_log,
redact_http_response_body,
)
from ..utils.http_client import AsyncHTTPClientContext, HTTPClientContext
from ._response_builders import build_network_error_response
from ._validation import validate_client_id
Expand Down Expand Up @@ -143,8 +149,12 @@ def token_exchange(
response_headers = (
dict(response.headers) if hasattr(response, "headers") else {}
)
# The token endpoint response carries the newly issued
# credentials, so redact them before this goes into a log.
res_obj = Res(
status=status_code, body=response_body, headers=response_headers
status=status_code,
body=redact_http_response_body(response_body),
headers=response_headers,
)

# Handle 200 success
Expand Down Expand Up @@ -790,8 +800,12 @@ async def token_exchange_async(
response_headers = (
dict(response.headers) if hasattr(response, "headers") else {}
)
# The token endpoint response carries the newly issued
# credentials, so redact them before this goes into a log.
res_obj = Res(
status=status_code, body=response_body, headers=response_headers
status=status_code,
body=redact_http_response_body(response_body),
headers=response_headers,
)

# Handle 200 success
Expand Down
3 changes: 2 additions & 1 deletion shopify_app/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .headers import _normalize_headers
from .input_converters import _get_attr, _to_res
from .redact import redact_http_log
from .redact import redact_http_log, redact_http_response_body
from .user_agent import _get_user_agent

__all__ = [
Expand All @@ -13,4 +13,5 @@
"_get_attr",
"_to_res",
"redact_http_log",
"redact_http_response_body",
]
51 changes: 49 additions & 2 deletions shopify_app/utils/redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@
from ..types import RequestInput

_REDACTED = "[REDACTED]"
_SENSITIVE_BODY_FIELDS = {"client_secret", "subject_token", "refresh_token"}
_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.
_SENSITIVE_RESPONSE_BODY_FIELDS = {
"access_token",
"refresh_token",
"client_secret",
}
_SENSITIVE_HEADER_FIELDS = {
"x-shopify-access-token",
"authorization",
Expand All @@ -35,7 +47,8 @@ def redact_http_log(req: RequestInput) -> RequestInput:
"""Redact sensitive values from a req object before including it in HTTP logs.

Redacts:
- Request body fields: client_secret, subject_token, refresh_token
- Request body fields: client_secret, subject_token, refresh_token,
access_token
- Request headers: X-Shopify-Access-Token, Authorization (case-insensitive)
- Request URL params: signature, id_token, hmac (case-insensitive)
"""
Expand Down Expand Up @@ -63,3 +76,37 @@ def redact_http_log(req: RequestInput) -> RequestInput:
pass

return cast(RequestInput, result)


def redact_http_response_body(body: str) -> str:
"""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-serialized when something was actually redacted, so a
body that carries no credentials keeps its original formatting.
"""
if not isinstance(body, str):
return body

try:
parsed = json.loads(body)
except (json.JSONDecodeError, TypeError):
return body

if not isinstance(parsed, dict):
return body

modified = False
for field in _SENSITIVE_RESPONSE_BODY_FIELDS:
if field in parsed:
parsed[field] = _REDACTED
modified = True

if not modified:
return body

return json.dumps(parsed, separators=(",", ":"), ensure_ascii=False)
Loading