Files
DiscordClone/convex/authGuard.ts
Bryan1029384756 6813bb40dc
All checks were successful
Build and Release / build-and-release (push) Successful in 20m36s
1.1.00
2026-04-16 20:14:27 -05:00

64 lines
1.7 KiB
TypeScript

"use node";
import crypto from "crypto";
import type { ActionCtx } from "./_generated/server";
import { internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
/**
* Detached Ed25519 check: caller must sign `canonicalMessage` with the
* Ed25519 private key that pairs with the userProfile's stored
* `publicSigningKey`. Matches `voice.getToken` and
* `recovery.resetPasswordAction` — same window, same SPKI/PEM encoding,
* same hex signature format — so client-side signing code looks
* identical across every sensitive call.
*
* Throws on failure. Sensitive mutations that previously trusted a raw
* `userId` arg should now route through a "use node" action that calls
* this helper before running the internal mutation.
*/
export async function requireAuth(
ctx: ActionCtx,
userId: Id<"userProfiles">,
timestamp: number,
signature: string,
canonicalMessage: string,
): Promise<void> {
if (
!Number.isFinite(timestamp) ||
Math.abs(Date.now() - timestamp) > MAX_CLOCK_SKEW_MS
) {
throw new Error("Request expired. Please try again.");
}
const user = await ctx.runQuery(internal.auth.getUserForVoiceToken, {
userId,
});
if (!user) {
throw new Error("User not found");
}
let isValid = false;
try {
const publicKeyObj = crypto.createPublicKey({
key: user.publicSigningKey,
format: "pem",
type: "spki",
});
isValid = crypto.verify(
null,
Buffer.from(canonicalMessage),
publicKeyObj,
Buffer.from(signature, "hex"),
);
} catch {
throw new Error("Signature verification failed");
}
if (!isValid) {
throw new Error("Invalid signature");
}
}