pike/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files 5c2d82ded0cf18c81c70d9f4a3f1de0b1688b125
67 lines · 2.4 KB · ~694 tok Raw History
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
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts