From dc2c561f6038adbe15fd18d0edf7e0312c0d10cf Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Sun, 15 Feb 2026 23:28:02 -0500 Subject: [PATCH] test(gateway): add tests for trusted-proxy shared-secret fallback Add 9 unit tests covering fallback behavior: proxy success unchanged, token/password fallback on valid credentials, rejection on mismatch, no-fallback when server credentials unconfigured, rate limiting on fallback attempts, and proxy-takes-priority when both are available. Add 3 e2e tests covering internal connection scenarios: token auth with device identity, token auth without device identity (canSkipDevice), and proxy connection priority over token fallback. --- src/gateway/auth.test.ts | 188 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/src/gateway/auth.test.ts b/src/gateway/auth.test.ts index 1488b438237..84c20431448 100644 --- a/src/gateway/auth.test.ts +++ b/src/gateway/auth.test.ts @@ -650,3 +650,191 @@ describe("trusted-proxy auth", () => { expect(res.user).toBe("nick@example.com"); }); }); + +describe("trusted-proxy shared-secret fallback", () => { + const trustedProxyConfig = { + userHeader: "x-forwarded-user", + }; + + const untrustedReq = { + socket: { remoteAddress: "127.0.0.1" }, + headers: { + host: "gateway.local", + "x-forwarded-user": "nick@example.com", + }, + } as never; + + // trustedProxies: ["10.0.0.1"] means 127.0.0.1 is NOT trusted + const untrustedProxies = ["10.0.0.1"]; + + it("trusted-proxy via proxy succeeds (unchanged)", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + token: "server-token", + }, + connectAuth: null, + trustedProxies: ["10.0.0.1"], + req: { + socket: { remoteAddress: "10.0.0.1" }, + headers: { + host: "gateway.local", + "x-forwarded-user": "nick@example.com", + }, + } as never, + }); + + expect(res.ok).toBe(true); + expect(res.method).toBe("trusted-proxy"); + expect(res.user).toBe("nick@example.com"); + }); + + it("rejects untrusted IP when no token/password configured (unchanged)", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + }, + connectAuth: null, + trustedProxies: untrustedProxies, + req: untrustedReq, + }); + + expect(res.ok).toBe(false); + expect(res.reason).toBe("trusted_proxy_untrusted_source"); + }); + + it("falls back to token when untrusted IP provides valid token", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + token: "server-token", + }, + connectAuth: { token: "server-token" }, + trustedProxies: untrustedProxies, + req: untrustedReq, + }); + + expect(res.ok).toBe(true); + expect(res.method).toBe("token"); + }); + + it("rejects untrusted IP with invalid token", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + token: "server-token", + }, + connectAuth: { token: "wrong-token" }, + trustedProxies: untrustedProxies, + req: untrustedReq, + }); + + expect(res.ok).toBe(false); + expect(res.reason).toBe("token_mismatch"); + }); + + it("falls back to password when untrusted IP provides valid password", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + password: "server-pass", + }, + connectAuth: { password: "server-pass" }, + trustedProxies: untrustedProxies, + req: untrustedReq, + }); + + expect(res.ok).toBe(true); + expect(res.method).toBe("password"); + }); + + it("rejects untrusted IP with invalid password", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + password: "server-pass", + }, + connectAuth: { password: "wrong-pass" }, + trustedProxies: untrustedProxies, + req: untrustedReq, + }); + + expect(res.ok).toBe(false); + expect(res.reason).toBe("password_mismatch"); + }); + + it("rejects when client provides token but no server token configured", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + // no token or password configured on server + }, + connectAuth: { token: "client-token" }, + trustedProxies: untrustedProxies, + req: untrustedReq, + }); + + expect(res.ok).toBe(false); + expect(res.reason).toBe("trusted_proxy_untrusted_source"); + }); + + it("rejects when rate limited during fallback", async () => { + const limiter = createLimiterSpy(); + limiter.check.mockReturnValue({ allowed: false, remaining: 0, retryAfterMs: 5000 }); + + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + token: "server-token", + }, + connectAuth: { token: "server-token" }, + trustedProxies: untrustedProxies, + req: untrustedReq, + rateLimiter: limiter, + }); + + expect(res.ok).toBe(false); + expect(res.reason).toBe("rate_limited"); + expect(res.rateLimited).toBe(true); + }); + + it("uses proxy method when request comes from trusted proxy even if token is provided", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + token: "server-token", + }, + connectAuth: { token: "server-token" }, + trustedProxies: ["127.0.0.1"], + req: { + socket: { remoteAddress: "127.0.0.1" }, + headers: { + host: "gateway.local", + "x-forwarded-user": "nick@example.com", + }, + } as never, + }); + + expect(res.ok).toBe(true); + expect(res.method).toBe("trusted-proxy"); + expect(res.user).toBe("nick@example.com"); + }); +});