-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.ts
More file actions
79 lines (69 loc) · 3.13 KB
/
Copy pathauth.ts
File metadata and controls
79 lines (69 loc) · 3.13 KB
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
// Sign-In With Ethereum (EIP-4361) — server side.
// Zero extra deps: viem/siwe for the message + verification, node:crypto for a
// signed (HMAC) session cookie. The canonical "why a web3 app needs a server"
// feature: the signature is verified server-side and a session is issued.
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServerFn } from "@tanstack/react-start";
import { deleteCookie, getCookie, setCookie } from "@tanstack/react-start/server";
import { type Address, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
import { generateSiweNonce, parseSiweMessage, verifySiweMessage } from "viem/siwe";
const NONCE_COOKIE = "siwe-nonce";
const SESSION_COOKIE = "siwe-session";
// Demo default — set SESSION_SECRET to a long random value in production.
const SECRET = process.env.SESSION_SECRET ?? "dev-only-insecure-change-me";
const COOKIE_OPTS = { httpOnly: true, sameSite: "lax", path: "/" } as const;
function sign(value: string): string {
return createHmac("sha256", SECRET).update(value).digest("hex");
}
function readSession(): Address | null {
const raw = getCookie(SESSION_COOKIE);
if (!raw) return null;
const dot = raw.lastIndexOf(".");
if (dot < 0) return null;
const address = raw.slice(0, dot);
const sig = raw.slice(dot + 1);
const expected = sign(address);
if (sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return null;
}
return address as Address;
}
/** Issue a fresh nonce (stored in an httpOnly cookie for the verify step). */
export const getSiweNonce = createServerFn().handler(() => {
const nonce = generateSiweNonce();
setCookie(NONCE_COOKIE, nonce, { ...COOKIE_OPTS, maxAge: 300 });
return nonce;
});
/** Verify the signed SIWE message, then issue a signed session cookie. */
export const verifySiwe = createServerFn({ method: "POST" })
.inputValidator((d: { message: string; signature: `0x${string}` }) => d)
.handler(async ({ data }) => {
const expectedNonce = getCookie(NONCE_COOKIE);
const { nonce, address } = parseSiweMessage(data.message);
if (!expectedNonce || nonce !== expectedNonce) {
throw new Error("Invalid or expired nonce — try signing in again.");
}
// EOA verification is offline; a client is only used for ERC-1271 smart accounts.
const client = createPublicClient({ chain: mainnet, transport: http() });
const valid = await verifySiweMessage(client, {
message: data.message,
signature: data.signature,
});
if (!valid || !address) throw new Error("Signature verification failed.");
deleteCookie(NONCE_COOKIE, COOKIE_OPTS);
setCookie(SESSION_COOKIE, `${address}.${sign(address)}`, {
...COOKIE_OPTS,
maxAge: 60 * 60 * 24 * 7,
});
return { address };
});
/** Read the current session (verified server-side from the signed cookie). */
export const getSession = createServerFn().handler(() => {
return { address: readSession() };
});
/** Clear the session cookie. */
export const signOut = createServerFn({ method: "POST" }).handler(() => {
deleteCookie(SESSION_COOKIE, COOKIE_OPTS);
return { address: null };
});