diff --git a/apps/web/app/api/payment/webhook/route.ts b/apps/web/app/api/payment/webhook/route.ts index 4d1285fc9..4111bf885 100644 --- a/apps/web/app/api/payment/webhook/route.ts +++ b/apps/web/app/api/payment/webhook/route.ts @@ -17,7 +17,8 @@ import { activateMembership } from "../helpers"; export async function POST(req: NextRequest) { try { - const body = await req.json(); + const rawBody = await req.text(); + const body = JSON.parse(rawBody); const domainName = req.headers.get("domain"); const domain = await getDomain(domainName); @@ -33,7 +34,13 @@ export async function POST(req: NextRequest) { return Response.json({ message: "Payment method not found" }); } - if (!(await paymentMethod.verify(body))) { + const headers: Record = { + "stripe-signature": req.headers.get("stripe-signature"), + "x-razorpay-signature": req.headers.get("x-razorpay-signature"), + "x-signature": req.headers.get("x-signature"), + }; + + if (!(await paymentMethod.verify(body, rawBody, headers))) { return Response.json({ message: "Payment not verified" }); } diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 9edff1c7c..c4b7818fb 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/payments-new/lemonsqueezy-payment.ts b/apps/web/payments-new/lemonsqueezy-payment.ts index 1dfe26233..2e61898e5 100644 --- a/apps/web/payments-new/lemonsqueezy-payment.ts +++ b/apps/web/payments-new/lemonsqueezy-payment.ts @@ -1,5 +1,6 @@ import Payment, { InitiateProps } from "./payment"; import { responses } from "../config/strings"; +import crypto from "crypto"; import { Constants, PaymentPlan, @@ -20,7 +21,7 @@ export default class LemonSqueezyPayment implements Payment { public siteinfo: SiteInfo; public name: string; private apiKey: string; - // private webhookSecret: string; + private webhookSecret: string | undefined; constructor(siteinfo: SiteInfo) { this.siteinfo = siteinfo; @@ -45,7 +46,7 @@ export default class LemonSqueezyPayment implements Payment { } this.apiKey = this.siteinfo.lemonsqueezyKey; - // this.webhookSecret = this.siteinfo.lemonsqueezyWebhookSecret; + this.webhookSecret = this.siteinfo.lemonsqueezyWebhookSecret; return this; } @@ -134,8 +135,38 @@ export default class LemonSqueezyPayment implements Payment { return store.attributes.currency.toLowerCase(); } - async verify(event: any) { - // if (!this.verifyWebhookSignature(event)) return false; + async verify( + event: any, + rawBody: string, + headers: Record, + ) { + const signature = headers["x-signature"]; + if (!signature || !this.webhookSecret) { + return false; + } + + const digest = crypto + .createHmac("sha256", this.webhookSecret) + .update(rawBody) + .digest("hex"); + const receivedHash = signature.startsWith("sha256=") + ? signature.slice(7) + : signature; + try { + const expected = Buffer.from(digest); + const received = Buffer.from(receivedHash); + if ( + expected.length !== received.length || + !crypto.timingSafeEqual( + new Uint8Array(expected), + new Uint8Array(received), + ) + ) { + return false; + } + } catch { + return false; + } const eventType = event.meta.event_name; const attributes = event.data.attributes; @@ -267,17 +298,4 @@ export default class LemonSqueezyPayment implements Payment { const { data } = await response.json(); return data; } - - // private verifyWebhookSignature(event: any) { - // const signature = event.meta.signature; - // const payload = JSON.stringify(event); - - // const hmac = crypto.createHmac("sha256", this.webhookSecret); - // const digest = hmac.update(payload).digest("hex"); - - // return crypto.timingSafeEqual( - // Buffer.from(signature), - // Buffer.from(digest) - // ); - // } } diff --git a/apps/web/payments-new/payment.ts b/apps/web/payments-new/payment.ts index ba9d60e3d..bb92f3a22 100644 --- a/apps/web/payments-new/payment.ts +++ b/apps/web/payments-new/payment.ts @@ -19,7 +19,11 @@ interface Metadata { export default interface Payment { setup: () => void; initiate: (obj: InitiateProps) => void; - verify: (event: any) => Promise; + verify: ( + event: any, + rawBody: string, + headers: Record, + ) => Promise; getPaymentIdentifier: (event: any) => unknown; getMetadata: (event: any) => Record; getName: () => string; diff --git a/apps/web/payments-new/razorpay-payment.ts b/apps/web/payments-new/razorpay-payment.ts index 7d93addca..7be36a7b2 100644 --- a/apps/web/payments-new/razorpay-payment.ts +++ b/apps/web/payments-new/razorpay-payment.ts @@ -3,6 +3,7 @@ import Payment, { InitiateProps } from "./payment"; import { responses } from "@config/strings"; import Razorpay from "razorpay"; import { getUnitAmount } from "./helpers"; +import crypto from "crypto"; const { payment_invalid_settings: paymentInvalidSettings, @@ -13,6 +14,7 @@ export default class RazorpayPayment implements Payment { public siteinfo: SiteInfo; public name: string; public razorpay: any; + private webhookSecret: string | undefined; constructor(siteinfo: SiteInfo) { this.siteinfo = siteinfo; @@ -33,6 +35,8 @@ export default class RazorpayPayment implements Payment { key_secret: this.siteinfo.razorpaySecret, }); + this.webhookSecret = this.siteinfo.razorpayWebhookSecret; + return this; } @@ -45,10 +49,40 @@ export default class RazorpayPayment implements Payment { return order.id; } - async verify(event) { + async verify( + event: any, + rawBody: string, + headers: Record, + ) { if (!event) { return false; } + + const signature = headers["x-razorpay-signature"]; + if (!signature || !this.webhookSecret) { + return false; + } + + const expectedSignature = crypto + .createHmac("sha256", this.webhookSecret) + .update(rawBody) + .digest("hex"); + try { + const expected = Buffer.from(expectedSignature); + const received = Buffer.from(signature); + if ( + expected.length !== received.length || + !crypto.timingSafeEqual( + new Uint8Array(expected), + new Uint8Array(received), + ) + ) { + return false; + } + } catch { + return false; + } + if ( event.event === "order.paid" && event.payload.order.entity.notes.membershipId diff --git a/apps/web/payments-new/stripe-payment.ts b/apps/web/payments-new/stripe-payment.ts index 9f8aa3112..000ccb9c7 100644 --- a/apps/web/payments-new/stripe-payment.ts +++ b/apps/web/payments-new/stripe-payment.ts @@ -18,6 +18,7 @@ export default class StripePayment implements Payment { public siteinfo: SiteInfo; public name: string; public stripe: any; + private webhookSecret: string | undefined; constructor(siteinfo: SiteInfo) { this.siteinfo = siteinfo; @@ -37,6 +38,8 @@ export default class StripePayment implements Payment { typescript: true, }); + this.webhookSecret = this.siteinfo.stripeWebhookSecret; + return this; } @@ -77,10 +80,27 @@ export default class StripePayment implements Payment { return this.siteinfo.currencyISOCode!; } - async verify(event: Stripe.Event) { - if (!event) { + async verify( + _event: any, + rawBody: string, + headers: Record, + ) { + const signature = headers["stripe-signature"]; + if (!signature || !this.webhookSecret) { + return false; + } + + let event: Stripe.Event; + try { + event = this.stripe.webhooks.constructEvent( + rawBody, + signature, + this.webhookSecret, + ); + } catch { return false; } + if ( event.type === "checkout.session.completed" && (event.data.object as any).payment_status === "paid" diff --git a/packages/orm-models/src/models/site-info.ts b/packages/orm-models/src/models/site-info.ts index 3430532a8..188198fd0 100644 --- a/packages/orm-models/src/models/site-info.ts +++ b/packages/orm-models/src/models/site-info.ts @@ -9,6 +9,7 @@ export const SettingsSchema = new mongoose.Schema({ currencyISOCode: { type: String, maxlength: 3 }, paymentMethod: { type: String, enum: Constants.paymentMethods }, stripeKey: { type: String }, + stripeWebhookSecret: { type: String }, codeInjectionHead: { type: String }, codeInjectionBody: { type: String }, stripeSecret: { type: String },