Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/bedrock_agentcore/payments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 6 additions & 4 deletions src/bedrock_agentcore/payments/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/bedrock_agentcore/payments/mpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))


Expand Down
11 changes: 11 additions & 0 deletions tests/bedrock_agentcore/payments/test_mpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
29 changes: 29 additions & 0 deletions tests/bedrock_agentcore/payments/test_mpp_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading