import { Hono } from "hono"; import * as swa from "@simplewebauthn/server"; import { randomUUID } from "node:crypto"; import { and, eq, gt, sql } from "drizzle-orm"; import { RP_ID, ORIGIN, db } from "./index.js"; import { sessionTable, userTable, webauthnChallenges } from "./db/schema.js"; import { stringify, parse } from "superjson"; import { getCookie, setCookie } from "hono/cookie"; import type { Context } from "hono"; export const LoginForm = () =>
; export async function getSession(c: Context) { let sessionId = getCookie(c, "session"); if (!sessionId) return null; let [result] = await db .select() .from(sessionTable) .innerJoin(userTable, eq(userTable.id, sessionTable.userId)) .where(({ sessions: session }) => and(eq(session.uuid, sessionId), gt(session.lastUse, sql`unixepoch() - ${60 * 60 * 24 * 7}`))); if (!result) return null; await db.update(sessionTable).set({ lastUse: sql`unixepoch()` }).where(eq(sessionTable.id, result.sessions.id)); return { user: result.users, lastUse: result.sessions.lastUse, uuid: result.sessions.uuid }; } let app = new Hono(); export default app; app.post("/register-begin", async c => { const username = randomUUID(); let options = await swa.generateRegistrationOptions({ rpName: "uneven", rpID: RP_ID, userName: username, authenticatorSelection: { residentKey: "required", userVerification: "preferred", }, }); await db.insert(webauthnChallenges).values({ key: "register:" + username, challenge: options.challenge }); return c.html(
); }); app.post("/register-finish", async c => { let { resp, username } = await c.req.json(); let [{ chall }] = await db.delete(webauthnChallenges).where(eq(webauthnChallenges.key, "register:" + username)).returning({ chall: webauthnChallenges.challenge }); let r = await swa.verifyRegistrationResponse({ response: resp, expectedChallenge: chall, expectedOrigin: ORIGIN }); if (!r.verified || !("registrationInfo" in r)) return c.html(

Could not verify registration response!

); await db.insert(userTable).values({ name: username, passkey: stringify(r.registrationInfo), passkeyId: r.registrationInfo!.credential.id }); return c.html(

You now have an account!

); }); app.post("/login-begin", async c => { let options = await swa.generateAuthenticationOptions({ rpID: RP_ID, userVerification: "preferred", }); let key = randomUUID(); await db.insert(webauthnChallenges).values({ challenge: options.challenge, key: "login:" + key }); return c.html(
); }); app.post("/login-finish", async c => { let { resp, key } = await c.req.json(); let [{ chall }] = await db.delete(webauthnChallenges).where(eq(webauthnChallenges.key, "login:" + key)).returning({ chall: webauthnChallenges.challenge }); let [{ id: userId, passkey }] = await db.select().from(userTable).where(user => eq(user.passkeyId, resp.id)); if (!passkey) return c.html(

Who are you?

); let r = await swa.verifyAuthenticationResponse({ response: resp, expectedChallenge: chall, expectedOrigin: ORIGIN, expectedRPID: RP_ID, requireUserVerification: false, credential: parse(passkey)!.credential, }); if (!r.verified) return c.html(

Could not verify authentication response!

); let uuid = randomUUID(); await db.insert(sessionTable).values({ userId, uuid }); setCookie(c, "session", uuid); return c.html(

Logged in!

); });