summaryrefslogtreecommitdiff
path: root/src/auth.tsx
blob: eef8b8ff34a3b1bfe7e0ba9bef55f78a7882a29d (plain) (blame)
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>);
});