diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index a7f32ffff..cc68acb9b 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -28,6 +28,7 @@ export * from "./verifyApiKeyAccess"; export * from "./verifySiteProvisioningKeyAccess"; export * from "./verifyDomainAccess"; export * from "./verifyUserIsOrgOwner"; +export * from "./verifyUserFromSessionOrHeaders"; export * from "./verifySiteResourceAccess"; export * from "./logActionAudit"; export * from "./verifyOlmAccess"; diff --git a/server/middlewares/verifyUserFromSessionOrHeaders.ts b/server/middlewares/verifyUserFromSessionOrHeaders.ts new file mode 100644 index 000000000..cd8e3c2d4 --- /dev/null +++ b/server/middlewares/verifyUserFromSessionOrHeaders.ts @@ -0,0 +1,104 @@ +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 185ca8841..4b8e00c51 100644 --- a/server/private/routers/internal.ts +++ b/server/private/routers/internal.ts @@ -18,8 +18,12 @@ import * as billing from "#private/routers/billing"; import * as license from "#private/routers/license"; import * as resource from "#private/routers/resource"; import * as browserTarget from "#private/routers/browserGatewayTarget"; +import * as ssh from "#private/routers/ssh"; -import { verifySessionUserMiddleware } from "@server/middlewares"; +import { + verifySessionUserMiddleware, + verifyUserFromSessionOrHeadersMiddleware +} from "@server/middlewares"; import { internalRouter as ir } from "@server/routers/internal"; @@ -42,6 +46,10 @@ internalRouter.get(`/license/status`, license.getLicenseStatus); internalRouter.get("/maintenance/info", resource.getMaintenanceInfo); -internalRouter.post("/org/:orgId/ssh/sign-key", ssh.signSshKey); +internalRouter.post( + "/org/:orgId/ssh/sign-key", + verifyUserFromSessionOrHeadersMiddleware, + ssh.signSshKey +); internalRouter.get("/resource/browser-target", browserTarget.getBrowserTarget); diff --git a/src/app/ssh/page.tsx b/src/app/ssh/page.tsx index f85bbee88..467756236 100644 --- a/src/app/ssh/page.tsx +++ b/src/app/ssh/page.tsx @@ -64,18 +64,23 @@ export default async function SshPage() { target = res.data.data; if (target.pamMode === "push") { - const { privateKeyPem, publicKeyOpenSSH } = - generateEphemeralKeyPair(); - privateKey = privateKeyPem; - const res = await priv.post>( - `/org/${target.orgId}/ssh/sign-key`, - { - publicKey: publicKeyOpenSSH, - resource: target.niceId - } - ); - signedKeyData = res.data.data; - console.log("Received signed SSH key:", signedKeyData); + try { + const { privateKeyPem, publicKeyOpenSSH } = + generateEphemeralKeyPair(); + privateKey = privateKeyPem; + const res = await priv.post>( + `/org/${target.orgId}/ssh/sign-key`, + { + publicKey: publicKeyOpenSSH, + resource: target.niceId + } + ); + signedKeyData = res.data.data; + console.log("Received signed SSH key:", signedKeyData); + } catch (err) { + console.error("Error signing SSH key:", err); + error = "Failed to sign SSH key for PAM push authentication."; + } } } catch (error) { console.error("Error fetching browser target:", error);