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
10 changes: 10 additions & 0 deletions .changeset/payment-request-sub-id-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@agentcommercekit/ack-pay": patch
---

Bind the payment request token subject to its id during verification.

`createPaymentRequestToken` always sets `sub` to the payment request id, but
`verifyPaymentRequestToken` never checked that binding, so a validly signed
token could carry a different `sub` and still verify. Reject tokens whose JWT
`sub` does not match the parsed payment request id.
2 changes: 1 addition & 1 deletion docs/ack-pay/payment-request-payload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Every Payment Request payload contains essential properties:

| Field | Type / Status | Description |
| :-------------------- | :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentRequestToken` | String / Required | A cryptographic token (e.g., JWT) generated by the Server, signing the `paymentRequest` payload to ensure integrity and authenticity. This token is verified by several participants in the protocol. |
| `paymentRequestToken` | String / Required | A cryptographic token (e.g., JWT) generated by the Server, signing the `paymentRequest` payload to ensure integrity and authenticity. When encoded as a JWT, the `sub` claim must equal `paymentRequest.id`. This token is verified by several participants in the protocol. |
| `paymentRequest` | Object / Required | The main payment request object containing all payment details. |
| ` id` | String / Required | A unique identifier generated for this specific payment request. Used for tracking and preventing replay attacks. |
| ` expiresAt` | String / Optional | An ISO 8601 timestamp indicating when this payment request becomes invalid. Clients should not attempt payment after this time. |
Expand Down
30 changes: 30 additions & 0 deletions packages/ack-pay/src/verify-payment-request-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,36 @@ describe("verifyPaymentRequestToken", () => {
expect(error.cause).toBeInstanceOf(Error)
})

it("throws when sub does not match id without cause", async () => {
const mismatchedToken = await createJwt(
{
...paymentRequest,
sub: "different-subject",
},
{
issuer: issuerDid,
signer,
},
{
alg: curveToJwtAlgorithm(keypair.curve),
},
)

const resolver = getDidResolver()
resolver.addToCache(issuerDid, issuerDidDocument)

const error = await verifyPaymentRequestToken(mismatchedToken, {
resolver,
verifyExpiry: false,
}).catch((e) => e)

expect(error).toBeInstanceOf(InvalidPaymentRequestTokenError)
expect(error.message).toBe(
"Payment Request token subject does not match its id",
)
expect(error.cause).toBeUndefined()
})

it("throws for a JWT that does not contain a payment config without cause", async () => {
// Create a JWT with valid format but missing payment config
const invalidToken = await createJwt(
Expand Down
8 changes: 8 additions & 0 deletions packages/ack-pay/src/verify-payment-request-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export async function verifyPaymentRequestToken(
)
}

const { sub } = parsedPaymentRequestToken.payload

if (typeof sub !== "string" || sub !== output.id) {
throw new InvalidPaymentRequestTokenError(
"Payment Request token subject does not match its id",
)
}

return {
paymentRequest: output,
parsed: parsedPaymentRequestToken,
Expand Down