File
Blob: tests/worker/remote-ip.test.ts
| 1 | import { exports } from "cloudflare:workers"; |
| 2 | import { describe, expect, it } from "vitest"; |
| 3 | |
| 4 | import { ISSUER, testHeaders } from "./helpers"; |
| 5 | |
| 6 | const 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 | |
| 14 | describe("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 | }); |