mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(auth): issue a Secure session cookie under the __Host- prefix
A cookie named healthlog_session can be set by any host under the same registrable domain, so a page on a sibling subdomain could plant a session of its choosing in a visitor's browser. Wherever the session cookie is Secure it is now issued as __Host-healthlog_session, which a browser accepts only from this exact host. Plain-HTTP installs, which cannot hold a __Host- cookie, keep the old name. Nobody is signed out: the old name is still read after the new one, a session found under it is re-issued under the new name and the old cookie expired, and a browser holding the new cookie is never answered from the old one. Every reader and writer goes through one module, which the proxy, the native-caller check, the native handoff and the OIDC callback use; the session-surface guard follows the new reader.
This commit is contained in:
1 parent
d3257a1b6b
commit
5a875d2840
19 files changed
+348
-88
No files matched your search
@@ -20,7 +20,6 @@ vi.mock("@/lib/rate-limit", () => ({
|
||||
const validateSessionWithCreatedAt = vi.fn();
|
||||
vi.mock("@/lib/auth/session", () => ({
|
||||
validateSessionWithCreatedAt: (v: unknown) => validateSessionWithCreatedAt(v),
|
||||
SESSION_COOKIE_NAME: "healthlog_session",
|
||||
}));
|
||||
|
||||
// Keep the real shared core (buildHandoffCallbackUrl is used by
|
||||
|
||||
@@ -29,11 +29,11 @@ import { annotate } from "@/lib/logging/context";
|
||||
import { checkAuthSurfaceRateLimit } from "@/lib/rate-limit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { auditLog } from "@/lib/auth/audit";
|
||||
import { shouldEmitSecureCookie } from "@/lib/auth/secure-cookie";
|
||||
import { validateSessionWithCreatedAt } from "@/lib/auth/session";
|
||||
import {
|
||||
validateSessionWithCreatedAt,
|
||||
SESSION_COOKIE_NAME,
|
||||
} from "@/lib/auth/session";
|
||||
expireSessionCookies,
|
||||
readSessionCookie,
|
||||
} from "@/lib/auth/session-cookie";
|
||||
import { mintNativeHandoff } from "@/lib/auth/native-handoff";
|
||||
import {
|
||||
NATIVE_HANDOFF_STATE_COOKIE,
|
||||
@@ -95,7 +95,7 @@ export const GET = apiHandler(async (req: NextRequest) => {
|
||||
// (row + expiry), never "a cookie is present". Projects `createdAt` so the
|
||||
// freshness comparison below is structurally always available.
|
||||
const authed = await validateSessionWithCreatedAt(
|
||||
req.cookies.get(SESSION_COOKIE_NAME)?.value,
|
||||
readSessionCookie(req.cookies)?.value,
|
||||
);
|
||||
if (!authed) {
|
||||
annotate({ meta: { reason: "no_session" } });
|
||||
@@ -140,13 +140,7 @@ export const GET = apiHandler(async (req: NextRequest) => {
|
||||
await prisma.session
|
||||
.delete({ where: { id: authed.session.id } })
|
||||
.catch(() => {});
|
||||
response.cookies.set(SESSION_COOKIE_NAME, "", {
|
||||
httpOnly: true,
|
||||
secure: shouldEmitSecureCookie(),
|
||||
sameSite: "lax",
|
||||
maxAge: 0,
|
||||
path: "/",
|
||||
});
|
||||
expireSessionCookies(response.cookies);
|
||||
|
||||
return response;
|
||||
});
|
||||
@@ -42,7 +42,6 @@ vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn() }));
|
||||
vi.mock("@/lib/auth/session", () => ({
|
||||
createSession: vi.fn(),
|
||||
validateSessionFromCookieValue: vi.fn(),
|
||||
SESSION_COOKIE_NAME: "healthlog_session",
|
||||
}));
|
||||
vi.mock("@/lib/auth/login-alert", () => ({ recordSignInDevice: vi.fn() }));
|
||||
vi.mock("@/lib/auth/mfa/challenge", () => ({
|
||||
|
||||
@@ -3,10 +3,8 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { annotate, getEvent } from "@/lib/logging/context";
|
||||
import { checkAuthSurfaceRateLimit } from "@/lib/rate-limit";
|
||||
import {
|
||||
validateSessionFromCookieValue,
|
||||
SESSION_COOKIE_NAME,
|
||||
} from "@/lib/auth/session";
|
||||
import { validateSessionFromCookieValue } from "@/lib/auth/session";
|
||||
import { readSessionCookie } from "@/lib/auth/session-cookie";
|
||||
import {
|
||||
discoverOidcMetadata,
|
||||
exchangeCodeForTokens,
|
||||
@@ -225,7 +223,7 @@ export const GET = apiHandler(async (req: NextRequest) => {
|
||||
// session gains nothing here but a redirect to a page they could already
|
||||
// reach — no code redemption, no session mint, no identity change.
|
||||
const existingSession = await validateSessionFromCookieValue(
|
||||
req.cookies.get(SESSION_COOKIE_NAME)?.value,
|
||||
readSessionCookie(req.cookies)?.value,
|
||||
);
|
||||
if (existingSession) {
|
||||
annotate({ meta: { reason: "duplicate_callback_authenticated" } });
|
||||
@@ -258,7 +256,7 @@ export const GET = apiHandler(async (req: NextRequest) => {
|
||||
: null;
|
||||
if (isClaim) {
|
||||
const liveSession = await validateSessionFromCookieValue(
|
||||
req.cookies.get(SESSION_COOKIE_NAME)?.value,
|
||||
readSessionCookie(req.cookies)?.value,
|
||||
);
|
||||
if (liveSession) {
|
||||
annotate({ meta: { reason: "claim_signed_in" } });
|
||||
|
||||
Reference in new issue
Block a user