-
Notifications
You must be signed in to change notification settings - Fork 109
fix(ack-pay): validate receipt payment option #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@agentcommercekit/ack-pay": patch | ||
| --- | ||
|
|
||
| Validate that a PaymentReceipt paymentOptionId matches an option from the verified Payment Request token. When a parsed credential is passed, the receipt fields are now derived from the signed proof (`proof.jwt`) rather than the caller-supplied object, so the binding cannot be bypassed by mutating the outer credential. Exports `InvalidPaymentReceiptError` so callers can catch receipt validation failures explicitly. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,14 @@ import { | |
| InvalidCredentialError, | ||
| InvalidCredentialSubjectError, | ||
| isCredential, | ||
| isJwtProof, | ||
| parseJwtCredential, | ||
| verifyParsedCredential, | ||
| type Verifiable, | ||
| type W3CCredential, | ||
| } from "@agentcommercekit/vc" | ||
|
|
||
| import { InvalidPaymentReceiptError } from "./errors" | ||
| import type { PaymentRequest } from "./payment-request" | ||
| import { | ||
| getReceiptClaimVerifier, | ||
|
|
@@ -68,7 +70,15 @@ export async function verifyPaymentReceipt( | |
| if (isJwtString(receipt)) { | ||
| parsedCredential = await parseJwtCredential(receipt, resolver) | ||
| } else if (isCredential(receipt)) { | ||
| parsedCredential = receipt | ||
| // A parsed credential's top-level fields are caller-supplied and are not | ||
| // bound to the signed proof: verifyParsedCredential() verifies `proof.jwt` | ||
| // but does not reconcile the outer object against the decoded payload. When | ||
| // the proof is a JWT proof, re-derive the credential from `proof.jwt` so the | ||
| // paymentOptionId / paymentRequestToken reads below come from the signed | ||
| // payload rather than a (potentially mutated) caller-supplied object. | ||
| parsedCredential = isJwtProof(receipt.proof) | ||
| ? await parseJwtCredential(receipt.proof.jwt, resolver) | ||
| : receipt | ||
| } else { | ||
| throw new InvalidCredentialError("Receipt is not a JWT or Credential") | ||
| } | ||
|
|
@@ -116,6 +126,18 @@ export async function verifyPaymentReceipt( | |
| }, | ||
| ) | ||
|
|
||
| const receiptPaymentOptionId = | ||
| parsedCredential.credentialSubject.paymentOptionId | ||
| const paymentOptionExists = paymentRequest.paymentOptions.some( | ||
| (paymentOption) => paymentOption.id === receiptPaymentOptionId, | ||
| ) | ||
|
Comment on lines
+129
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For parsed-credential inputs, this new binding check reads Useful? React with 👍 / 👎. |
||
|
|
||
| if (!paymentOptionExists) { | ||
| throw new InvalidPaymentReceiptError( | ||
| "Receipt paymentOptionId does not match any payment option in the Payment Request token", | ||
| ) | ||
| } | ||
|
|
||
| return { | ||
| receipt: parsedCredential, | ||
| paymentRequestToken, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. One note for future reference: this check runs against
parsedCredential.credentialSubject, which is trustworthy when the input is a JWT string (decoded from the signed payload) but is the caller-supplied object when the input is a pre-parsed credential.The proof verification validates the JWT inside
proof, but doesn't currently enforce that the outer object matches the signed payload. Not a blocker for this PR tho.