Skip to content
File

Blob: tests/worker/remote-ip.test.ts

typescript60 lines
1import { exports } from "cloudflare:workers";
2import { describe, expect, it } from "vitest";
3 
4import { ISSUER, testHeaders } from "./helpers";
5 
6const SELF = exports.default;
7 
8// remoteIp is the CF-Connecting-IP gate for every tessera-owned public
9// flow that runs Turnstile or rate-limit. It must:
10// - return the CF-Connecting-IP value when present
11// - reject 400 when absent (no fallback to "unknown" or X-Forwarded-For)
12// - ignore X-Forwarded-For entirely (forgeable on non-Cloudflare paths)
13 
14describe("remoteIp fail-closed contract", () => {
15 it("400 missing_client_ip when CF-Connecting-IP is absent on /api/sign-in", async () => {
16 const res = await SELF.fetch(`${ISSUER}/api/sign-in`, {
17 method: "POST",
18 headers: testHeaders(),
19 body: JSON.stringify({ email: "x@example.com", password: "y", turnstileToken: "z" }),
20 });
21 expect(res.status).toBe(400);
22 const body = (await res.json()) as { error?: string };
23 expect(body.error).toBe("missing_client_ip");
24 });
25 
26 it("ignores X-Forwarded-For (forgeable; not the Cloudflare contract)", async () => {
27 const res = await SELF.fetch(`${ISSUER}/api/sign-in`, {
28 method: "POST",
29 headers: testHeaders({ "X-Forwarded-For": "1.2.3.4" }),
30 body: JSON.stringify({ email: "x@example.com", password: "y", turnstileToken: "z" }),
31 });
32 expect(res.status).toBe(400);
33 const body = (await res.json()) as { error?: string };
34 expect(body.error).toBe("missing_client_ip");
35 });
36 
37 it("accepts CF-Connecting-IP and lets the request progress past the IP gate", async () => {
38 // Without a seeded user this still 401s on the credential check, but
39 // the gate has been crossed — proves remoteIp returned a real value.
40 const res = await SELF.fetch(`${ISSUER}/api/sign-in`, {
41 method: "POST",
42 headers: testHeaders({ "CF-Connecting-IP": "203.0.113.7" }),
43 body: JSON.stringify({ email: "ghost@example.com", password: "y", turnstileToken: "z" }),
44 });
45 // Whatever the downstream status, it is not the 400 missing_client_ip.
46 expect(res.status).not.toBe(400);
47 });
48 
49 it("400 missing_client_ip on POST /api/invite/:token (invite flow uses remoteIp too)", async () => {
50 const res = await SELF.fetch(`${ISSUER}/api/invite/never-existed`, {
51 method: "POST",
52 headers: testHeaders(),
53 body: JSON.stringify({ name: "x", password: "y", turnstileToken: "z" }),
54 });
55 expect(res.status).toBe(400);
56 const body = (await res.json()) as { error?: string };
57 expect(body.error).toBe("missing_client_ip");
58 });
59});