Skip to content

Commit

Permalink
Refactor QStash signature verification to use raw request body
Browse files Browse the repository at this point in the history
- Updated multiple API routes to read the raw body from requests instead of parsing JSON directly.
- Modified the `verifyQstashSignature` function to accept the raw body for signature verification.
- Ensured consistent handling of request bodies across various endpoints, improving reliability in signature validation.
  • Loading branch information
devkiran committed Jan 13, 2025
1 parent c67670b commit a977192
Showing 13 changed files with 60 additions and 44 deletions.
7 changes: 4 additions & 3 deletions apps/web/app/api/cron/domains/configure-dns/route.ts
Original file line number Diff line number Diff line change
@@ -9,9 +9,10 @@ export const dynamic = "force-dynamic";
*/
export async function POST(req: Request) {
try {
const body = await req.json();
await verifyQstashSignature({ req, body });
const { domain } = body;
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const { domain } = JSON.parse(rawBody);

const res = await configureDNS({ domain });
console.log("Dynadot DNS configured.", res);
7 changes: 3 additions & 4 deletions apps/web/app/api/cron/domains/delete/route.ts
Original file line number Diff line number Diff line change
@@ -18,11 +18,10 @@ const schema = z.object({
// POST /api/cron/domains/delete
export async function POST(req: Request) {
try {
const body = await req.json();
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

await verifyQstashSignature({ req, body });

const { domain, workspaceId } = schema.parse(body);
const { domain, workspaceId } = schema.parse(JSON.parse(rawBody));

const domainRecord = await prisma.domain.findUnique({
where: {
9 changes: 5 additions & 4 deletions apps/web/app/api/cron/domains/transfer/route.ts
Original file line number Diff line number Diff line change
@@ -19,11 +19,12 @@ export const dynamic = "force-dynamic";

export async function POST(req: Request) {
try {
const body = await req.json();
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

await verifyQstashSignature({ req, body });

const { currentWorkspaceId, newWorkspaceId, domain } = schema.parse(body);
const { currentWorkspaceId, newWorkspaceId, domain } = schema.parse(
JSON.parse(rawBody),
);

const links = await prisma.link.findMany({
where: { domain, projectId: currentWorkspaceId },
9 changes: 5 additions & 4 deletions apps/web/app/api/cron/domains/update/route.ts
Original file line number Diff line number Diff line change
@@ -20,11 +20,12 @@ const pageSize = 100;
// POST /api/cron/domains/update
export async function POST(req: Request) {
try {
const body = await req.json();
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

await verifyQstashSignature({ req, body });

const { newDomain, oldDomain, workspaceId, page } = schema.parse(body);
const { newDomain, oldDomain, workspaceId, page } = schema.parse(
JSON.parse(rawBody),
);

const newDomainRecord = await prisma.domain.findUnique({
where: {
6 changes: 4 additions & 2 deletions apps/web/app/api/cron/import/bitly/route.ts
Original file line number Diff line number Diff line change
@@ -12,8 +12,10 @@ export const dynamic = "force-dynamic";

export async function POST(req: Request) {
try {
const body = await req.json();
await verifyQstashSignature({ req, body });
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const body = JSON.parse(rawBody);
const { workspaceId, bitlyGroup, importTags } = body;

try {
6 changes: 4 additions & 2 deletions apps/web/app/api/cron/import/csv/route.ts
Original file line number Diff line number Diff line change
@@ -45,8 +45,10 @@ type MapperResult =

export async function POST(req: Request) {
try {
const body = await req.json();
await verifyQstashSignature({ req, body });
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const body = JSON.parse(rawBody);
const { workspaceId, userId, id, url } = body;
const mapping = linkMappingSchema.parse(body.mapping);

6 changes: 4 additions & 2 deletions apps/web/app/api/cron/import/rebrandly/route.ts
Original file line number Diff line number Diff line change
@@ -10,8 +10,10 @@ export const dynamic = "force-dynamic";

export async function POST(req: Request) {
try {
const body = await req.json();
await verifyQstashSignature({ req, body });
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const body = JSON.parse(rawBody);
const { workspaceId, importTags } = body;

try {
6 changes: 4 additions & 2 deletions apps/web/app/api/cron/import/short/route.ts
Original file line number Diff line number Diff line change
@@ -10,8 +10,10 @@ export const dynamic = "force-dynamic";

export async function POST(req: Request) {
try {
const body = await req.json();
await verifyQstashSignature({ req, body });
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const body = JSON.parse(rawBody);
const {
workspaceId,
userId,
7 changes: 4 additions & 3 deletions apps/web/app/api/cron/links/delete/route.ts
Original file line number Diff line number Diff line change
@@ -11,9 +11,10 @@ export const dynamic = "force-dynamic";
*/
export async function POST(req: Request) {
try {
const body = await req.json();
await verifyQstashSignature({ req, body });
const { linkId } = body;
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const { linkId } = JSON.parse(rawBody);

const link = await prisma.link.findUnique({
where: {
6 changes: 3 additions & 3 deletions apps/web/app/api/cron/shopify/order-paid/route.ts
Original file line number Diff line number Diff line change
@@ -14,10 +14,10 @@ const schema = z.object({
// POST /api/cron/shopify/order-paid
export async function POST(req: Request) {
try {
const body = await req.json();
await verifyQstashSignature({ req, body });
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const { workspaceId, checkoutToken } = schema.parse(body);
const { workspaceId, checkoutToken } = schema.parse(JSON.parse(rawBody));

// Find Shopify order
const event = await redis.hget(
5 changes: 4 additions & 1 deletion apps/web/app/api/cron/usage/route.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,10 @@ async function handler(req: Request) {
if (req.method === "GET") {
await verifyVercelSignature(req);
} else if (req.method === "POST") {
await verifyQstashSignature({ req });
await verifyQstashSignature({
req,
rawBody: await req.text(),
});
}

await updateUsage();
7 changes: 3 additions & 4 deletions apps/web/app/api/cron/workspaces/delete/route.ts
Original file line number Diff line number Diff line change
@@ -15,11 +15,10 @@ const schema = z.object({
// POST /api/cron/workspaces/delete
export async function POST(req: Request) {
try {
const body = await req.json();
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

await verifyQstashSignature({ req, body });

const { workspaceId } = schema.parse(body);
const { workspaceId } = schema.parse(JSON.parse(rawBody));

const workspace = await prisma.project.findUnique({
where: {
23 changes: 13 additions & 10 deletions apps/web/lib/cron/verify-qstash.ts
Original file line number Diff line number Diff line change
@@ -9,26 +9,29 @@ const receiver = new Receiver({

export const verifyQstashSignature = async ({
req,
body,
bodyType = "json",
rawBody,
}: {
req: Request;
body?: any;
// due to a weird QStash bug, webhook URLs that have query params
// need to be verified with the text body type (instead of JSON)
bodyType?: "json" | "text";
rawBody: string; // Make sure to pass the raw body not the parsed JSON
}) => {
body = body || (bodyType === "json" ? await req.json() : await req.text());
const signature = req.headers.get("Upstash-Signature");

if (!signature) {
throw new DubApiError({
code: "bad_request",
message: "Upstash-Signature header not found.",
});
}

const isValid = await receiver.verify({
signature: req.headers.get("Upstash-Signature") || "",
body: bodyType === "json" ? JSON.stringify(body) : body,
signature,
body: rawBody,
});

if (!isValid) {
throw new DubApiError({
code: "unauthorized",
message: "Invalid QStash request signature",
message: "Invalid QStash request signature.",
});
}
};

0 comments on commit a977192

Please sign in to comment.