This commit is contained in:
63
convex/authGuard.ts
Normal file
63
convex/authGuard.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
"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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user