fix: support Discord sign-in behind a trusted TLS proxy
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# Loaded by bun start and bun run db:migrate:production.
|
||||
# Set your public origin, e.g. https://habits.example.com
|
||||
APP_ORIGIN=
|
||||
# Enable only behind a private proxy that overwrites X-Forwarded-Proto.
|
||||
TRUST_PROXY=false
|
||||
# Docker sets HOST=0.0.0.0; local startup defaults to 127.0.0.1.
|
||||
# Optional overrides: PORT, DATABASE_PATH, DISCORD_CLIENT_ID,
|
||||
# DISCORD_CLIENT_SECRET, AUTH_COOKIE_SECRET.
|
||||
|
||||
@@ -55,6 +55,32 @@ beforeEach(() => {
|
||||
afterEach(() => sqlite.close());
|
||||
|
||||
describe("Discord sign-in and sessions", () => {
|
||||
test("trusted TLS proxy supports sign-in and secure callback cookies", async () => {
|
||||
const publicOrigin = "https://habits.example";
|
||||
const proxyApp = createApi(db, { ...config, origin: publicOrigin, trustProxy: true }, request, () => timestamp);
|
||||
const response = await proxyApp.request("http://habits.example/api/auth/discord", { headers: { "X-Forwarded-Proto": "https" } });
|
||||
const location = new URL(response.headers.get("Location")!);
|
||||
expect(location.origin).toBe("https://discord.com");
|
||||
expect(location.searchParams.get("redirect_uri")).toBe(`${publicOrigin}/api/auth/discord/callback`);
|
||||
expect(response.headers.get("Set-Cookie")).toContain("Secure");
|
||||
const callback = await proxyApp.request(`http://habits.example/api/auth/discord/callback?state=${location.searchParams.get("state")}&code=test-code`, {
|
||||
headers: { "X-Forwarded-Proto": "https", Cookie: cookie(response, "__Host-minabot_oauth") },
|
||||
});
|
||||
expect(cookie(callback, "__Host-minabot_session")).not.toBe("");
|
||||
expect(callback.headers.get("Set-Cookie")).toContain("Secure");
|
||||
});
|
||||
|
||||
test("forwarded protocol is opt-in and cannot override the actual hostname", async () => {
|
||||
for (const [trustProxy, host] of [[false, "habits.example"], [true, "wrong.example"]] as const) {
|
||||
const proxyApp = createApi(db, { ...config, origin: "https://habits.example", trustProxy }, request, () => timestamp);
|
||||
const response = await proxyApp.request(`http://${host}/api/auth/discord`, {
|
||||
headers: { "X-Forwarded-Proto": "https", "X-Forwarded-Host": "habits.example" },
|
||||
});
|
||||
expect(new URL(response.headers.get("Location")!).origin).toBe("https://habits.example");
|
||||
expect(response.headers.get("Set-Cookie")).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test("me requires a valid session and health stays public", async () => {
|
||||
for (const value of ["", "minabot_session=malformed", `minabot_session=${"a".repeat(43)}`]) {
|
||||
const response = await app.request(`${origin}/api/me`, { headers: { Cookie: value } });
|
||||
|
||||
@@ -3,6 +3,7 @@ export type AuthConfig = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
cookieSecret: string;
|
||||
trustProxy?: boolean;
|
||||
};
|
||||
|
||||
export function readAuthConfig(env = process.env): AuthConfig {
|
||||
@@ -28,6 +29,7 @@ export function readAuthConfig(env = process.env): AuthConfig {
|
||||
clientId: env.DISCORD_CLIENT_ID ?? "",
|
||||
clientSecret: env.DISCORD_CLIENT_SECRET ?? "",
|
||||
cookieSecret: env.AUTH_COOKIE_SECRET ?? "",
|
||||
trustProxy: env.TRUST_PROXY === "true",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,11 @@ export function createAuth(db: AppDatabase, config: AuthConfig, request: FetchDi
|
||||
routes.get("/discord", async c => {
|
||||
if (!isAuthConfigured(config)) return c.redirect("/?auth_error=not_configured");
|
||||
// Use one configured origin for redirects and cookie ownership.
|
||||
if (new URL(c.req.url).origin !== config.origin) return c.redirect(`${config.origin}/api/auth/discord?${new URLSearchParams({ timezone: normalizeTimezone(c.req.query("timezone")) })}`);
|
||||
const requestUrl = new URL(c.req.url);
|
||||
// Enable only behind a private proxy that overwrites X-Forwarded-Proto.
|
||||
// Keep the actual Host; never trust a client-supplied forwarded hostname.
|
||||
if (config.trustProxy && c.req.header("X-Forwarded-Proto") === "https") requestUrl.protocol = "https:";
|
||||
if (requestUrl.origin !== config.origin) return c.redirect(`${config.origin}/api/auth/discord?${new URLSearchParams({ timezone: normalizeTimezone(c.req.query("timezone")) })}`);
|
||||
const state = randomToken();
|
||||
await setSignedCookie(c, stateName, JSON.stringify({
|
||||
state,
|
||||
|
||||
Reference in New Issue
Block a user