pike/glyph Public Public: anyone can read this; nobody can change it.
forked from apps/glyph
workbenchthis machine
Files pike/3-events-leaks-every-user-s-drawings-to-ev
| 1 | /** |
| 2 | * Who is asking. The forge signs every request it proxies to the app: |
| 3 | * |
| 4 | * x-forge-identity: <userId>|<username>|<role>|<expiresMs> |
| 5 | * x-forge-signature: base64url(HMAC-SHA256(FORGE_APP_IDENTITY_KEY, that string)) |
| 6 | * |
| 7 | * `role` is `user` (signed in and allowed to see this app) or `admin` |
| 8 | * (allowed to write its repository). The forge strips these two headers |
| 9 | * from whatever a client sends, so a signature that verifies is the only |
| 10 | * trust there is: no headers, no key, a bad signature or a stamp in the |
| 11 | * past all mean `anon`. Nothing here reads a cookie. |
| 12 | */ |
| 13 | |
| 14 | import { createHmac, timingSafeEqual } from "node:crypto"; |
| 15 | |
| 16 | export type Who = |
| 17 | | { role: "anon" } |
| 18 | | { role: "user" | "admin"; id: string; name: string }; |
| 19 | |
| 20 | export const ANON: Who = { role: "anon" }; |
| 21 | |
| 22 | export function who(request: Request, key: string, now = Date.now()): Who { |
| 23 | const identity = request.headers.get("x-forge-identity"); |
| 24 | const signature = request.headers.get("x-forge-signature"); |
| 25 | if (key === "" || identity === null || signature === null) return ANON; |
| 26 | if (!same(sign(identity, key), signature)) return ANON; |
| 27 | const parts = identity.split("|"); |
| 28 | if (parts.length !== 4) return ANON; |
| 29 | const [id, name, role, expires] = parts as [string, string, string, string]; |
| 30 | if (role !== "user" && role !== "admin") return ANON; |
| 31 | if ( |
| 32 | id === "" || |
| 33 | name === "" || |
| 34 | !/^\d+$/.test(expires) || |
| 35 | Number(expires) <= now |
| 36 | ) |
| 37 | return ANON; |
| 38 | return { role, id, name }; |
| 39 | } |
| 40 | |
| 41 | /** The signature the forge computes. Exported so a test can be the forge. */ |
| 42 | export const sign = (identity: string, key: string): string => |
| 43 | createHmac("sha256", key).update(identity).digest("base64url"); |
| 44 | |
| 45 | const same = (a: string, b: string): boolean => { |
| 46 | const x = Buffer.from(a); |
| 47 | const y = Buffer.from(b); |
| 48 | return x.length === y.length && timingSafeEqual(x, y); |
| 49 | }; |
| 50 | |
| 51 | /** |
| 52 | * The person at the terminal. A tty session is started by the forge itself, |
| 53 | * so its environment is trusted: `FORGE_APP_USER` is the account at the |
| 54 | * keyboard (empty for a guest), `FORGE_APP_USER_ID` and `FORGE_APP_USER_ROLE` |
| 55 | * say the rest when the forge knows them. |
| 56 | */ |
| 57 | export function whoAtTty( |
| 58 | env: Record<string, string | undefined> = process.env, |
| 59 | ): Who { |
| 60 | const name = env.FORGE_APP_USER ?? ""; |
| 61 | if (name === "") return ANON; |
| 62 | return { |
| 63 | role: env.FORGE_APP_USER_ROLE === "admin" ? "admin" : "user", |
| 64 | id: env.FORGE_APP_USER_ID ?? name, |
| 65 | name, |
| 66 | }; |
| 67 | } |