Skip to content

fix(payments): send MPP credential in the header the challenge selects - #719

Open
abhiramshaji-cyber wants to merge 1 commit into
aws:mainfrom
abhiramshaji-cyber:fix/mpp-payment-authorization-header
Open

abhiramshaji-cyber wants to merge 1 commit into
aws:mainfrom
abhiramshaji-cyber:fix/mpp-payment-authorization-header

Conversation

@abhiramshaji-cyber

Copy link
Copy Markdown
Contributor

Fixes #702

A paid MPP request whose server asks for the credential in Payment-Authorization now sends it there, so the payment is accepted and the caller's own login token is kept.

What this code is for

PaymentManager.generate_payment_header handles an HTTP 402 response. For MPP it reads the WWW-Authenticate: Payment challenges with parse_www_authenticate, picks one with select_challenge, pays it through ProcessPayment, and returns the header to attach when the request is sent again. The Strands plugin and the LangGraph middleware put that header on the retried request with apply_payment_header.

Before this PR

An agent calls an mppx server created with requiresAuth: true, sending Authorization: Bearer <token>.

  1. The server replies 402 with a challenge that includes header="Payment-Authorization".
  2. parse_www_authenticate keeps the challenge and select_challenge selects it.
  3. ProcessPayment is called and the payment is made.
  4. _generate_mpp_payment_header always returns {"Authorization": "Payment <credential>"}.
  5. apply_payment_header writes that over the caller's Authorization: Bearer <token>.
  6. The server looks for the credential only in Payment-Authorization, finds nothing, and the request fails after the money was spent.

After this PR

Same agent, same server.

  1. Same as above through step 3.
  2. _generate_mpp_payment_header sees header on the selected challenge and returns {"Payment-Authorization": "Payment <credential>"}.
  3. apply_payment_header adds it next to Authorization: Bearer <token>, which stays as it was.
  4. The server reads the credential from Payment-Authorization and serves the resource.
  5. A challenge without header still returns {"Authorization": "Payment <credential>"}.
  6. A challenge with any other header value is dropped by _is_valid_challenge, so it is never paid.

The diagram

flowchart LR

classDef box fill:#eef1ff,stroke:#4c5bd4,color:#111827
classDef bad fill:#ffd6d6,stroke:#cc0000,color:#111827,stroke-width:3px
classDef good fill:#c8f0c8,stroke:#008800,color:#111827,stroke-width:3px

subgraph BEFORE["BEFORE: paid, then rejected"]
direction TB
  B1["402 with header=Payment-Authorization"] --> B2["select_challenge"]
  B2 --> B3["ProcessPayment"]
  B3 --> B4{"always Authorization"}
  B4 -->|"Bearer token overwritten"| B5["server finds no credential"]
  B4 -->|"challenge without header"| B6["Authorization: Payment, works"]
end

subgraph AFTER["AFTER: paid and accepted"]
direction TB
  A1["402 with header=Payment-Authorization"] --> A2["select_challenge"]
  A2 --> A3["ProcessPayment"]
  A3 --> A4{"THE CHANGE: header on challenge?"}
  A4 -->|"yes"| A5["Payment-Authorization: Payment, Bearer kept"]
  A4 -->|"no"| A6["Authorization: Payment, works"]
end

B1 ~~~ A1

class B1,B2,B3,B6 box
class A1,A2,A3,A6 box
class B4,B5 bad
class A4,A5 good

linkStyle 3 stroke:#cc0000,stroke-width:5px
linkStyle 8 stroke:#008800,stroke-width:5px

style BEFORE fill:#fff5f5,stroke:#cc0000,color:#111827
style AFTER fill:#f3fff3,stroke:#008800,color:#111827
Loading

The change

The fix is in _generate_mpp_payment_header: the returned key is now MPP_PAYMENT_AUTHORIZATION_HEADER if "header" in selected else "Authorization". The new constant MPP_PAYMENT_AUTHORIZATION_HEADER = "Payment-Authorization" lives in constants.py.

The second change is a check in _is_valid_challenge: a challenge whose header is anything other than Payment-Authorization is treated as invalid. The spec says a client must not send a credential for such a challenge, so this stops the SDK from paying one. If no other challenge is payable, generate_payment_header raises PaymentError as it already does when nothing can be selected.

Tests: test_payment_authorization_header_param_is_accepted, test_unrecognized_header_param_is_rejected in test_mpp.py, and test_payment_authorization_challenge_returns_payment_authorization_header, test_unrecognized_header_challenge_is_not_paid in test_mpp_manager.py.

What changes for a user

  • Servers that send header="Payment-Authorization" now get the credential where they read it, and the request succeeds after payment.
  • The caller's own Authorization header is no longer overwritten in that case.
  • Servers that do not send header see no change.
  • A challenge with an unknown header value is no longer paid. If it was the only option, the call now fails before any money moves instead of after.
  • The credential's challenge object is built by the service from the raw challenge, which already includes header, so this PR does not touch that part.

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 aws#702
@abhiramshaji-cyber
abhiramshaji-cyber requested a review from a team October 27, 2026 16:24
@github-actions github-actions Bot added the size/s PR size: S label Oct 27, 2026
@abhiramshaji-cyber abhiramshaji-cyber changed the title fix(payments): send MPP credential in the challenge's header fix(payments): send MPP credential in the header the challenge selects Oct 27, 2026
@github-actions github-actions Bot added size/s PR size: S and removed size/s PR size: S labels Oct 27, 2026

This branch is waiting to be deployed

1 waiting deployment
manual-approval — 1c7e92b6 Waiting Oct 27, 2026 by abhiramshaji-cyber via Test (identity) #1665
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MPP: credential is always sent in Authorization, ignoring header="Payment-Authorization" in the challenge

1 participant