From 815de9906a014c2eb1a4fe2bd8cf1b3077f03c9c Mon Sep 17 00:00:00 2001 From: Mathias Magnusson Date: Thu, 14 Aug 2025 18:42:27 +0200 Subject: Add passkey authentication --- src/auth.tsx | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/auth.tsx (limited to 'src/auth.tsx') diff --git a/src/auth.tsx b/src/auth.tsx new file mode 100644 index 0000000..ebda74e --- /dev/null +++ b/src/auth.tsx @@ -0,0 +1,93 @@ +import { Hono } from "hono"; +import * as swa from "@simplewebauthn/server"; +import { randomUUID } from "node:crypto"; +import { eq } 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 { setCookie } from "hono/cookie"; + +let app = new Hono(); + +export const LoginForm = () =>
+ + +
; + +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!

); +}); + +export default app; -- cgit v1.2.3