Repository navigation
fix(payments): send MPP credential in the header the challenge selects - #719
Open
abhiramshaji-cyber wants to merge 1 commit into
Open
abhiramshaji-cyber wants to merge 1 commit into
abhiramshaji-cyber wants to merge 1 commit into
Conversation
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
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
abhiramshaji-cyber
requested a deployment
to
manual-approval
October 27, 2026 16:43 — with
GitHub Actions
Waiting
This branch is waiting to be deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #702
A paid MPP request whose server asks for the credential in
Payment-Authorizationnow 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_headerhandles an HTTP 402 response. For MPP it reads theWWW-Authenticate: Paymentchallenges withparse_www_authenticate, picks one withselect_challenge, pays it throughProcessPayment, 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 withapply_payment_header.Before this PR
An agent calls an mppx server created with
requiresAuth: true, sendingAuthorization: Bearer <token>.header="Payment-Authorization".parse_www_authenticatekeeps the challenge andselect_challengeselects it.ProcessPaymentis called and the payment is made._generate_mpp_payment_headeralways returns{"Authorization": "Payment <credential>"}.apply_payment_headerwrites that over the caller'sAuthorization: Bearer <token>.Payment-Authorization, finds nothing, and the request fails after the money was spent.After this PR
Same agent, same server.
_generate_mpp_payment_headerseesheaderon the selected challenge and returns{"Payment-Authorization": "Payment <credential>"}.apply_payment_headeradds it next toAuthorization: Bearer <token>, which stays as it was.Payment-Authorizationand serves the resource.headerstill returns{"Authorization": "Payment <credential>"}.headervalue 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:#111827The change
The fix is in
_generate_mpp_payment_header: the returned key is nowMPP_PAYMENT_AUTHORIZATION_HEADER if "header" in selected else "Authorization". The new constantMPP_PAYMENT_AUTHORIZATION_HEADER = "Payment-Authorization"lives inconstants.py.The second change is a check in
_is_valid_challenge: a challenge whoseheaderis anything other thanPayment-Authorizationis 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_headerraisesPaymentErroras it already does when nothing can be selected.Tests:
test_payment_authorization_header_param_is_accepted,test_unrecognized_header_param_is_rejectedintest_mpp.py, andtest_payment_authorization_challenge_returns_payment_authorization_header,test_unrecognized_header_challenge_is_not_paidintest_mpp_manager.py.What changes for a user
header="Payment-Authorization"now get the credential where they read it, and the request succeeds after payment.Authorizationheader is no longer overwritten in that case.headersee no change.headervalue is no longer paid. If it was the only option, the call now fails before any money moves instead of after.challengeobject is built by the service from the raw challenge, which already includesheader, so this PR does not touch that part.