Some checks failed
Docker Deploy / build-and-push (push) Has been cancelled
103 lines
2.7 KiB
TypeScript
103 lines
2.7 KiB
TypeScript
import type { APIRoute } from "astro";
|
|
import { verifyAuthenticationResponse } from "@simplewebauthn/server";
|
|
import { db } from "../../../../../db";
|
|
import { users, passkeys, passkeyChallenges } from "../../../../../db/schema";
|
|
import { eq, and, gt } from "drizzle-orm";
|
|
import { createSession } from "../../../../../lib/auth";
|
|
|
|
export const POST: APIRoute = async ({ request, cookies }) => {
|
|
const body = await request.json();
|
|
const { id } = body;
|
|
|
|
const passkey = await db.query.passkeys.findFirst({
|
|
where: eq(passkeys.id, id),
|
|
});
|
|
|
|
if (!passkey) {
|
|
return new Response(JSON.stringify({ error: "Passkey not found" }), {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.id, passkey.userId),
|
|
});
|
|
|
|
if (!user) return new Response(null, { status: 400 });
|
|
|
|
const clientDataJSON = Buffer.from(
|
|
body.response.clientDataJSON,
|
|
"base64url",
|
|
).toString("utf-8");
|
|
const clientData = JSON.parse(clientDataJSON);
|
|
const challenge = clientData.challenge;
|
|
|
|
const dbChallenge = await db.query.passkeyChallenges.findFirst({
|
|
where: and(
|
|
eq(passkeyChallenges.challenge, challenge),
|
|
gt(passkeyChallenges.expiresAt, new Date()),
|
|
),
|
|
});
|
|
|
|
if (!dbChallenge) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Invalid or expired challenge" }),
|
|
{
|
|
status: 400,
|
|
},
|
|
);
|
|
}
|
|
|
|
let verification;
|
|
try {
|
|
verification = await verifyAuthenticationResponse({
|
|
response: body,
|
|
expectedChallenge: challenge as string,
|
|
expectedOrigin: new URL(request.url).origin,
|
|
expectedRPID: new URL(request.url).hostname,
|
|
credential: {
|
|
id: passkey.id,
|
|
publicKey: new Uint8Array(Buffer.from(passkey.publicKey, "base64")),
|
|
counter: passkey.counter,
|
|
transports: passkey.transports
|
|
? JSON.parse(passkey.transports)
|
|
: undefined,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
if (verification.verified) {
|
|
const { authenticationInfo } = verification;
|
|
|
|
await db
|
|
.update(passkeys)
|
|
.set({
|
|
counter: authenticationInfo.newCounter,
|
|
lastUsedAt: new Date(),
|
|
})
|
|
.where(eq(passkeys.id, passkey.id));
|
|
|
|
const { sessionId, expiresAt } = await createSession(user.id);
|
|
|
|
cookies.set("session_id", sessionId, {
|
|
path: "/",
|
|
httpOnly: true,
|
|
secure: import.meta.env.PROD,
|
|
sameSite: "lax",
|
|
expires: expiresAt,
|
|
});
|
|
|
|
await db
|
|
.delete(passkeyChallenges)
|
|
.where(eq(passkeyChallenges.challenge, challenge));
|
|
|
|
return new Response(JSON.stringify({ verified: true }));
|
|
}
|
|
|
|
return new Response(JSON.stringify({ verified: false }), { status: 400 });
|
|
};
|