Skip to content

fix(client): match challenges on intent, not just method - #133

Open
pucedoteth wants to merge 1 commit into
tempoxyz:mainfrom
pucedoteth:fix-client-intent-aware-challenge-selection
Open

fix(client): match challenges on intent, not just method#133
pucedoteth wants to merge 1 commit into
tempoxyz:mainfrom
pucedoteth:fix-client-intent-aware-challenge-selection

Conversation

@pucedoteth

Copy link
Copy Markdown

The client transport selects the first challenge whose method token is configured, ignoring the challenge intent (pkg/client/transport.go:72):

if m, ok := t.methods[ch.Method]; ok {

The core spec's Intent Negotiation section has a server offer the same method under two intents:

WWW-Authenticate: Payment id="abc", ..., method="example", intent="charge", request="..."
WWW-Authenticate: Payment id="def", ..., method="example", intent="authorize", request="..."

and requires:

Clients choose which challenge to respond to. Clients that do not recognize an intent SHOULD treat the challenge as unsupported.

Matching on the method alone commits to whichever challenge is listed first.

Impact

Built-in Tempo method — the request fails outright. CreateCredential rejects a non-charge intent (pkg/tempo/client/method.go:94) and RoundTrip turns that into an error, so a server offering [tempo/authorize, tempo/charge] gets no payment at all, even though the charge challenge is settleable:

mpp: creating credential for method "example": unsupported challenge intent "authorize"

Third-party methods — the wrong intent gets paid. The client.Method interface does not require an intent check, so a Method that does not re-check it itself will build a credential for an intent it never meant to settle.

Note the client never sends Accept-Payment, so the server cannot filter on the client's behalf either — this check is the only thing standing between the client and an unrecognized intent.

Fix

Add an optional client.IntentMethod interface so a Method can declare the intents it supports, and consult it during selection.

Methods that do not implement it are treated as accepting any intent, so existing implementations are unaffected — I chose this over defaulting to charge-only because Go interfaces cannot be extended without breaking implementors, and silently narrowing third-party methods would be a regression. The built-in Tempo client method implements it and now skips non-charge challenges instead of failing on them.

Cross-SDK consistency

This brings mpp-go in line with its siblings: mpp-rs already selects on (method, intent) via PaymentProvider::supports(method, intent), and pympp does the same after tempoxyz/pympp#216 ("Challenge selection ignores the intent").

Test plan

Three tests in pkg/client/transport_test.go, all reproducing the spec's two-intent example:

  • SkipsUnsupportedIntent — without the fix: built credentials for [authorize], want only [charge]
  • UnsupportedIntentFirstStillPays — without the fix: request failed even though a payable challenge was offered: mpp: creating credential for method "example": unsupported challenge intent "authorize"
  • MethodWithoutIntentsAcceptsAny — compatibility guard; passes with and without the fix by design

Verified by reverting only the three production files and re-running: the first two fail, the third still passes. make check is clean and the full suite passes (13 packages).


🤖 Written with Claude Code. Every claim above was verified against a local build and test run; the red/green proof is reproducible by reverting pkg/client/client.go, pkg/client/transport.go and pkg/tempo/client/method.go.

The transport selected the first challenge whose method token was
configured, ignoring the challenge intent:

    if m, ok := t.methods[ch.Method]; ok {

The core spec's intent negotiation section has a server offer the same
method under two intents, and requires that clients "that do not
recognize an intent SHOULD treat the challenge as unsupported". Matching
on the method alone commits to whichever challenge is listed first.

For the built-in Tempo method the result is a failed request rather than
a wrong payment: CreateCredential rejects a non-charge intent, and
RoundTrip turns that into an error, so a server offering
[tempo/authorize, tempo/charge] gets no payment at all even though the
charge challenge is settleable. A third-party Method that does not
re-check the intent itself would instead build a credential for the
wrong intent.

Add an optional client.IntentMethod interface so a Method can declare
the intents it supports, and consult it during selection. Methods that
do not implement it are treated as accepting any intent, so existing
implementations are unaffected; the built-in Tempo client method
implements it and now skips non-charge challenges instead of failing on
them.

mpp-rs already selects on (method, intent) via PaymentProvider::supports,
and pympp does the same after tempoxyz/pympp#216.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant