From 1c7e92b618bbaa0537eca17f787442fab739ae63 Mon Sep 17 00:00:00 2001 From: abhiramshaji-cyber Date: Tue, 27 Oct 2026 12:18:29 -0400 Subject: [PATCH] fix(payments): send MPP credential in the header the challenge selects An MPP challenge with header="Payment-Authorization" requires the credential in Payment-Authorization so the request keeps its own Authorization value. The SDK always returned Authorization, so the server ignored the credential and the caller's Bearer token was overwritten. Return the credential under Payment-Authorization for such challenges, and treat any other header value as an invalid challenge so it is never paid, as the spec requires. Fixes #702 --- src/bedrock_agentcore/payments/constants.py | 2 ++ src/bedrock_agentcore/payments/manager.py | 10 ++++--- src/bedrock_agentcore/payments/mpp.py | 4 +++ tests/bedrock_agentcore/payments/test_mpp.py | 11 +++++++ .../payments/test_mpp_manager.py | 29 +++++++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/bedrock_agentcore/payments/constants.py b/src/bedrock_agentcore/payments/constants.py index 15f43c87..36a662fe 100644 --- a/src/bedrock_agentcore/payments/constants.py +++ b/src/bedrock_agentcore/payments/constants.py @@ -118,6 +118,8 @@ class PaymentsAuthorizerType(Enum): # The `Payment` auth-scheme name used in `WWW-Authenticate` / `Authorization` headers. MPP_AUTH_SCHEME = "Payment" +MPP_PAYMENT_AUTHORIZATION_HEADER = "Payment-Authorization" + # Only the `charge` intent is implemented. Challenges advertising `session` or # `subscription` are filtered out during selection. MPP_SUPPORTED_INTENT = "charge" diff --git a/src/bedrock_agentcore/payments/manager.py b/src/bedrock_agentcore/payments/manager.py index dc813bc0..2a8cabf0 100644 --- a/src/bedrock_agentcore/payments/manager.py +++ b/src/bedrock_agentcore/payments/manager.py @@ -17,7 +17,7 @@ from bedrock_agentcore._utils.user_agent import build_user_agent_suffix from ._validation import validate_permit2_allowance_limit -from .constants import MPP_AUTH_SCHEME, MPP_DEFAULT_VERSION +from .constants import MPP_AUTH_SCHEME, MPP_DEFAULT_VERSION, MPP_PAYMENT_AUTHORIZATION_HEADER from .mpp import ( MppChallengeSelectionError, extract_challenges, @@ -1241,8 +1241,9 @@ def _generate_mpp_payment_header( omitting it preserves the service-side protocol default. Returns: - Dictionary with the ready-to-send Authorization header, - e.g. {"Authorization": "Payment eyJ..."} + Dictionary with the ready-to-send credential header, e.g. + {"Authorization": "Payment eyJ..."}, or {"Payment-Authorization": "Payment eyJ..."} + when the selected challenge carries header="Payment-Authorization" Raises: PaymentError: If no challenge can be satisfied or payment processing fails @@ -1345,8 +1346,9 @@ def _generate_mpp_payment_header( else: header_value = f"{scheme_prefix}{credential}" + header_name = MPP_PAYMENT_AUTHORIZATION_HEADER if "header" in selected else "Authorization" logger.info("Successfully generated MPP payment header for challenge %s", selected_id) - return {"Authorization": header_value} + return {header_name: header_value} def __getattr__(self, name: str): """Dynamically forward method calls to the PaymentClient. diff --git a/src/bedrock_agentcore/payments/mpp.py b/src/bedrock_agentcore/payments/mpp.py index b67d417e..55c712f3 100644 --- a/src/bedrock_agentcore/payments/mpp.py +++ b/src/bedrock_agentcore/payments/mpp.py @@ -32,6 +32,7 @@ from .constants import ( MPP_AUTH_SCHEME, MPP_METHOD_BLOCKCHAIN, + MPP_PAYMENT_AUTHORIZATION_HEADER, MPP_SOLANA_DEFAULT_NETWORK, MPP_SOLANA_NETWORK_ALIASES, MPP_SUPPORTED_INTENT, @@ -313,6 +314,9 @@ def _is_valid_challenge(challenge: Dict[str, Any]) -> bool: if not method.isascii() or not method.isalpha() or method.lower() != method: return False + if "header" in challenge and challenge["header"] != MPP_PAYMENT_AUTHORIZATION_HEADER: + return False + return bool(decode_challenge_request(challenge)) diff --git a/tests/bedrock_agentcore/payments/test_mpp.py b/tests/bedrock_agentcore/payments/test_mpp.py index 4d29617c..5119718c 100644 --- a/tests/bedrock_agentcore/payments/test_mpp.py +++ b/tests/bedrock_agentcore/payments/test_mpp.py @@ -257,6 +257,17 @@ def test_invalid_sibling_does_not_suppress_valid_challenge(self): assert [challenge["id"] for challenge in parse_www_authenticate(f"{invalid}, {valid}")] == ["valid"] + def test_payment_authorization_header_param_is_accepted(self): + header = challenge_header("x", "evm", evm_request()) + ', header="Payment-Authorization"' + + assert parse_www_authenticate(header)[0]["header"] == "Payment-Authorization" + + @pytest.mark.parametrize("value", ["Authorization", "X-Payment", "payment-authorization", ""]) + def test_unrecognized_header_param_is_rejected(self, value): + header = challenge_header("x", "evm", evm_request()) + f', header="{value}"' + + assert parse_www_authenticate(header) == [] + class TestExtractChallenges: """Tests for pulling challenges out of a 402 payment required request.""" diff --git a/tests/bedrock_agentcore/payments/test_mpp_manager.py b/tests/bedrock_agentcore/payments/test_mpp_manager.py index c6c41513..99fdefd0 100644 --- a/tests/bedrock_agentcore/payments/test_mpp_manager.py +++ b/tests/bedrock_agentcore/payments/test_mpp_manager.py @@ -101,6 +101,35 @@ def test_mpp_402_returns_authorization_header(self, manager): assert header == {"Authorization": "Payment eyJhIjoxfQ"} + def test_payment_authorization_challenge_returns_payment_authorization_header(self, manager): + set_instrument_network(manager, "ETHEREUM") + set_mpp_result(manager, credential="eyJhIjoxfQ") + challenge = challenge_header("evm-1", "evm", evm_request()) + ', header="Payment-Authorization"' + + header = manager.generate_payment_header( + payment_instrument_id="pi-1", + payment_session_id="ps-1", + user_id="user-1", + payment_required_request=mpp_402(challenge), + ) + + assert header == {"Payment-Authorization": "Payment eyJhIjoxfQ"} + + def test_unrecognized_header_challenge_is_not_paid(self, manager): + set_instrument_network(manager, "ETHEREUM") + set_mpp_result(manager) + challenge = challenge_header("evm-1", "evm", evm_request()) + ', header="X-Payment"' + + with pytest.raises(PaymentError): + manager.generate_payment_header( + payment_instrument_id="pi-1", + payment_session_id="ps-1", + user_id="user-1", + payment_required_request=mpp_402(challenge), + ) + + manager._payment_client.process_payment.assert_not_called() + def test_mpp_sends_payment_type_mpp(self, manager): set_instrument_network(manager, "ETHEREUM") set_mpp_result(manager)