From d8671aa47dd0472b9779918a7a9cdddf9bfde355 Mon Sep 17 00:00:00 2001 From: Sam Anthony Date: Fri, 6 Mar 2026 16:31:54 -0500 Subject: doc: architecture and authentication --- .gitignore | 1 + Makefile | 7 ++++++- README | 2 ++ cmd/buthd/main.go | 16 ++++++++++++++++ cmd/webshopd/main.go | 15 --------------- doc/arch.dot | 18 ++++++++++++++++++ doc/arch.md | 35 +++++++++++++++++++++++++++++++++++ doc/arch.png | Bin 0 -> 41371 bytes doc/auth.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ doc/libs.md | 6 ++++++ doc/notation.md | 2 ++ 11 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 cmd/buthd/main.go delete mode 100644 cmd/webshopd/main.go create mode 100644 doc/arch.dot create mode 100644 doc/arch.md create mode 100644 doc/arch.png create mode 100644 doc/auth.md create mode 100644 doc/libs.md create mode 100644 doc/notation.md diff --git a/.gitignore b/.gitignore index 0314984..ef2d82e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/ +doc/.obsidian/ .hugo_build.lock public/ diff --git a/Makefile b/Makefile index 27685dc..5f7f1bd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: public bin/webshopd +all: doc public bin/webshopd public: hugo build @@ -6,5 +6,10 @@ public: bin/%: cmd/% go build -o $@ ./$< +doc: doc/arch.png + +doc/arch.png: doc/arch.dot + dot -Tpng <$< >$@ + clean: rm -rf bin public diff --git a/README b/README index 1e7bbb8..0602dd3 100644 --- a/README +++ b/README @@ -1 +1,3 @@ Web shop for shop.samanthony.xyz. + +Bùth is Gaelic for "shop". diff --git a/cmd/buthd/main.go b/cmd/buthd/main.go new file mode 100644 index 0000000..60ab749 --- /dev/null +++ b/cmd/buthd/main.go @@ -0,0 +1,16 @@ +// Buthd is the API server. +package main + +import ( + "fmt" + "html" + "log" + "net/http" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) + }) + log.Fatal(http.ListenAndServe(":8081", nil)) +} diff --git a/cmd/webshopd/main.go b/cmd/webshopd/main.go deleted file mode 100644 index 519a299..0000000 --- a/cmd/webshopd/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "html" - "log" - "net/http" -) - -func main() { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) - }) - log.Fatal(http.ListenAndServe(":8081", nil)) -} diff --git a/doc/arch.dot b/doc/arch.dot new file mode 100644 index 0000000..fa6f5aa --- /dev/null +++ b/doc/arch.dot @@ -0,0 +1,18 @@ +graph { + graph [rankdir=BT; nodesep=1.0; ranksep=1.0] + node [shape=box] + + client -- relay [label="HTTPS"] + client -- relay [label="9P/WS"] + relay -- shop [label="HTTP\nstatic html"] + relay --api [label="HTTP\nhtmx frags"] + relay -- api [label="9P/WS\napi"] + relay --auth [label="HTTP\nforms"] + api -- authfs [label="9P/TCP"] + auth --authfs [label="9P/TCP"] + + subgraph cluster_lan { + relay, shop, api, auth + authfs [shape=cylinder] + } +} diff --git a/doc/arch.md b/doc/arch.md new file mode 100644 index 0000000..faac44e --- /dev/null +++ b/doc/arch.md @@ -0,0 +1,35 @@ + +# Architecture + +Intended to be deployed on OpenBSD. + +- LAN + - shop.samanthony.xyz + - Basic web server, e.g. httpd(8) + - Serves static HTML files + - Serves scripts (js/wasm) including htmx.js + - api.shop.samanthony.xyz + - `buthapi` API server + - Serves htmx fragments + - Serves 9P {/cart, /checkout} to authenticated clients via websockets + - auth.shop.samanthony.xyz + - `buthauth` web authentication gateway + - Client-facing HTTP interface to authfs + - Handles registration and login forms + - authfs + - `buthauthfs` daemon + - Persistent user database + - Stores password hashes + - Manages client sessions + - Serves 9P to api and auth servers + - relay + - relayd(8) + - TLS proxy/gateway +- WAN + - Client web browser + - HTML renderer, js/wasm interpreter + - Generates and stores its session ID (in a cookie) + +The LAN could be either a single OpenBSD host, several vmd(8) VMs, or several machines in a VPN, e.g. Tailscale. + +![[arch.png]] diff --git a/doc/arch.png b/doc/arch.png new file mode 100644 index 0000000..d9cb9f0 Binary files /dev/null and b/doc/arch.png differ diff --git a/doc/auth.md b/doc/auth.md new file mode 100644 index 0000000..a8aeb25 --- /dev/null +++ b/doc/auth.md @@ -0,0 +1,50 @@ +# Authentication + +## Registration flow +This is how a new user registers himself in `authfs`, after which he can open sessions. +- Client `GET`s `shop./register.html` +- Client enters username and password, `POST`s form to `auth./register` +- `buthauth` server registers user in `authfs` + - Creates `/users//` + - If it already exists, the username is taken; return error to client + - Writes password to `/users//passwd` +- `authfs` ingests and hashes the password. Subsequent reads of `/users//passwdhash` will return the hash (`authfs` discards the cleartext password after it is hashed). +- If successful, client can now login to obtain a session + +## Login flow +This is how a client authenticates itself and creates a _session_. A session gives the client a shopping cart and access to the checkout. +- Client `GET`s `shop./login.html`. +- Client enters username and password (assume they already have an account). +- Js or wasm script running in client generates a 256-bit session ID and injects it into the login form. +- Client `POST`s the login form to `auth./login` +- `buthauth` writes the password to `/users//login` in `authfs` + - If `/users//` doesn't exist, the username is invalid; fail the login +- `buthauth` reads from `/users//login`. +- If the password was correct, `authfs` returns a session ID corresponding to `/sessions// + - Otherwise if the password was wrong, `authfs` returns `Rerror`. `buthauth` fails the login, returning an error to the client +- `buthauth` returns a session cookie, containing the session ID, to the client (with proper security params: HttpOnly etc.) +- Client includes the session cookie in subsequent requests, giving it elevated privileges. + +On the client, session is a _session cookie_ (no expiry date). On the server, it should expire after `lastseen+t`. `authfs` periodically sweeps the sessions and removes old ones. `authfs` updates a sessions's last-seen time whenever it gets a request on `/sessions//*`. + +## API usage +This is how a client accesses the 9P API once he's logged in (has a session cookie). +- Client `GET`s `shop./foo.html` + - Includes cookie in request +- Httpd returns static page and scripts +- Script opens websocket on `api./ws` +- Browser sends cookie automatically in upgrade request +- `buthapi` validates session (using cookie) by checking if `/sessions//` exists on `authfs`. + - If not, the session is invalid, return 401 +- `buthapi` upgrades the websocket connection and serves 9P +- Client uses ws/9p connection to access API + - E.g. pressing "add to cart" button does `Tcreate /cart/sku123` + - (will need to figure out how to wire this up with htmx/js and 9p js/wasm lib) + +## Logout flow +This is how a client destroys an active session on the server. +- Client clicks Logout button +- Htmx `POST`s to `auth./logout`, includes the session cookie in the request +- `buthauth` reads the session ID from the cookie +- `buthauth` removes `/sessions/` on `authfs` +- `buthauth` returns a response header telling the client browser to destroy the session cookie. diff --git a/doc/libs.md b/doc/libs.md new file mode 100644 index 0000000..3934bf4 --- /dev/null +++ b/doc/libs.md @@ -0,0 +1,6 @@ +# Libraries + +https://github.com/acaloiaro/hugo-htmx-go-template +https://github.com/docker-archive/go-p9p +https://github.com/gorilla/websocket +https://github.com/alexedwards/argon2id diff --git a/doc/notation.md b/doc/notation.md new file mode 100644 index 0000000..1b1303e --- /dev/null +++ b/doc/notation.md @@ -0,0 +1,2 @@ +# Notation +- `api./foo` is short for `api.shop.samanthony.xyz/foo` \ No newline at end of file -- cgit v1.2.3