mirror of
https://github.com/tale/headplane.git
synced 2026-08-07 04:43:12 +00:00
feat(auth): support reverse-proxy driven proxy auth
Closes HP-353.
This commit is contained in:
@@ -196,6 +196,113 @@ describe("session round-trip", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("proxy authentication", () => {
|
||||
test("creates a user-backed principal from trusted proxy headers", async () => {
|
||||
const { auth } = createTestAuth({
|
||||
headscaleApiKey: "configured-headscale-key",
|
||||
proxyAuth: {
|
||||
enabled: true,
|
||||
allowedCidrs: ["10.10.0.0/16"],
|
||||
emailHeader: "X-Forwarded-Email",
|
||||
nameHeader: "X-Forwarded-Name",
|
||||
},
|
||||
});
|
||||
const request = new Request("http://localhost/test", {
|
||||
headers: {
|
||||
"Remote-User": "alice",
|
||||
"X-Forwarded-Email": "alice@example.com",
|
||||
"X-Forwarded-Name": "Alice Example",
|
||||
},
|
||||
});
|
||||
|
||||
auth.registerRequestClientAddress(request, "10.10.42.9");
|
||||
|
||||
const principal = await auth.require(request);
|
||||
expect(principal.kind).toBe("proxy");
|
||||
if (principal.kind === "proxy") {
|
||||
expect(principal.user.subject).toBe("proxy:alice");
|
||||
expect(principal.user.role).toBe("owner");
|
||||
expect(principal.profile.name).toBe("Alice Example");
|
||||
expect(principal.profile.email).toBe("alice@example.com");
|
||||
expect(principal.profile.username).toBe("alice");
|
||||
}
|
||||
expect(auth.getHeadscaleApiKey(principal)).toBe("configured-headscale-key");
|
||||
});
|
||||
|
||||
test("uses localhost CIDRs by default when no allowed CIDRs are configured", async () => {
|
||||
const { auth } = createTestAuth({
|
||||
headscaleApiKey: "configured-headscale-key",
|
||||
proxyAuth: { enabled: true },
|
||||
});
|
||||
const request = new Request("http://localhost/test", {
|
||||
headers: { "Remote-User": "alice" },
|
||||
});
|
||||
|
||||
auth.registerRequestClientAddress(request, "::ffff:127.0.0.1");
|
||||
|
||||
const principal = await auth.require(request);
|
||||
expect(principal.kind).toBe("proxy");
|
||||
expect(auth.getHeadscaleApiKey(principal)).toBe("configured-headscale-key");
|
||||
});
|
||||
|
||||
test("falls back to normal session auth outside allowed CIDRs", async () => {
|
||||
const { auth } = createTestAuth({
|
||||
headscaleApiKey: "configured-headscale-key",
|
||||
proxyAuth: { enabled: true, allowedCidrs: ["10.10.0.0/16"] },
|
||||
});
|
||||
const request = new Request("http://localhost/test");
|
||||
|
||||
auth.registerRequestClientAddress(request, "10.11.42.9");
|
||||
|
||||
await expect(auth.require(request)).rejects.toThrow("No session cookie found");
|
||||
});
|
||||
|
||||
test("can check allowed CIDRs against a forwarded IP from a trusted proxy", async () => {
|
||||
const { auth } = createTestAuth({
|
||||
headscaleApiKey: "configured-headscale-key",
|
||||
proxyAuth: {
|
||||
enabled: true,
|
||||
allowedCidrs: ["203.0.113.0/24"],
|
||||
trustedProxyCidrs: ["10.0.0.0/24"],
|
||||
ipHeader: "X-Forwarded-For",
|
||||
},
|
||||
});
|
||||
const request = new Request("http://localhost/test", {
|
||||
headers: {
|
||||
"Remote-User": "alice",
|
||||
"X-Forwarded-For": "203.0.113.42, 10.0.0.10",
|
||||
},
|
||||
});
|
||||
|
||||
auth.registerRequestClientAddress(request, "10.0.0.10");
|
||||
|
||||
const principal = await auth.require(request);
|
||||
expect(principal.kind).toBe("proxy");
|
||||
});
|
||||
|
||||
test("does not trust forwarded IP headers from untrusted direct peers", async () => {
|
||||
const { auth } = createTestAuth({
|
||||
headscaleApiKey: "configured-headscale-key",
|
||||
proxyAuth: {
|
||||
enabled: true,
|
||||
allowedCidrs: ["203.0.113.0/24"],
|
||||
trustedProxyCidrs: ["10.0.0.0/24"],
|
||||
ipHeader: "X-Real-IP",
|
||||
},
|
||||
});
|
||||
const request = new Request("http://localhost/test", {
|
||||
headers: {
|
||||
"Remote-User": "alice",
|
||||
"X-Real-IP": "203.0.113.42",
|
||||
},
|
||||
});
|
||||
|
||||
auth.registerRequestClientAddress(request, "198.51.100.10");
|
||||
|
||||
await expect(auth.require(request)).rejects.toThrow("No session cookie found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("authorization", () => {
|
||||
let auth: AuthService;
|
||||
|
||||
|
||||
@@ -3,12 +3,28 @@ import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
||||
|
||||
import { createAuthService } from "~/server/web/auth";
|
||||
|
||||
export function createTestAuth() {
|
||||
export function createTestAuth(
|
||||
options: {
|
||||
headscaleApiKey?: string;
|
||||
proxyAuth?: {
|
||||
enabled: boolean;
|
||||
allowedCidrs?: string[];
|
||||
trustedProxyCidrs?: string[];
|
||||
ipHeader?: string;
|
||||
userHeader?: string;
|
||||
emailHeader?: string;
|
||||
nameHeader?: string;
|
||||
pictureHeader?: string;
|
||||
};
|
||||
} = {},
|
||||
) {
|
||||
const db = drizzle(":memory:");
|
||||
migrate(db, { migrationsFolder: "./drizzle" });
|
||||
|
||||
const auth = createAuthService({
|
||||
secret: "test-secret-key-for-unit-tests",
|
||||
headscaleApiKey: options.headscaleApiKey,
|
||||
proxyAuth: options.proxyAuth,
|
||||
db,
|
||||
cookie: {
|
||||
name: "_hp_test",
|
||||
|
||||
Reference in New Issue
Block a user