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)