Validate BIM sessions against Launcher
This commit is contained in:
parent
66fc4960bf
commit
ffa60e4a22
157
server/index.js
157
server/index.js
|
|
@ -53,7 +53,9 @@ const INTERNAL_ACCESS_TOKEN =
|
|||
process.env.NODEDC_PLATFORM_SERVICE_TOKEN ||
|
||||
"";
|
||||
const BIM_SESSION_COOKIE = process.env.NODEDC_BIM_SESSION_COOKIE || "nodedc_bim_session";
|
||||
const BIM_HANDOFF_COOKIE = process.env.NODEDC_BIM_HANDOFF_COOKIE || "nodedc_bim_handoff";
|
||||
const BIM_SESSION_TTL_MS = Number(process.env.NODEDC_BIM_SESSION_TTL_MS || 12 * 60 * 60 * 1000);
|
||||
const BIM_HANDOFF_TTL_SECONDS = Number(process.env.NODEDC_BIM_HANDOFF_TTL_SECONDS || 60);
|
||||
const BIM_COOKIE_SECURE = parseBooleanEnv(process.env.NODEDC_BIM_COOKIE_SECURE, false);
|
||||
const BIM_COOKIE_SAMESITE = process.env.NODEDC_BIM_COOKIE_SAMESITE || (BIM_COOKIE_SECURE ? "None" : "Lax");
|
||||
const SHARE_AUTH_GUEST_PARAM = "ndc_bim_guest";
|
||||
|
|
@ -757,8 +759,25 @@ const buildCookieHeader = (name, value, maxAgeSeconds) => {
|
|||
return parts.join("; ");
|
||||
};
|
||||
|
||||
const appendSetCookieHeader = (res, cookie) => {
|
||||
const existing = res.getHeader("Set-Cookie");
|
||||
if (!existing) {
|
||||
res.setHeader("Set-Cookie", cookie);
|
||||
return;
|
||||
}
|
||||
res.setHeader("Set-Cookie", Array.isArray(existing) ? [...existing, cookie] : [existing, cookie]);
|
||||
};
|
||||
|
||||
const clearBimSessionCookie = (res) => {
|
||||
res.setHeader("Set-Cookie", buildCookieHeader(BIM_SESSION_COOKIE, "", 0));
|
||||
appendSetCookieHeader(res, buildCookieHeader(BIM_SESSION_COOKIE, "", 0));
|
||||
};
|
||||
|
||||
const setBimHandoffCookie = (res) => {
|
||||
appendSetCookieHeader(res, buildCookieHeader(BIM_HANDOFF_COOKIE, randomBase64Url(16), BIM_HANDOFF_TTL_SECONDS));
|
||||
};
|
||||
|
||||
const clearBimHandoffCookie = (res) => {
|
||||
appendSetCookieHeader(res, buildCookieHeader(BIM_HANDOFF_COOKIE, "", 0));
|
||||
};
|
||||
|
||||
const pruneExpiredBimSessions = () => {
|
||||
|
|
@ -770,17 +789,18 @@ const pruneExpiredBimSessions = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const createBimSession = (res, user) => {
|
||||
const createBimSession = (res, user, {launcherSessionId = null} = {}) => {
|
||||
pruneExpiredBimSessions();
|
||||
const id = randomBase64Url(32);
|
||||
const session = {
|
||||
id,
|
||||
user,
|
||||
launcherSessionId: typeof launcherSessionId === "string" && launcherSessionId ? launcherSessionId : null,
|
||||
createdAt: nowIso(),
|
||||
expiresAt: Date.now() + BIM_SESSION_TTL_MS
|
||||
};
|
||||
bimSessions.set(id, session);
|
||||
res.setHeader("Set-Cookie", buildCookieHeader(BIM_SESSION_COOKIE, id, BIM_SESSION_TTL_MS / 1000));
|
||||
appendSetCookieHeader(res, buildCookieHeader(BIM_SESSION_COOKIE, id, BIM_SESSION_TTL_MS / 1000));
|
||||
return session;
|
||||
};
|
||||
|
||||
|
|
@ -797,6 +817,79 @@ const getCurrentBimSession = (req) => {
|
|||
return session;
|
||||
};
|
||||
|
||||
const destroyCurrentBimSession = (req, res) => {
|
||||
const sessionId = parseCookies(req.headers.cookie || "")[BIM_SESSION_COOKIE];
|
||||
if (sessionId) {
|
||||
bimSessions.delete(sessionId);
|
||||
}
|
||||
clearBimSessionCookie(res);
|
||||
};
|
||||
|
||||
const validateBimSessionWithLauncher = async (session) => {
|
||||
if (!BIM_AUTH_REQUIRED) {
|
||||
return {active: true, user: session?.user || null};
|
||||
}
|
||||
if (!INTERNAL_ACCESS_TOKEN || !session?.launcherSessionId) {
|
||||
return {active: false, error: "launcher_session_missing"};
|
||||
}
|
||||
const response = await fetch(new URL("/api/internal/session/validate", LAUNCHER_INTERNAL_BASE_URL), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${INTERNAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
serviceSlug: BIM_SERVICE_SLUG,
|
||||
launcherSessionId: session.launcherSessionId
|
||||
})
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload?.ok || payload.active !== true) {
|
||||
return {active: false, error: payload?.error || `launcher_session_invalid_${response.status}`};
|
||||
}
|
||||
return {active: true, user: payload.user || session.user};
|
||||
};
|
||||
|
||||
const getValidatedBimSession = async (req, res = null) => {
|
||||
const session = getCurrentBimSession(req);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const validation = await validateBimSessionWithLauncher(session);
|
||||
if (validation.active) {
|
||||
if (validation.user) {
|
||||
session.user = validation.user;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
bimSessions.delete(session.id);
|
||||
if (res) {
|
||||
clearBimSessionCookie(res);
|
||||
clearBimHandoffCookie(res);
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.warn("[server] Launcher session validation failed", err?.message || err);
|
||||
bimSessions.delete(session.id);
|
||||
if (res) {
|
||||
clearBimSessionCookie(res);
|
||||
clearBimHandoffCookie(res);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const consumeBimHandoffCookie = (req, res) => {
|
||||
const token = parseCookies(req.headers.cookie || "")[BIM_HANDOFF_COOKIE];
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
req.__bimHandoffNavigation = true;
|
||||
clearBimHandoffCookie(res);
|
||||
return true;
|
||||
};
|
||||
|
||||
const getRequestBaseUrl = (req) => {
|
||||
if (BIM_PUBLIC_BASE_URL) {
|
||||
return BIM_PUBLIC_BASE_URL;
|
||||
|
|
@ -853,7 +946,7 @@ const shouldPromoteShareNavigation = (req, url) => {
|
|||
if (url.searchParams.get(SHARE_AUTH_GUEST_PARAM) === "1") {
|
||||
return false;
|
||||
}
|
||||
if (getCurrentBimSession(req)) {
|
||||
if (req.__bimHandoffNavigation) {
|
||||
return false;
|
||||
}
|
||||
const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
|
||||
|
|
@ -872,6 +965,33 @@ const redirectShareNavigationToPromotion = (res, token) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const currentRequestReturnPath = (url) => sanitizeReturnTo(`${url.pathname}${url.search || ""}`);
|
||||
|
||||
const handleLauncherOwnedNavigation = (req, res, url) => {
|
||||
if (!BIM_AUTH_REQUIRED || !INTERNAL_ACCESS_TOKEN || !isHtmlNavigation(req, url)) {
|
||||
return false;
|
||||
}
|
||||
if (isPublicSharePath(url) && url.searchParams.get(SHARE_AUTH_GUEST_PARAM) === "1") {
|
||||
destroyCurrentBimSession(req, res);
|
||||
clearBimHandoffCookie(res);
|
||||
return false;
|
||||
}
|
||||
if (consumeBimHandoffCookie(req, res)) {
|
||||
return false;
|
||||
}
|
||||
if (isPublicSharePath(url)) {
|
||||
return redirectShareNavigationToPromotion(res, shareTokenFromPath(url.pathname));
|
||||
}
|
||||
if (getCurrentBimSession(req)) {
|
||||
res.statusCode = 302;
|
||||
res.setHeader("Location", buildLauncherLaunchUrl(currentRequestReturnPath(url)));
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const sendAuthRequired = (req, res, url) => {
|
||||
const nextPath = sanitizeReturnTo(`${url.pathname}${url.search || ""}`);
|
||||
if (!INTERNAL_ACCESS_TOKEN) {
|
||||
|
|
@ -939,11 +1059,11 @@ const requestNeedsBimSession = (req, url) => {
|
|||
return isHtmlNavigation(req, url);
|
||||
};
|
||||
|
||||
const ensureBimSessionForRequest = (req, res, url) => {
|
||||
const ensureBimSessionForRequest = async (req, res, url) => {
|
||||
if (!requestNeedsBimSession(req, url)) {
|
||||
return false;
|
||||
}
|
||||
if (getCurrentBimSession(req)) {
|
||||
if (await getValidatedBimSession(req, res)) {
|
||||
return false;
|
||||
}
|
||||
sendAuthRequired(req, res, url);
|
||||
|
|
@ -969,7 +1089,10 @@ const consumeLauncherHandoff = async (token) => {
|
|||
if (!response.ok || !payload?.ok) {
|
||||
throw new Error(payload?.error || `Launcher handoff failed: HTTP ${response.status}`);
|
||||
}
|
||||
return payload.user;
|
||||
return {
|
||||
user: payload.user,
|
||||
launcherSessionId: typeof payload.launcherSessionId === "string" ? payload.launcherSessionId : null
|
||||
};
|
||||
};
|
||||
|
||||
const safeProjectId = (id) => {
|
||||
|
|
@ -1623,8 +1746,8 @@ const handleGetUploadVersions = async (req, res, searchParams) => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleAuthSession = (req, res) => {
|
||||
const session = getCurrentBimSession(req);
|
||||
const handleAuthSession = async (req, res) => {
|
||||
const session = await getValidatedBimSession(req, res);
|
||||
sendJSON(res, 200, {
|
||||
ok: true,
|
||||
authRequired: BIM_AUTH_REQUIRED,
|
||||
|
|
@ -1643,8 +1766,9 @@ const handleLauncherHandoff = async (req, res, url) => {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const user = await consumeLauncherHandoff(token);
|
||||
createBimSession(res, user);
|
||||
const handoff = await consumeLauncherHandoff(token);
|
||||
createBimSession(res, handoff.user, {launcherSessionId: handoff.launcherSessionId});
|
||||
setBimHandoffCookie(res);
|
||||
res.statusCode = 302;
|
||||
res.setHeader("Location", nextPath);
|
||||
res.end();
|
||||
|
|
@ -1691,6 +1815,7 @@ const handleLogout = (req, res, {redirect = true} = {}) => {
|
|||
bimSessions.delete(sessionId);
|
||||
}
|
||||
clearBimSessionCookie(res);
|
||||
clearBimHandoffCookie(res);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
if (redirect) {
|
||||
res.statusCode = 302;
|
||||
|
|
@ -1704,7 +1829,7 @@ const handleLogout = (req, res, {redirect = true} = {}) => {
|
|||
};
|
||||
|
||||
const handleCreateShare = async (req, res) => {
|
||||
const session = getCurrentBimSession(req);
|
||||
const session = await getValidatedBimSession(req, res);
|
||||
if (BIM_AUTH_REQUIRED && !session) {
|
||||
sendJSON(res, 401, {ok: false, error: "bim_auth_required"});
|
||||
return;
|
||||
|
|
@ -1807,7 +1932,7 @@ const handleGetShare = async (req, res, token) => {
|
|||
sendText(res, 404, "Share not found");
|
||||
return;
|
||||
}
|
||||
const session = getCurrentBimSession(req);
|
||||
const session = await getValidatedBimSession(req, res);
|
||||
const authenticated = !!session;
|
||||
const promotionUrls = BIM_AUTH_REQUIRED && !authenticated
|
||||
? buildSharePromotionUrls(safeToken)
|
||||
|
|
@ -3315,6 +3440,10 @@ const requestHandler = async (req, res) => {
|
|||
return handleLogout(req, res, {redirect: false});
|
||||
}
|
||||
|
||||
if (handleLauncherOwnedNavigation(req, res, url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET" && /^\/api\/shares\/[^/]+\/?$/.test(url.pathname)) {
|
||||
return handleGetShare(req, res, shareTokenFromPath(url.pathname));
|
||||
}
|
||||
|
|
@ -3323,7 +3452,7 @@ const requestHandler = async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (ensureBimSessionForRequest(req, res, url)) {
|
||||
if (await ensureBimSessionForRequest(req, res, url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue