Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Readme

LastLogin Authentication

Add user auth to your app via LastLogin. LastLogin is a hosted auth provider that enables login through email, Google, Github, etc.

How to setup

These instructions were written to be easily copy-and-pasteable into LLMs like Townie.

  1. import { lastlogin } from "https://esm.town/v/stevekrouse/lastlogin_safe";
  2. Wrap your HTTP handler in it, ie export default lastLogin(handler)
  3. In your handler, redirect to /auth/login or /auth/logout to trigger those flows.
  4. In your HTTP handler, read the X-LastLogin-Email header, ie const email = req.headers.get("X-LastLogin-Email")
  5. If the user is logged in, you now have a email you can work with. If not, it will be empty

Screenshot 2024-08-08 at 08.48.41.gif

Live demo

How it works:

  1. Your users click on a link to /auth/login in your app
  2. This middleware directs them to login via LastLogin
  3. They authenticate to LastLogin
  4. LastLogin redirects them back to your app
  5. This middleware "logs them in" to your app by giving them a session cookie.
  6. In your app, you can read the X-LastLogin-Email header to see which (if any) user is logged in

Notes

  • If you want username & password auth: @stevekrouse/lucia_middleware
  • This middleware stores sessions in the lastlogin_session table in your Val Town SQLite
  • This val has NOT been properly audited for security. I am not a security expert. I would suggest only using it for demos, prototypes, or apps where security is not paramount. If you are a security expert, I would appreciate your help auditing this!

Todos

  • Let the user customize the name of the SQLite table
  • Get a proper security audit for this
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
/** @jsxImportSource npm:hono/jsx */
async function createSession(email: string, hostname: string) {
const { zip } = await import("https://esm.town/v/pomdtr/sql");
const { sqlite } = await import("https://esm.town/v/std/sqlite");
const sessionID = crypto.randomUUID();
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 14);
await sqlite.batch([
`CREATE TABLE IF NOT EXISTS lastlogin_session (email TEXT, sessionID TEXT, hostname TEXT, expiresAt INTEGER)`,
{
sql: `DELETE FROM lastlogin_session WHERE email=? & hostname=?`,
args: [email, hostname],
},
{
sql: `INSERT INTO lastlogin_session VALUES (?, ?, ?, ?)`,
args: [email, sessionID, hostname, expiresAt.getTime()],
},
]);
return sessionID;
}
async function getSession(sessionID: string, hostname: string) {
const { zip } = await import("https://esm.town/v/pomdtr/sql");
const { sqlite } = await import("https://esm.town/v/std/sqlite");
try {
const res = await sqlite.execute({
sql: `SELECT * FROM lastlogin_session WHERE sessionID=? AND hostname=?`,
args: [sessionID, hostname],
});
const rows = zip(res);
if (rows.length === 0) {
return null;
}
const session = rows[0];
if (session.expiresAt < Date.now()) {
await sqlite.execute({
sql: `DELETE FROM lastlogin_session WHERE sessionID=?`,
args: [sessionID],
});
return null;
}
return session;
} catch (_) {
return null;
}
}
async function deleteSession(sessionID: string) {
const { sqlite } = await import("https://esm.town/v/std/sqlite");
await sqlite.execute({
sql: `DELETE FROM lastlogin_session WHERE sessionID=?`,
args: [sessionID],
});
}
const SESSION_COOKIE = "lastlogin_session";
const OAUTH_COOKIE = "oauth_store";
export function lastlogin(
handler: (req: Request) => Response | Promise<Response>,
) {
return async (req: Request) => {
const { api } = await import("https://esm.town/v/pomdtr/api");
const { deleteCookie, getCookies, setCookie } = await import("jsr:@std/http/cookie");
const url = new URL(req.url);
const clientID = `${url.protocol}//${url.host}/`;
const redirectUri = `${url.protocol}//${url.host}/auth/callback`;
if (url.pathname == "/auth/callback") {
const cookies = await getCookies(req.headers);
const store = JSON.parse(decodeURIComponent(cookies[OAUTH_COOKIE]));
const state = url.searchParams.get("state");
if (!state || state != store.state) {
return new Response("state mismatch", { status: 400 });
}
const code = url.searchParams.get("code");
if (!code) {
return new Response("code not found", { status: 400 });
}
const tokenUrl = new URL("https://lastlogin.io/token");
tokenUrl.searchParams.set("client_id", clientID);
tokenUrl.searchParams.set("code", code);
tokenUrl.searchParams.set("redirect_uri", redirectUri);
tokenUrl.searchParams.set("response_type", "code");
tokenUrl.searchParams.set("state", store.state);
const tokenResp = await fetch(tokenUrl.toString());
if (!tokenResp.ok) {
throw new Error(await tokenResp.text());
}
September 19, 2024