From 0f6311fe28ede564f8a651e1fcdc3f38ac977f23 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 11 Mar 2026 14:41:57 -0400 Subject: [PATCH] Create new Release. --- CHANGELOG.md | 4 ++ shopify_app/_version.py | 2 +- shopify_app/exchange/client_credentials.py | 16 +++-- shopify_app/exchange/refresh_token.py | 31 +++++---- shopify_app/exchange/token_exchange.py | 16 +++-- shopify_app/graphql/admin_graphql.py | 50 ++++++-------- .../helpers/app_home_parent_redirect.py | 14 ++-- .../helpers/app_home_patch_id_token.py | 12 ++-- shopify_app/helpers/app_home_redirect.py | 14 ++-- shopify_app/utils/__init__.py | 9 ++- shopify_app/utils/redact.py | 65 +++++++++++++++++++ shopify_app/verify/_body_hmac_in_header.py | 16 +++-- .../verify/_non_exchangeable_id_token.py | 18 ++--- shopify_app/verify/admin_ui_ext.py | 20 +++--- shopify_app/verify/app_home_req.py | 23 ++++--- shopify_app/verify/app_proxy.py | 16 +++-- shopify_app/verify/pos_ui_ext.py | 20 +++--- 17 files changed, 219 insertions(+), 127 deletions(-) create mode 100644 shopify_app/utils/redact.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a420c4b..7cf7f5b 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). +## [0.1.2] + +- Redact sensitive information in `log` and `http_logs` + ## [0.1.1] - Update encoding of appHomeRedirectUrl. diff --git a/shopify_app/_version.py b/shopify_app/_version.py index 53da7ee..38c5661 100644 --- a/shopify_app/_version.py +++ b/shopify_app/_version.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/shopify_app/exchange/client_credentials.py b/shopify_app/exchange/client_credentials.py index 4ef2f21..860d515 100644 --- a/shopify_app/exchange/client_credentials.py +++ b/shopify_app/exchange/client_credentials.py @@ -22,7 +22,7 @@ RequestInput, Res, ) -from ..utils import _get_user_agent +from ..utils import _get_user_agent, redact_http_log from ..utils.http_client import AsyncHTTPClientContext, HTTPClientContext from ._response_builders import build_network_error_response from ._validation import validate_shop @@ -171,12 +171,14 @@ def _build_request(client_id, client_secret, shop): "User-Agent": _get_user_agent(), } - req_obj = { - "method": "POST", - "url": token_endpoint, - "headers": request_headers, - "body": json.dumps(request_body), - } + req_obj = redact_http_log( + { + "method": "POST", + "url": token_endpoint, + "headers": request_headers, + "body": json.dumps(request_body), + } + ) return token_endpoint, request_body, request_headers, req_obj diff --git a/shopify_app/exchange/refresh_token.py b/shopify_app/exchange/refresh_token.py index d9b3451..7264609 100644 --- a/shopify_app/exchange/refresh_token.py +++ b/shopify_app/exchange/refresh_token.py @@ -6,6 +6,7 @@ from __future__ import annotations +import json from datetime import datetime, timedelta, timezone from typing import List, Literal, Optional, Tuple, Union, cast @@ -21,7 +22,7 @@ TokenExchangeResult, User, ) -from ..utils import _get_attr, _get_user_agent +from ..utils import _get_attr, _get_user_agent, redact_http_log from ..utils.http_client import AsyncHTTPClientContext, HTTPClientContext from ._response_builders import build_network_error_response from ._validation import validate_client_id, validate_shop @@ -96,12 +97,14 @@ def refresh_access_token( http_logs: List[HttpLog] = [] # Build the request object for logging - req_log: RequestInput = { - "url": token_endpoint, - "method": "POST", - "headers": request_headers, - "body": "", # Don't log sensitive body - } + req_log: RequestInput = redact_http_log( + { + "url": token_endpoint, + "method": "POST", + "headers": request_headers, + "body": json.dumps(request_body), + } + ) with HTTPClientContext(http_client) as client: while attempt <= max_retries: @@ -520,12 +523,14 @@ async def refresh_access_token_async( http_logs: List[HttpLog] = [] # Build the request object for logging - req_log: RequestInput = { - "url": token_endpoint, - "method": "POST", - "headers": request_headers, - "body": "", # Don't log sensitive body - } + req_log: RequestInput = redact_http_log( + { + "url": token_endpoint, + "method": "POST", + "headers": request_headers, + "body": json.dumps(request_body), + } + ) async with AsyncHTTPClientContext(http_client) as client: while attempt <= max_retries: diff --git a/shopify_app/exchange/token_exchange.py b/shopify_app/exchange/token_exchange.py index bdd481f..397f163 100644 --- a/shopify_app/exchange/token_exchange.py +++ b/shopify_app/exchange/token_exchange.py @@ -25,7 +25,7 @@ TokenExchangeResult, User, ) -from ..utils import _get_attr, _get_user_agent, _to_res +from ..utils import _get_attr, _get_user_agent, _to_res, redact_http_log from ..utils.http_client import AsyncHTTPClientContext, HTTPClientContext from ._response_builders import build_network_error_response from ._validation import validate_client_id @@ -355,12 +355,14 @@ def _build_request(client_id, client_secret, jwt_string, access_mode, shop_url): "User-Agent": _get_user_agent(), } - req_obj = { - "method": "POST", - "url": token_endpoint, - "headers": request_headers, - "body": json.dumps(request_body), - } + req_obj = redact_http_log( + { + "method": "POST", + "url": token_endpoint, + "headers": request_headers, + "body": json.dumps(request_body), + } + ) return token_endpoint, request_body, request_headers, req_obj diff --git a/shopify_app/graphql/admin_graphql.py b/shopify_app/graphql/admin_graphql.py index aa20509..53b4e0b 100644 --- a/shopify_app/graphql/admin_graphql.py +++ b/shopify_app/graphql/admin_graphql.py @@ -15,7 +15,7 @@ import httpx from shopify_app.types import AppConfig, GQLResult, HttpLog, Log, RequestInput, Res -from shopify_app.utils import _get_user_agent, _to_res +from shopify_app.utils import _get_user_agent, _to_res, redact_http_log from ..utils.http_client import AsyncHTTPClientContext, HTTPClientContext @@ -80,6 +80,14 @@ def admin_graphql_request( # Execute request with retry logic attempt = 0 logs: List[HttpLog] = [] + req: RequestInput = redact_http_log( + { + "url": endpoint, + "method": "POST", + "headers": request_headers, + "body": json.dumps(request_body), + } + ) # Use injected client or create one via context manager if http_client is not None: @@ -102,13 +110,6 @@ def admin_graphql_request( response_body = response.text response_headers = dict(response.headers) - req: RequestInput = { - "url": endpoint, - "method": "POST", - "headers": request_headers, - "body": json.dumps(request_body), - } - res = Res( status=status_code, body=response_body, headers=response_headers ) @@ -263,13 +264,6 @@ def admin_graphql_request( except (httpx.RequestError, httpx.ConnectError, httpx.TimeoutException): # Network/connection errors - return immediately without retry - # Use the req already defined above, create new res for error - error_req: RequestInput = { - "url": endpoint, - "method": "POST", - "headers": request_headers, - "body": json.dumps(request_body), - } error_res = Res(status=0, body="", headers={}) return GQLResult( ok=False, @@ -282,7 +276,7 @@ def admin_graphql_request( HttpLog( code="network_error", detail="Network error occurred during GraphQL request", - req=error_req, + req=req, res=error_res, ) ], @@ -691,6 +685,14 @@ async def admin_graphql_request_async( # Execute request with retry logic attempt = 0 logs: List[HttpLog] = [] + req: RequestInput = redact_http_log( + { + "url": endpoint, + "method": "POST", + "headers": request_headers, + "body": json.dumps(request_body), + } + ) async with AsyncHTTPClientContext(http_client) as client: while attempt <= max_retries: @@ -705,13 +707,6 @@ async def admin_graphql_request_async( response_body = response.text response_headers = dict(response.headers) - req: RequestInput = { - "url": endpoint, - "method": "POST", - "headers": request_headers, - "body": json.dumps(request_body), - } - res = Res( status=status_code, body=response_body, headers=response_headers ) @@ -876,13 +871,6 @@ async def admin_graphql_request_async( except (httpx.RequestError, httpx.ConnectError, httpx.TimeoutException): # Network/connection errors - return immediately without retry - # Use different variable names to avoid redefinition - error_req: RequestInput = { - "url": endpoint, - "method": "POST", - "headers": request_headers, - "body": json.dumps(request_body), - } error_res = Res(status=0, body="", headers={}) return GQLResult( ok=False, @@ -895,7 +883,7 @@ async def admin_graphql_request_async( HttpLog( code="network_error", detail="Network error occurred during GraphQL request", - req=error_req, + req=req, res=error_res, ) ], diff --git a/shopify_app/helpers/app_home_parent_redirect.py b/shopify_app/helpers/app_home_parent_redirect.py index fe3b1da..2f20e54 100644 --- a/shopify_app/helpers/app_home_parent_redirect.py +++ b/shopify_app/helpers/app_home_parent_redirect.py @@ -11,6 +11,7 @@ from urllib.parse import parse_qs, urlencode, urlparse from ..types import AppConfig, LogWithReq, RequestInput, Res, ResultForReq +from ..utils import redact_http_log from ..utils.encoding import _json_encode_for_js from ..utils.headers import _normalize_headers @@ -56,6 +57,7 @@ def app_home_parent_redirect( client_id = config.get("client_id", "") shop_domain = f"{shop}.myshopify.com" + req = redact_http_log(request) # Validate request object headers = request.get("headers") if not isinstance(headers, dict): @@ -65,7 +67,7 @@ def app_home_parent_redirect( log=LogWithReq( code="configuration_error", detail="Expected request.headers to be an object", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), ) @@ -78,7 +80,7 @@ def app_home_parent_redirect( log=LogWithReq( code="configuration_error", detail="Expected request.url to be a non-empty string", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), ) @@ -95,7 +97,7 @@ def app_home_parent_redirect( log=LogWithReq( code="invalid_target", detail=f"Target must be '_top' or '_blank'. Received {target}. Respond 400 Bad Request using the provided response.", - req=request, + req=req, ), response=Res(status=400, body="Bad Request", headers={}), ) @@ -109,7 +111,7 @@ def app_home_parent_redirect( log=LogWithReq( code="configuration_error", detail="Redirect URL must use http or https scheme", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), ) @@ -132,7 +134,7 @@ def app_home_parent_redirect( log=LogWithReq( code="app_home_parent_redirect_success", detail="App Home Parent Redirect response constructed. Respond with the provided response to redirect outside the app iframe.", - req=request, + req=req, ), response=Res( status=401, @@ -158,7 +160,7 @@ def app_home_parent_redirect( log=LogWithReq( code="app_home_parent_redirect_success", detail="App Home Parent Redirect response constructed. Respond with the provided response to redirect outside the app iframe.", - req=request, + req=req, ), response=Res( status=200, diff --git a/shopify_app/helpers/app_home_patch_id_token.py b/shopify_app/helpers/app_home_patch_id_token.py index 3d28148..9f57c13 100644 --- a/shopify_app/helpers/app_home_patch_id_token.py +++ b/shopify_app/helpers/app_home_patch_id_token.py @@ -10,6 +10,7 @@ from urllib.parse import parse_qs, urlparse from ..types import AppConfig, LogWithReq, RequestInput, Res, ResultForReq +from ..utils import redact_http_log def app_home_patch_id_token(request: RequestInput, config: AppConfig) -> ResultForReq: @@ -27,6 +28,7 @@ def app_home_patch_id_token(request: RequestInput, config: AppConfig) -> ResultF ResultForReq: Result with ok, shop, log, and response containing HTML and headers """ client_id = config.get("client_id", "") + req = redact_http_log(request) # Check for missing client ID if not client_id: @@ -36,7 +38,7 @@ def app_home_patch_id_token(request: RequestInput, config: AppConfig) -> ResultF log=LogWithReq( code="missing_client_id", detail="Client ID is required but was not provided. Check configuration and respond 500 Internal Server Error using the provided response.", - req=request, + req=req, ), response=Res(status=500, body="Internal Server Error", headers={}), ) @@ -51,7 +53,7 @@ def app_home_patch_id_token(request: RequestInput, config: AppConfig) -> ResultF log=LogWithReq( code="missing_request_url", detail="Request URL is required but was not provided.", - req=request, + req=req, ), response=Res(status=400, body="Bad Request", headers={}), ) @@ -74,7 +76,7 @@ def app_home_patch_id_token(request: RequestInput, config: AppConfig) -> ResultF log=LogWithReq( code="missing_shop", detail="Shop parameter is required in request URL query string but was not provided. Respond 400 Bad Request using the provided response.", - req=request, + req=req, ), response=Res(status=400, body="Bad Request", headers={}), ) @@ -87,7 +89,7 @@ def app_home_patch_id_token(request: RequestInput, config: AppConfig) -> ResultF log=LogWithReq( code="missing_shopify_reload", detail="shopify-reload parameter is required in request URL query string but was not provided. Respond 400 Bad Request using the provided response.", - req=request, + req=req, ), response=Res(status=400, body="Bad Request", headers={}), ) @@ -101,7 +103,7 @@ def app_home_patch_id_token(request: RequestInput, config: AppConfig) -> ResultF log=LogWithReq( code="patch_id_token_page_success", detail="App Home Patch ID Token page Response constructed. Respond with the provided response and App Bridge will obtain an id token.", - req=request, + req=req, ), response=Res( status=200, diff --git a/shopify_app/helpers/app_home_redirect.py b/shopify_app/helpers/app_home_redirect.py index 3191a07..b610c2d 100644 --- a/shopify_app/helpers/app_home_redirect.py +++ b/shopify_app/helpers/app_home_redirect.py @@ -10,6 +10,7 @@ from urllib.parse import parse_qs, urlencode, urlparse from ..types import AppConfig, LogWithReq, RequestInput, Res, ResultForReq +from ..utils import redact_http_log from ..utils.encoding import _json_encode_for_js from ..utils.headers import _normalize_headers @@ -34,6 +35,7 @@ def app_home_redirect( client_id = config.get("client_id", "") shop_domain = f"{shop}.myshopify.com" + req = redact_http_log(request) # Validate request object headers = request.get("headers") if not isinstance(headers, dict): @@ -43,7 +45,7 @@ def app_home_redirect( log=LogWithReq( code="configuration_error", detail="Expected request.headers to be an object", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), ) @@ -56,7 +58,7 @@ def app_home_redirect( log=LogWithReq( code="configuration_error", detail="Expected request.url to be a non-empty string", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), ) @@ -69,7 +71,7 @@ def app_home_redirect( log=LogWithReq( code="invalid_redirect_url", detail=f"Redirect URL must be a relative path starting with '/'. Received {redirect_url}. Respond 400 Bad Request using the provided response.", - req=request, + req=req, ), response=Res(status=400, body="Bad Request", headers={}), ) @@ -98,7 +100,7 @@ def app_home_redirect( log=LogWithReq( code="app_home_redirect_success", detail="App Home Redirect response constructed. Respond with the provided response to redirect within the app.", - req=request, + req=req, ), response=Res( status=200, @@ -119,7 +121,7 @@ def app_home_redirect( log=LogWithReq( code="app_home_redirect_success", detail="App Home Redirect response constructed. Respond with the provided response to redirect within the app.", - req=request, + req=req, ), response=Res( status=302, @@ -137,7 +139,7 @@ def app_home_redirect( log=LogWithReq( code="app_home_redirect_success", detail="App Home Redirect response constructed. Respond with the provided response to redirect within the app.", - req=request, + req=req, ), response=Res( status=302, diff --git a/shopify_app/utils/__init__.py b/shopify_app/utils/__init__.py index ba73f25..cf9e6a0 100644 --- a/shopify_app/utils/__init__.py +++ b/shopify_app/utils/__init__.py @@ -4,6 +4,13 @@ from .headers import _normalize_headers from .input_converters import _get_attr, _to_res +from .redact import redact_http_log from .user_agent import _get_user_agent -__all__ = ["_normalize_headers", "_get_user_agent", "_get_attr", "_to_res"] +__all__ = [ + "_normalize_headers", + "_get_user_agent", + "_get_attr", + "_to_res", + "redact_http_log", +] diff --git a/shopify_app/utils/redact.py b/shopify_app/utils/redact.py new file mode 100644 index 0000000..25197a3 --- /dev/null +++ b/shopify_app/utils/redact.py @@ -0,0 +1,65 @@ +"""HTTP log redaction utilities.""" + +from __future__ import annotations + +import json +import re +from typing import cast + +from ..types import RequestInput + +_REDACTED = "[REDACTED]" +_SENSITIVE_BODY_FIELDS = {"client_secret", "subject_token", "refresh_token"} +_SENSITIVE_HEADER_FIELDS = { + "x-shopify-access-token", + "authorization", + "x-shopify-hmac-sha256", + "shopify-hmac-sha256", +} +_SENSITIVE_URL_PARAMS = {"signature", "id_token", "hmac"} + + +def _sanitize_url(url: str) -> str: + """Redact sensitive query parameter values in a URL string.""" + for param in _SENSITIVE_URL_PARAMS: + url = re.sub( + r"([?&]" + re.escape(param) + r"=)[^&]*", + r"\g<1>" + _REDACTED, + url, + flags=re.IGNORECASE, + ) + return url + + +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 headers: X-Shopify-Access-Token, Authorization (case-insensitive) + - Request URL params: signature, id_token, hmac (case-insensitive) + """ + # Work with a plain dict so we can preserve non-conforming runtime values + # (TypedDict is not enforced at runtime; callers may pass invalid field types) + result: dict = dict(req) + + if isinstance(result.get("headers"), dict): + result["headers"] = { + k: (_REDACTED if k.lower() in _SENSITIVE_HEADER_FIELDS else v) + for k, v in result["headers"].items() + } + + if isinstance(result.get("url"), str): + result["url"] = _sanitize_url(result["url"]) + + if isinstance(result.get("body"), str): + try: + body_dict = json.loads(result["body"]) + for field in _SENSITIVE_BODY_FIELDS: + if field in body_dict: + body_dict[field] = _REDACTED + result["body"] = json.dumps(body_dict, separators=(",", ":")) + except (json.JSONDecodeError, TypeError): + pass + + return cast(RequestInput, result) diff --git a/shopify_app/verify/_body_hmac_in_header.py b/shopify_app/verify/_body_hmac_in_header.py index bd7ac9f..72aa1d3 100644 --- a/shopify_app/verify/_body_hmac_in_header.py +++ b/shopify_app/verify/_body_hmac_in_header.py @@ -15,6 +15,7 @@ from typing import Sequence from ..types import AppConfig, LogWithReq, RequestInput, Res, ResultForReq +from ..utils import redact_http_log from ..utils.headers import _normalize_headers @@ -49,6 +50,7 @@ def _verify_body_hmac_in_header( Returns: ResultForReq: Verification result with ok, shop, log, and response fields """ + req = redact_http_log(request) # Validate request object method = request.get("method") if not isinstance(method, str) or method == "": @@ -58,7 +60,7 @@ def _verify_body_hmac_in_header( log=LogWithReq( code="configuration_error", detail="Expected request.method to be a non-empty string", - req=request, + req=req, ), response=Res( status=500, @@ -75,7 +77,7 @@ def _verify_body_hmac_in_header( log=LogWithReq( code="configuration_error", detail="Expected request.headers to be an object", - req=request, + req=req, ), response=Res( status=500, @@ -92,7 +94,7 @@ def _verify_body_hmac_in_header( log=LogWithReq( code="configuration_error", detail="Expected request.body to be a string", - req=request, + req=req, ), response=Res( status=500, @@ -115,7 +117,7 @@ def _verify_body_hmac_in_header( f"{request_type} requests are expected to use the POST method. " "Respond 405 Method Not Allowed using the provided response." ), - req=request, + req=req, ), response=Res( status=405, @@ -143,7 +145,7 @@ def _verify_body_hmac_in_header( "Required hmac header is missing. " "Respond 400 Bad Request using the provided response." ), - req=request, + req=req, ), response=Res( status=400, @@ -179,7 +181,7 @@ def calculate_hmac(secret: str) -> str: "hmac header value does not match the body's HMAC. " "Respond 401 Unauthorized using the provided response." ), - req=request, + req=req, ), response=Res( status=401, @@ -202,7 +204,7 @@ def calculate_hmac(secret: str) -> str: f"{request_type} request verified successfully. " "Respond 200 OK using the provided response." ), - req=request, + req=req, ), response=Res( status=200, diff --git a/shopify_app/verify/_non_exchangeable_id_token.py b/shopify_app/verify/_non_exchangeable_id_token.py index 19ec4db..a98d1f0 100644 --- a/shopify_app/verify/_non_exchangeable_id_token.py +++ b/shopify_app/verify/_non_exchangeable_id_token.py @@ -19,6 +19,7 @@ Res, ResultWithNonExchangeableIdToken, ) +from ..utils import redact_http_log from ..utils.headers import _normalize_headers @@ -36,6 +37,7 @@ def _verify_non_exchangeable_id_token( Returns: ResultWithNonExchangeableIdToken: Verification result with id_token details """ + req = redact_http_log(request) # Validate request object method = request.get("method") if not isinstance(method, str) or method == "": @@ -46,7 +48,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code="configuration_error", detail="Expected request.method to be a non-empty string", - req=request, + req=req, ), response=Res( status=500, @@ -64,7 +66,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code="configuration_error", detail="Expected request.headers to be an object", - req=request, + req=req, ), response=Res( status=500, @@ -93,7 +95,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code="options_request", detail="OPTIONS request handled for CORS preflight. Respond 204 No Content using the provided response.", - req=request, + req=req, ), response=Res( status=204, @@ -116,7 +118,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code="missing_authorization_header", detail="Required `Authorization` header is missing. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -135,7 +137,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code="invalid_id_token", detail="ID token verification failed. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -187,7 +189,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code=error_code, detail=detail_msg, - req=request, + req=req, ), response=Res( status=401, @@ -206,7 +208,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code="invalid_aud", detail="ID token audience (aud) claim does not match clientId. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -230,7 +232,7 @@ def _verify_non_exchangeable_id_token( log=LogWithReq( code="verified", detail=f"{request_type} request verified. Proceed with business logic.", - req=request, + req=req, ), response=Res( status=200, diff --git a/shopify_app/verify/admin_ui_ext.py b/shopify_app/verify/admin_ui_ext.py index 72dc6df..bf9d6b2 100644 --- a/shopify_app/verify/admin_ui_ext.py +++ b/shopify_app/verify/admin_ui_ext.py @@ -17,6 +17,7 @@ Res, ResultWithExchangeableIdToken, ) +from ..utils import redact_http_log from ..utils.headers import _normalize_headers @@ -33,6 +34,7 @@ def verify_admin_ui_ext_req( Returns: ResultWithExchangeableIdToken: Verification result with exchangeable ID token """ + req = redact_http_log(request) # Validate request object method = request.get("method") if not isinstance(method, str) or method == "": @@ -42,7 +44,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="configuration_error", detail="Expected request.method to be a non-empty string", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), user_id=None, @@ -58,7 +60,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="configuration_error", detail="Expected request.headers to be an object", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), user_id=None, @@ -74,7 +76,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="configuration_error", detail="Expected request.url to be a non-empty string", - req=request, + req=req, ), response=Res(status=500, body="", headers={}), user_id=None, @@ -100,7 +102,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="options_request", detail="OPTIONS request handled for CORS preflight. Respond 204 No Content using the provided response.", - req=request, + req=req, ), response=Res( status=204, @@ -125,7 +127,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="missing_authorization_header", detail="Required `Authorization` header is missing. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res(status=401, body="Unauthorized", headers={}), user_id=None, @@ -142,7 +144,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="invalid_id_token", detail="ID token verification failed. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -194,7 +196,7 @@ def verify_admin_ui_ext_req( return ResultWithExchangeableIdToken( ok=False, shop=None, - log=LogWithReq(code=error_code, detail=detail_msg, req=request), + log=LogWithReq(code=error_code, detail=detail_msg, req=req), response=Res( status=401, body="Unauthorized", @@ -214,7 +216,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="invalid_aud", detail="ID token audience (aud) claim does not match clientId. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -239,7 +241,7 @@ def verify_admin_ui_ext_req( log=LogWithReq( code="verified", detail="Admin UI Extension request verified. Proceed with business logic.", - req=request, + req=req, ), response=Res(status=200, body="", headers={}), user_id=user_id, diff --git a/shopify_app/verify/app_home_req.py b/shopify_app/verify/app_home_req.py index 5262cf8..d9acd34 100644 --- a/shopify_app/verify/app_home_req.py +++ b/shopify_app/verify/app_home_req.py @@ -20,6 +20,7 @@ Res, ResultWithExchangeableIdToken, ) +from ..utils import redact_http_log from ..utils.headers import _normalize_headers @@ -63,6 +64,7 @@ def _build_patch_id_token_redirect( Returns: ResultWithExchangeableIdToken: Redirect response with 302 status and Location header """ + req = redact_http_log(request) clean_query = _remove_query_param(raw_query, "id_token") reload_path = path + ("?" + clean_query if clean_query else "") @@ -80,7 +82,7 @@ def _build_patch_id_token_redirect( log=LogWithReq( code="redirect_to_patch_id_token_page", detail="Embedded app without id_token. Redirect to the patch ID token page to obtain a new token using the provided response.", - req=request, + req=req, ), response=Res( status=302, @@ -109,6 +111,7 @@ def verify_app_home_req( Returns: ResultWithExchangeableIdToken: Verification result with exchangeable ID token """ + req = redact_http_log(request) # Validate app_home_patch_id_token_path if not isinstance(app_home_patch_id_token_path, str): return ResultWithExchangeableIdToken( @@ -117,7 +120,7 @@ def verify_app_home_req( log=LogWithReq( code="configuration_error", detail="Expected appHomePatchIdTokenPath to be a non-empty string", - req=request, + req=req, ), response=Res( status=500, @@ -136,7 +139,7 @@ def verify_app_home_req( log=LogWithReq( code="configuration_error", detail="Expected appHomePatchIdTokenPath to be a non-empty string, but got ''", - req=request, + req=req, ), response=Res( status=500, @@ -157,7 +160,7 @@ def verify_app_home_req( log=LogWithReq( code="configuration_error", detail="Expected request.url to be a non-empty string", - req=request, + req=req, ), response=Res( status=500, @@ -177,7 +180,7 @@ def verify_app_home_req( log=LogWithReq( code="configuration_error", detail="Expected request.headers to be an object", - req=request, + req=req, ), response=Res( status=500, @@ -235,7 +238,7 @@ def verify_app_home_req( log=LogWithReq( code="invalid_id_token", detail="ID token verification failed. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -257,7 +260,7 @@ def verify_app_home_req( log=LogWithReq( code="missing_authorization_and_id_token", detail="Neither Authorization header nor id_token query parameter present. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -318,7 +321,7 @@ def verify_app_home_req( log=LogWithReq( code=error_code, detail=detail_msg, - req=request, + req=req, ), response=Res( status=401, @@ -348,7 +351,7 @@ def verify_app_home_req( log=LogWithReq( code="invalid_aud", detail="ID token audience (aud) claim does not match clientId. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -419,7 +422,7 @@ def verify_app_home_req( log=LogWithReq( code="verified", detail=log_detail, - req=request, + req=req, ), response=Res( status=200, diff --git a/shopify_app/verify/app_proxy.py b/shopify_app/verify/app_proxy.py index 07de0d4..1089356 100644 --- a/shopify_app/verify/app_proxy.py +++ b/shopify_app/verify/app_proxy.py @@ -19,6 +19,7 @@ Res, ResultWithLoggedInCustomerId, ) +from ..utils import redact_http_log def verify_app_proxy_req( @@ -34,6 +35,7 @@ def verify_app_proxy_req( Returns: ResultWithLoggedInCustomerId: Verification result with logged_in_customer_id """ + req = redact_http_log(request) # Validate request object url = request.get("url") if not isinstance(url, str) or url == "": @@ -44,7 +46,7 @@ def verify_app_proxy_req( log=LogWithReq( code="configuration_error", detail="Expected request.url to be a non-empty string", - req=request, + req=req, ), response=Res( status=500, @@ -78,7 +80,7 @@ def verify_app_proxy_req( log=LogWithReq( code="missing_timestamp", detail="Required `timestamp` query parameter is missing. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -104,7 +106,7 @@ def verify_app_proxy_req( log=LogWithReq( code="timestamp_too_old", detail="The `timestamp` query parameter is more than 90 seconds old. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -120,7 +122,7 @@ def verify_app_proxy_req( log=LogWithReq( code="invalid_timestamp", detail="The `timestamp` query parameter is not a valid integer. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -138,7 +140,7 @@ def verify_app_proxy_req( log=LogWithReq( code="missing_signature", detail="Required `signature` query parameter is missing. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -178,7 +180,7 @@ def calculate_hmac_hex(secret: str) -> str: log=LogWithReq( code="invalid_signature", detail="`signature` query parameter does not match the expected HMAC. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -212,7 +214,7 @@ def calculate_hmac_hex(secret: str) -> str: log=LogWithReq( code="verified", detail="App Proxy request verified successfully. Proceed with business logic.", - req=request, + req=req, ), response=Res( status=200, diff --git a/shopify_app/verify/pos_ui_ext.py b/shopify_app/verify/pos_ui_ext.py index 5c4aaeb..18f1f96 100644 --- a/shopify_app/verify/pos_ui_ext.py +++ b/shopify_app/verify/pos_ui_ext.py @@ -17,6 +17,7 @@ Res, ResultWithExchangeableIdToken, ) +from ..utils import redact_http_log from ..utils.headers import _normalize_headers @@ -33,6 +34,7 @@ def verify_pos_ui_ext_req( Returns: ResultWithExchangeableIdToken: Verification result with ok, shop, log, response, user_id, id_token, and new_id_token_response fields """ + req = redact_http_log(request) # Validate request object method = request.get("method") if not isinstance(method, str) or method == "": @@ -42,7 +44,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="configuration_error", detail="Expected request.method to be a non-empty string", - req=request, + req=req, ), response=Res( status=500, @@ -62,7 +64,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="configuration_error", detail="Expected request.headers to be an object", - req=request, + req=req, ), response=Res( status=500, @@ -82,7 +84,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="configuration_error", detail="Expected request.url to be a non-empty string", - req=request, + req=req, ), response=Res( status=500, @@ -112,7 +114,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="options_request", detail="OPTIONS request handled for CORS preflight. Respond 204 No Content using the provided response.", - req=request, + req=req, ), response=Res( status=204, @@ -137,7 +139,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="missing_authorization_header", detail="Required `Authorization` header is missing. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -158,7 +160,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="invalid_id_token", detail="ID token verification failed. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -212,7 +214,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code=error_code, detail=detail_msg, - req=request, + req=req, ), response=Res( status=401, @@ -233,7 +235,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="invalid_aud", detail="ID token audience (aud) claim does not match clientId. Respond 401 Unauthorized using the provided response.", - req=request, + req=req, ), response=Res( status=401, @@ -258,7 +260,7 @@ def verify_pos_ui_ext_req( log=LogWithReq( code="verified", detail="POS UI Extension request verified. Proceed with business logic.", - req=request, + req=req, ), response=Res( status=200,