Bläddra i källkod

RHL-005-fix(auth): improve session cookie security handling and update tests

Code_Uwe 1 dag sedan
förälder
incheckning
29ad68afa5
2 ändrade filer med 32 tillägg och 23 borttagningar
  1. 29 19
      lib/auth/session.js
  2. 3 4
      lib/auth/session.test.js

+ 29 - 19
lib/auth/session.js

@@ -1,3 +1,4 @@
+// lib/auth/session.js
 import { cookies } from "next/headers";
 import { SignJWT, jwtVerify } from "jose";
 
@@ -15,13 +16,25 @@ function getSessionSecretKey() {
 }
 
 /**
- * Create a signed session JWT and store it in a HTTP-only cookie.
+ * Resolve whether the session cookie should be marked as "Secure".
+ *
+ * Default:
+ * - Secure in production (`NODE_ENV=production`)
  *
- * @param {Object} params
- * @param {string} params.userId - MongoDB user id as string.
- * @param {string} params.role - User role ("branch" | "admin" | "dev").
- * @param {string|null} params.branchId - Branch id or null.
- * @returns {Promise<string>} The signed JWT.
+ * Override (useful for local HTTP testing):
+ * - SESSION_COOKIE_SECURE=false
+ * - SESSION_COOKIE_SECURE=true
+ */
+function resolveCookieSecureFlag() {
+	const raw = (process.env.SESSION_COOKIE_SECURE || "").trim().toLowerCase();
+	if (raw === "true") return true;
+	if (raw === "false") return false;
+
+	return process.env.NODE_ENV === "production";
+}
+
+/**
+ * Create a signed session JWT and store it in a HTTP-only cookie.
  */
 export async function createSession({ userId, role, branchId }) {
 	if (!userId || !role) {
@@ -40,11 +53,11 @@ export async function createSession({ userId, role, branchId }) {
 		.setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
 		.sign(getSessionSecretKey());
 
-	const cookieStore = cookies();
+	const cookieStore = await cookies();
 
 	cookieStore.set(SESSION_COOKIE_NAME, jwt, {
 		httpOnly: true,
-		secure: process.env.NODE_ENV === "production",
+		secure: resolveCookieSecureFlag(),
 		sameSite: "lax",
 		path: "/",
 		maxAge: SESSION_MAX_AGE_SECONDS,
@@ -55,12 +68,9 @@ export async function createSession({ userId, role, branchId }) {
 
 /**
  * Read the current session from the HTTP-only cookie.
- *
- * @returns {Promise<{ userId: string, role: string, branchId: string | null } | null>}
- *          The session payload, or null if no valid session exists.
  */
 export async function getSession() {
-	const cookieStore = cookies();
+	const cookieStore = await cookies();
 	const cookie = cookieStore.get(SESSION_COOKIE_NAME);
 
 	if (!cookie?.value) {
@@ -83,12 +93,12 @@ export async function getSession() {
 			role,
 			branchId: typeof branchId === "string" ? branchId : null,
 		};
-	} catch (error) {
-		// Invalid or expired token: clear the cookie for hygiene and return null
-		const store = cookies();
+	} catch {
+		// Invalid or expired token: clear cookie and return null
+		const store = await cookies();
 		store.set(SESSION_COOKIE_NAME, "", {
 			httpOnly: true,
-			secure: process.env.NODE_ENV === "production",
+			secure: resolveCookieSecureFlag(),
 			sameSite: "lax",
 			path: "/",
 			maxAge: 0,
@@ -101,12 +111,12 @@ export async function getSession() {
 /**
  * Destroy the current session by clearing the session cookie.
  */
-export function destroySession() {
-	const cookieStore = cookies();
+export async function destroySession() {
+	const cookieStore = await cookies();
 
 	cookieStore.set(SESSION_COOKIE_NAME, "", {
 		httpOnly: true,
-		secure: process.env.NODE_ENV === "production",
+		secure: resolveCookieSecureFlag(),
 		sameSite: "lax",
 		path: "/",
 		maxAge: 0,

+ 3 - 4
lib/auth/session.test.js

@@ -41,7 +41,6 @@ import { __cookieStore } from "next/headers";
 describe("auth session utilities", () => {
 	beforeEach(() => {
 		__cookieStore.clear();
-		// Align tests with env policy: strong secret (>= 32 chars)
 		process.env.SESSION_SECRET = "x".repeat(64);
 		process.env.NODE_ENV = "test";
 	});
@@ -124,7 +123,7 @@ describe("auth session utilities", () => {
 			branchId: "NL02",
 		});
 
-		destroySession();
+		await destroySession();
 
 		const store = __cookieStore.dump();
 		const cookie = store.get(SESSION_COOKIE_NAME);
@@ -134,8 +133,8 @@ describe("auth session utilities", () => {
 		expect(cookie.options.maxAge).toBe(0);
 	});
 
-	it("destroySession sets an empty cookie even if none existed before", () => {
-		destroySession();
+	it("destroySession sets an empty cookie even if none existed before", async () => {
+		await destroySession();
 
 		const store = __cookieStore.dump();
 		const cookie = store.get(SESSION_COOKIE_NAME);