1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
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 = () => <section class="register-form">
<button hx-post="/auth/register-begin" hx-target="closest .register-form" hx-swap="outerHTML">register</button>
<button hx-post="/auth/login-begin" hx-target="closest .register-form" hx-swap="outerHTML">login</button>
</section>;
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(
<section
class="register-form"
_={`
on load
call SimpleWebAuthnBrowser.startRegistration(${JSON.stringify(options)})
fetch /auth/register-finish with method:"POST", body:JSON.stringify({ resp: it, username: ${JSON.stringify(username)} })
put the result into my.outerHTML
`}
/>
);
});
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(<p style="color: red">Could not verify registration response!</p>);
await db.insert(userTable).values({ name: username, passkey: stringify(r.registrationInfo), passkeyId: r.registrationInfo!.credential.id });
return c.html(
<p>You now have an account!</p>
);
});
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(
<section
class="register-form"
_={`
on load
call SimpleWebAuthnBrowser.startAuthentication(${JSON.stringify(options)})
fetch /auth/login-finish with method:"POST", body:JSON.stringify({ resp: it, key: ${JSON.stringify(key)} })
put the result into my.outerHTML
`}
/>
);
});
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(<p style="color: red">Who are you?</p>);
let r = await swa.verifyAuthenticationResponse({
response: resp,
expectedChallenge: chall,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
requireUserVerification: false,
credential: parse<swa.VerifiedRegistrationResponse["registrationInfo"]>(passkey)!.credential,
});
if (!r.verified) return c.html(<p style="color: red">Could not verify authentication response!</p>);
let uuid = randomUUID();
await db.insert(sessionTable).values({ userId, uuid });
setCookie(c, "session", uuid);
return c.html(<p>Logged in!</p>);
});
|