From 5cc88dc73f075eaabdb1e3c62aff140487aced41 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 31 May 2026 11:11:26 -0700 Subject: [PATCH] Pull the session from badger --- .../verifyUserFromResourceSession.ts | 102 +++++++++++++++++ .../verifyUserFromSessionOrHeaders.ts | 104 ------------------ server/private/routers/internal.ts | 4 +- 3 files changed, 104 insertions(+), 106 deletions(-) create mode 100644 server/middlewares/verifyUserFromResourceSession.ts delete mode 100644 server/middlewares/verifyUserFromSessionOrHeaders.ts diff --git a/server/middlewares/verifyUserFromResourceSession.ts b/server/middlewares/verifyUserFromResourceSession.ts new file mode 100644 index 000000000..515b979e7 --- /dev/null +++ b/server/middlewares/verifyUserFromResourceSession.ts @@ -0,0 +1,102 @@ +import { Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { resourceSessions, users } from "@server/db"; +import { eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getUserSessionWithUser } from "@server/db/queries/verifySessionQueries"; +import { encodeHexLowerCase } from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; +import config from "@server/lib/config"; +import logger from "@server/logger"; + +export const verifyUserFromResourceSessionMiddleware = async ( + req: any, + res: Response, + next: NextFunction +) => { + if (!req.user) { + const sessionCookieName = + config.getRawConfig().server.session_cookie_name; + + // Collect all resource session cookies (format: {name}[_s].{timestamp}=token) + const cookieHeader: string | undefined = req.headers.cookie; + const candidates: { timestamp: number; token: string }[] = []; + + if (cookieHeader) { + for (const part of cookieHeader.split(";")) { + const trimmed = part.trim(); + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + + const cookieName = trimmed.slice(0, eqIdx).trim(); + const cookieValue = trimmed.slice(eqIdx + 1).trim(); + + // Match both secure (_s.timestamp) and non-secure (.timestamp) variants + const securePrefix = `${sessionCookieName}_s.`; + const httpPrefix = `${sessionCookieName}.`; + + let timestampStr: string | null = null; + if (cookieName.startsWith(securePrefix)) { + timestampStr = cookieName.slice(securePrefix.length); + } else if (cookieName.startsWith(httpPrefix)) { + timestampStr = cookieName.slice(httpPrefix.length); + } + + if (timestampStr !== null && /^\d+$/.test(timestampStr)) { + candidates.push({ + timestamp: parseInt(timestampStr, 10), + token: cookieValue + }); + } + } + } + + // Pick the most recently issued session (highest timestamp) + candidates.sort((a, b) => b.timestamp - a.timestamp); + const best = candidates[0]; + + if (best) { + try { + const sessionId = encodeHexLowerCase( + sha256(new TextEncoder().encode(best.token)) + ); + + const [resourceSession] = await db + .select() + .from(resourceSessions) + .where(eq(resourceSessions.sessionId, sessionId)) + .limit(1); + + if (resourceSession && Date.now() < resourceSession.expiresAt) { + if (resourceSession.userSessionId) { + const result = await getUserSessionWithUser( + resourceSession.userSessionId + ); + + if (result?.user && result?.session) { + req.user = result.user; + req.session = result.session; + } + } + } + } catch (e) { + logger.error( + "verifyUserFromResourceSessionMiddleware: failed to validate resource session", + e + ); + } + } + } + + // Populate userOrgRoleIds if an orgId is available in route params + if (req.user && req.params?.orgId && !req.userOrgRoleIds) { + req.userOrgRoleIds = await getUserOrgRoleIds( + req.user.userId, + req.params.orgId + ); + } + + next(); +}; diff --git a/server/middlewares/verifyUserFromSessionOrHeaders.ts b/server/middlewares/verifyUserFromSessionOrHeaders.ts deleted file mode 100644 index cd8e3c2d4..000000000 --- a/server/middlewares/verifyUserFromSessionOrHeaders.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Response, NextFunction } from "express"; -import { db } from "@server/db"; -import { users } from "@server/db"; -import { eq, or } from "drizzle-orm"; -import createHttpError from "http-errors"; -import HttpCode from "@server/types/HttpCode"; -import { verifySession } from "@server/auth/sessions/verifySession"; -import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; - -/** - * Middleware that populates req.user from either: - * 1. A valid session cookie (normal authenticated flow), or - * 2. Badger-injected headers: Remote-User-Id, Remote-User (username), Remote-Email - * - * If an orgId is present in req.params, req.userOrgRoleIds is also populated. - * - * If neither source yields a user, returns 401. - * If header-based lookup matches more than one user, returns 400. - */ -export const verifyUserFromSessionOrHeadersMiddleware = async ( - req: any, - res: Response, - next: NextFunction -) => { - // 1. Try session-based auth first - if (!req.user) { - try { - const { session, user } = await verifySession(req); - if (session && user) { - const rows = await db - .select() - .from(users) - .where(eq(users.userId, user.userId)); - - if (rows[0]) { - req.user = rows[0]; - req.session = session; - } - } - } catch { - // session lookup failure is not fatal; fall through to header auth - } - } - - // 2. Fall back to Badger-injected headers - if (!req.user) { - const userId = req.headers["remote-user-id"] as string | undefined; - const username = req.headers["remote-user"] as string | undefined; - const email = req.headers["remote-email"] as string | undefined; - - if (!userId && !username && !email) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") - ); - } - - let foundUsers; - - if (userId) { - // Most reliable: look up directly by ID - foundUsers = await db - .select() - .from(users) - .where(eq(users.userId, userId)); - } else { - // Fall back to username / email (may be absent depending on badger version) - const conditions = []; - if (username) conditions.push(eq(users.username, username)); - if (email) conditions.push(eq(users.email, email)); - - foundUsers = await db - .select() - .from(users) - .where(or(...conditions)); - } - - if (!foundUsers || foundUsers.length === 0) { - return next( - createHttpError(HttpCode.UNAUTHORIZED, "User not found") - ); - } - - if (foundUsers.length > 1) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Multiple users found matching the provided credentials" - ) - ); - } - - req.user = foundUsers[0]; - } - - // 3. Populate userOrgRoleIds if an orgId is available in route params - if (req.user && req.params?.orgId && !req.userOrgRoleIds) { - req.userOrgRoleIds = await getUserOrgRoleIds( - req.user.userId, - req.params.orgId - ); - } - - next(); -}; diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts index 4b8e00c51..3b643b108 100644 --- a/server/private/routers/internal.ts +++ b/server/private/routers/internal.ts @@ -22,7 +22,7 @@ import * as ssh from "#private/routers/ssh"; import { verifySessionUserMiddleware, - verifyUserFromSessionOrHeadersMiddleware + verifyUserFromResourceSessionMiddleware } from "@server/middlewares"; import { internalRouter as ir } from "@server/routers/internal"; @@ -48,7 +48,7 @@ internalRouter.get("/maintenance/info", resource.getMaintenanceInfo); internalRouter.post( "/org/:orgId/ssh/sign-key", - verifyUserFromSessionOrHeadersMiddleware, + verifyUserFromResourceSessionMiddleware, ssh.signSshKey );