Avatar

hlobil

3 public vals
Joined May 25, 2023

Passkeys Demo

Passkeys are pretty neat! I wanted to get a demo working in Val Town so I ported over https://github.com/maximousblk/passkeys-demo.

One challenge was that the original extensively uses DenoKV store with compound keys and values. I created @stevekrouse/DenoSyntheticKV as a replacement for DenoKV. It uses SuperJSON to encode the keys and values.

You can find the client-side script for the main page here: @stevekrouse/passkey_script

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@3/jsx */
import { deleteCookie, getSignedCookie, setSignedCookie } from "https://deno.land/x/hono@v3.6.3/middleware.ts";
import { Hono } from "https://deno.land/x/hono@v3.6.3/mod.ts";
import { jwtVerify, SignJWT } from "https://deno.land/x/jose@v4.14.6/index.ts";
import {
generateAuthenticationOptions,
generateRegistrationOptions,
verifyAuthenticationResponse,
verifyRegistrationResponse,
} from "https://deno.land/x/simplewebauthn@v10.0.0/deno/server.ts";
import { isoBase64URL, isoUint8Array } from "https://deno.land/x/simplewebauthn@v10.0.0/deno/server/helpers.ts";
import type {
AuthenticationResponseJSON,
RegistrationResponseJSON,
} from "https://deno.land/x/simplewebauthn@v10.0.0/deno/typescript-types.ts";
import { DenoSyntheticKV } from "https://esm.town/v/stevekrouse/DenoSyntheticKV";
// CONSTANTS
const SECRET = new TextEncoder().encode(Deno.env.get("JWT_SECRET") ?? "development");
const RP_ID = "stevekrouse-passkeys_demo.web.val.run";
const RP_NAME = Deno.env.get("WEBAUTHN_RP_NAME") ?? "Deno Passkeys Demo";
const CHALLENGE_TTL = Number(Deno.env.get("WEBAUTHN_CHALLENGE_TTL")) || 60_000;
// UTILS
function generateJWT(userId: string) {
return new SignJWT({ userId }).setProtectedHeader({ alg: "HS256" }).sign(SECRET);
}
function verifyJWT(token: string) {
return jwtVerify(token, SECRET);
}
function generateRandomString() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
// DATABASE
const kv = new DenoSyntheticKV("passkeys_example");
type User = {
username: string;
data: string;
credentials: Record<string, Credential>;
};
type Credential = {
credentialID: Uint8Array;
credentialPublicKey: Uint8Array;
counter: number;
};
type Challenge = true;
// RP SERVER
const app = new Hono();
app.get("/", c =>
c.html(
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passkeys Demo</title>
<link rel="icon" href="https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/🦕_color.svg" />
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/bamboo.css" />
</head>
<body>
<h1>🦕 Passkeys Demo</h1>
<p>
<a href="https://www.val.town/v/stevekrouse/passkeys_demo">View code</a> on Val Town. Port of{" "}
<a href="https://github.com/maximousblk/passkeys-demo
">
maximousblk/passkeys-demo
</a>.
</p>
<p id="passkeys_check">Passkeys are not supported! ❌</p>
<noscript>
<blockquote>
<p>⚠️ Passkeys require JavaScript to work.</p>
</blockquote>
</noscript>
<form>
<fieldset id="auth" disabled>
<legend>Login</legend>
<label for="name">
Name <span style="opacity: 0.5">(Optional)</span>
</label>
<input type="text" id="name" name="name" autocomplete="username webauthn" placeholder="Anon" />
<hr />
<button type="button" id="register" onclick="handleRegister()">Register</button>
<button type="button" id="login" onclick="handleLogin()">Login</button>
<button type="button" id="logout" onclick="handleLogout()">Logout</button>

A minimal bookmarking tool

This allows you to bookmark links and view them later. Completely powered by ValTown and SQLite.

To set this up for yourself

  1. Fork the val
  2. From your ValTown settings page, add an environment variable named bookmarks_client_id and give it a value (you will be using this for saving)
  3. Add another environment variable named bookmarks_client_secret and give it a value (you will also be using this for saving)
  4. At first, the "bookmarks" table will not exist, so we need to save an article first, which will create the "bookmarks" table
  5. To do this, add a bookmarklet to your browser with this value (replace BOOKMARKS-CLIENT-ID and BOOKMARKS-CLIENT-SECRET with the values you added to the environment variables, and replace BOOKMARKS-URL with your VAL's URL):
javascript:void(open('BOOKMARKS-URL/save?u='+encodeURIComponent(location.href)+'&t='+encodeURIComponent(document.title)+'&id=BOOKMARKS-CLIENT-ID&secret=BOOKMARKS-CLIENT-SECRET', 'Bookmark a link', 'width=400,height=450'))
  1. Click this bookmarklet to bookmark the URL of the current active tab
  2. Go to your VAL URL homepage to see the bookmark

Demo

Here are my bookmarks: https://ramkarthik-bookmark.web.val.run/

Note

Make sure you don't share bookmarks_client_id and bookmarks_client_secret. It is used for authentication before saving a bookmark.

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
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { Hono } from "npm:hono@3";
const app = new Hono();
app.get("/", async (c) => {
var page = parseInt(c.req.query("page") || "0");
var offset = 0;
if (page && page > 0) {
offset = page * 10;
}
var bookmarks = await sqlite.execute({
sql: "select title, url from bookmarks order by created_at desc limit 10 offset :offset",
args: { offset: offset },
});
var totalBookmarkRows = await sqlite.execute("select count(1) from bookmarks order by created_at desc");
var totalBookmarks = parseInt(totalBookmarkRows.rows[0][0].toString());
var pagesCount = Math.floor(
((totalBookmarks % 10) == 0 && totalBookmarks / 10 > 0)
? ((totalBookmarks / 10) - 1)
: (totalBookmarks / 10),
);
var bookmarksList = "";
for (var i = 0; i < bookmarks.rows.length; i++) {
bookmarksList += "<p>" + (page * 10 + i + 1) + ". <a href=\"" + bookmarks.rows[i][1] + "\">" + bookmarks.rows[i][0]
+ "</a></p>";
}
var pagination = "<div style=\"flex-direction: row; width: 100%;justify-content: space-between;\">";
if (page > 0) {
pagination += "<a href=\"?page=" + (page - 1) + "\"> < prev </a>";
}
if (page < pagesCount) {
pagination += "<a href=\"?page=" + (page + 1) + "\"> next > </a>";
}
pagination += "</div>";
const html = `<html>
<head>
<title>My reading list</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
</head>
<body style="width:70%; margin-left:15%">
<h1>Reading list</h1>`
+ bookmarksList
+ pagination + `</body>
</html>`;
return new Response(
html,
{
headers: {
"Content-Type": "text/html",
},
},
);
});
app.get("/save", async (c) => {
const id = c.req.query("id");
const secret = c.req.query("secret");
const title = c.req.query("t");
const url = c.req.query("u");
if (!id && !secret) {
return c.text("Authentication details (ID/Secret) missing!");
}
if (id != Deno.env.get("bookmarks_client_id") || secret != Deno.env.get("bookmarks_client_secret")) {
return c.text("Unauthorized!");
}
if (!url) {
return c.text("URL missing!");
}
const create_table = await sqlite.execute(
"CREATE TABLE IF NOT EXISTS bookmarks (title TEXT, url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)",
);
const res = await sqlite.execute({
sql: `insert into bookmarks (title, url) values (:title, :url)`,
args: { title: title, url: url },
});
return c.text("Saved!");
});
export default app.fetch;

SQLite Admin

This is a lightweight SQLite Admin interface to view and debug your SQLite data.

Screenshot 2023-12-08 at 13.35.04.gif

It's currently super limited (no pagination, editing data, data-type specific viewers), and is just a couple dozens lines of code over a couple different vals. Forks encouraged! Just comment on the val if you add any features that you want to share.

To use it on your own Val Town SQLite database, fork it to your account.

It uses basic authentication with your Val Town API Token as the password (leave the username field blank).

1
2
3
4
5
6
7
8
9
10
11
import { basicAuth } from "https://esm.town/v/pomdtr/basicAuth?v=38";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { html } from "https://esm.town/v/stevekrouse/html";
import { sqlite_admin_table } from "https://esm.town/v/stevekrouse/sqlite_admin_table";
import { sqlite_admin_tables } from "https://esm.town/v/stevekrouse/sqlite_admin_tables";
import { Hono } from "npm:hono@3.9.2";
const app = new Hono();
app.get("/", async (c) => c.html(await sqlite_admin_tables()));
app.get("/:table", async (c) => c.html(await sqlite_admin_table(c.req.param("table"))));
export default basicAuth(app.fetch);
Next