mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-29 07:21:16 -07:00
Fix: set cache private/no-store when auth enabled
This commit is contained in:
+13
-3
@@ -6,6 +6,16 @@ import { isAuthEnabled } from "utils/env";
|
|||||||
const authEnabled = isAuthEnabled();
|
const authEnabled = isAuthEnabled();
|
||||||
const authSecret = process.env.NEXTAUTH_SECRET || process.env.HOMEPAGE_AUTH_SECRET;
|
const authSecret = process.env.NEXTAUTH_SECRET || process.env.HOMEPAGE_AUTH_SECRET;
|
||||||
|
|
||||||
|
// Prerendered pages carry `s-maxage`, and the dashboard HTML embeds the service and
|
||||||
|
// bookmark inventory. Without this, a CDN or caching reverse proxy in front of Homepage
|
||||||
|
// would store an authenticated response and serve it to anonymous visitors.
|
||||||
|
function withPrivateCache(res) {
|
||||||
|
if (authEnabled) {
|
||||||
|
res.headers.set("Cache-Control", "private, no-store");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
export async function middleware(req) {
|
export async function middleware(req) {
|
||||||
// Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set
|
// Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set
|
||||||
const host = req.headers.get("host");
|
const host = req.headers.get("host");
|
||||||
@@ -26,18 +36,18 @@ export async function middleware(req) {
|
|||||||
if (authEnabled && !pathname.startsWith("/api/healthcheck")) {
|
if (authEnabled && !pathname.startsWith("/api/healthcheck")) {
|
||||||
// The MCP API handler authorizes both bearer tokens and Homepage sessions.
|
// The MCP API handler authorizes both bearer tokens and Homepage sessions.
|
||||||
if (pathname === "/api/mcp") {
|
if (pathname === "/api/mcp") {
|
||||||
return NextResponse.next();
|
return withPrivateCache(NextResponse.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await getToken({ req, secret: authSecret });
|
const token = await getToken({ req, secret: authSecret });
|
||||||
if (!token) {
|
if (!token) {
|
||||||
const signInUrl = new URL("/auth/signin", req.url);
|
const signInUrl = new URL("/auth/signin", req.url);
|
||||||
signInUrl.searchParams.set("callbackUrl", "/");
|
signInUrl.searchParams.set("callbackUrl", "/");
|
||||||
return NextResponse.redirect(signInUrl);
|
return withPrivateCache(NextResponse.redirect(signInUrl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return withPrivateCache(NextResponse.next());
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
+42
-10
@@ -2,9 +2,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
const { NextResponse, getToken } = vi.hoisted(() => ({
|
const { NextResponse, getToken } = vi.hoisted(() => ({
|
||||||
NextResponse: {
|
NextResponse: {
|
||||||
json: vi.fn((body, init) => ({ type: "json", body, init })),
|
json: vi.fn((body, init) => ({ type: "json", body, init, headers: new Headers() })),
|
||||||
next: vi.fn(() => ({ type: "next" })),
|
next: vi.fn(() => ({ type: "next", headers: new Headers() })),
|
||||||
redirect: vi.fn((url) => ({ type: "redirect", url })),
|
redirect: vi.fn((url) => ({ type: "redirect", url, headers: new Headers() })),
|
||||||
},
|
},
|
||||||
getToken: vi.fn(),
|
getToken: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -47,7 +47,7 @@ describe("middleware", () => {
|
|||||||
const res = await middleware(createReq("localhost:3000"));
|
const res = await middleware(createReq("localhost:3000"));
|
||||||
|
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res.type).toBe("next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks requests when host is not allowed", async () => {
|
it("blocks requests when host is not allowed", async () => {
|
||||||
@@ -74,7 +74,7 @@ describe("middleware", () => {
|
|||||||
const res = await middleware(createReq("anything.example"));
|
const res = await middleware(createReq("anything.example"));
|
||||||
|
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res.type).toBe("next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows requests when host is included in HOMEPAGE_ALLOWED_HOSTS", async () => {
|
it("allows requests when host is included in HOMEPAGE_ALLOWED_HOSTS", async () => {
|
||||||
@@ -85,7 +85,7 @@ describe("middleware", () => {
|
|||||||
const res = await middleware(createReq("example.com:3000", "http://example.com:3000/"));
|
const res = await middleware(createReq("example.com:3000", "http://example.com:3000/"));
|
||||||
|
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res.type).toBe("next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows healthcheck requests without auth when host is allowed", async () => {
|
it("allows healthcheck requests without auth when host is allowed", async () => {
|
||||||
@@ -97,7 +97,7 @@ describe("middleware", () => {
|
|||||||
|
|
||||||
expect(getToken).not.toHaveBeenCalled();
|
expect(getToken).not.toHaveBeenCalled();
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res.type).toBe("next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["false", "0", "no", "off", ""])("treats HOMEPAGE_AUTH_ENABLED=%j as disabled", async (value) => {
|
it.each(["false", "0", "no", "off", ""])("treats HOMEPAGE_AUTH_ENABLED=%j as disabled", async (value) => {
|
||||||
@@ -107,7 +107,7 @@ describe("middleware", () => {
|
|||||||
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/some"));
|
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/some"));
|
||||||
|
|
||||||
expect(getToken).not.toHaveBeenCalled();
|
expect(getToken).not.toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res.type).toBe("next");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to signin when auth is enabled and no token is present", async () => {
|
it("redirects to signin when auth is enabled and no token is present", async () => {
|
||||||
@@ -138,7 +138,39 @@ describe("middleware", () => {
|
|||||||
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/"));
|
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/"));
|
||||||
|
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res.type).toBe("next");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks responses private so shared caches cannot store them when auth is enabled", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
|
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
||||||
|
|
||||||
|
getToken.mockResolvedValueOnce({ sub: "user" });
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware();
|
||||||
|
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/"));
|
||||||
|
|
||||||
|
expect(res.headers.get("Cache-Control")).toBe("private, no-store");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks the signin redirect private as well", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
|
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
||||||
|
|
||||||
|
getToken.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware();
|
||||||
|
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/some"));
|
||||||
|
|
||||||
|
expect(res.type).toBe("redirect");
|
||||||
|
expect(res.headers.get("Cache-Control")).toBe("private, no-store");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves cache headers alone when auth is disabled", async () => {
|
||||||
|
const middleware = await loadMiddleware();
|
||||||
|
const res = await middleware(createReq("localhost:3000", "http://localhost:3000/"));
|
||||||
|
|
||||||
|
expect(res.headers.get("Cache-Control")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("delegates MCP authorization to the API handler", async () => {
|
it("delegates MCP authorization to the API handler", async () => {
|
||||||
@@ -150,6 +182,6 @@ describe("middleware", () => {
|
|||||||
|
|
||||||
expect(getToken).not.toHaveBeenCalled();
|
expect(getToken).not.toHaveBeenCalled();
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res.type).toBe("next");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user