diff --git a/apps/frackend/package-lock.json b/apps/frackend/package-lock.json index 1b530ead7..454ff7ead 100644 --- a/apps/frackend/package-lock.json +++ b/apps/frackend/package-lock.json @@ -17,11 +17,12 @@ "http-proxy-middleware": "3.0.0-beta.0", "jwks-rsa": "3.0.1", "morgan": "1.10.0", + "node-cache": "5.1.2", "node-jose": "2.2.0", "uuid": "9.0.0" }, "devDependencies": { - "@types/cookie-parser": "^1.4.3", + "@types/cookie-parser": "1.4.3", "@types/express": "4.17.17", "@types/express-session": "1.17.7", "@types/node": "20.5.7", @@ -1484,6 +1485,14 @@ "node": ">=0.8.0" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3958,6 +3967,17 @@ "node": ">= 0.6" } }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -6653,6 +6673,11 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8477,6 +8502,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + } + }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", diff --git a/apps/frackend/package.json b/apps/frackend/package.json index 0f378ce6a..f09fb0954 100644 --- a/apps/frackend/package.json +++ b/apps/frackend/package.json @@ -17,17 +17,16 @@ "cookie-parser": "1.4.6", "express": "4.18.2", "express-jwt": "8.4.1", - "express-session": "1.17.3", "http-proxy-middleware": "3.0.0-beta.0", "jwks-rsa": "3.0.1", "morgan": "1.10.0", + "node-cache": "5.1.2", "node-jose": "2.2.0", "uuid": "9.0.0" }, "devDependencies": { "@types/cookie-parser": "1.4.3", "@types/express": "4.17.17", - "@types/express-session": "1.17.7", "@types/node": "20.5.7", "@types/node-jose": "1.1.10", "@types/uuid": "9.0.2", diff --git a/apps/frackend/src/apiProxy.ts b/apps/frackend/src/apiProxy.ts index b73dffb1e..48230ac42 100644 --- a/apps/frackend/src/apiProxy.ts +++ b/apps/frackend/src/apiProxy.ts @@ -3,6 +3,7 @@ import { createProxyMiddleware } from "http-proxy-middleware"; import config from "./config.js"; import { addOnBehalfOfToken } from "./onbehalfof.js"; +import { getOboTokenForRequest } from "./sessionCache"; import { verifyJWTToken } from "./tokenValidation.js"; function setupProxy( @@ -22,9 +23,8 @@ function setupProxy( logger: console, on: { proxyReq: (proxyRequest, request) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const accessToken = request?.session[scope]?.accessToken; + const accessToken = getOboTokenForRequest(request, scope) + ?.accessToken; if (accessToken) { proxyRequest.setHeader("Authorization", `Bearer ${accessToken}`); } else { diff --git a/apps/frackend/src/onbehalfof.ts b/apps/frackend/src/onbehalfof.ts index 10d15c844..747ccd252 100644 --- a/apps/frackend/src/onbehalfof.ts +++ b/apps/frackend/src/onbehalfof.ts @@ -4,6 +4,7 @@ import jose from "node-jose"; import { v4 as uuidv4 } from "uuid"; import config from "./config.js"; +import { getOboTokenForRequest, setOboTokenForRequest } from "./sessionCache"; import { getTokenFromRequestHeader } from "./tokenValidation.js"; const azureAdHeaderConfig = { @@ -24,12 +25,12 @@ export async function addOnBehalfOfToken( next: NextFunction, scope: string, ) { - const currentSession = request.session[scope]; - if (currentSession) { - if (currentSession.expiresAt > Date.now() / 1000 + 10) { + const currentOboToken = getOboTokenForRequest(request, scope); + if (currentOboToken) { + if (currentOboToken.expiresAt > Date.now() / 1000 + 10) { return next(); } - const token = await getRefreshToken(currentSession.refreshToken, scope); + const token = await getRefreshToken(currentOboToken.refreshToken, scope); updateSession(request, scope, token); return next(); } @@ -53,11 +54,12 @@ const updateSession = ( scope: string, result: OnBehalfOfResponse, ) => { - request.session[scope] = { + const oboToken = { expiresAt: Date.now() / 1000 + result.expires_in, accessToken: result.access_token, refreshToken: result.refresh_token, }; + setOboTokenForRequest(request, oboToken, scope); }; async function getOnBehalfOfToken(request: Request, scope: string) { diff --git a/apps/frackend/src/server.ts b/apps/frackend/src/server.ts index ec4b4dd74..128579c4f 100644 --- a/apps/frackend/src/server.ts +++ b/apps/frackend/src/server.ts @@ -1,9 +1,9 @@ +import cookieParser from "cookie-parser"; import express from "express"; import { setupActuators } from "./actuators.js"; import { setupNomApiProxy, setupTeamcatApiProxy } from "./apiProxy.js"; import { setupStaticRoutes } from "./frontendRoute.js"; -import { setupSession } from "./session.js"; // Create Express Server const app = express(); @@ -12,7 +12,9 @@ const app = express(); app.use(express.urlencoded({ extended: true })); setupActuators(app); -setupSession(app); + +app.set("trust proxy", 1); +app.use(cookieParser()); setupNomApiProxy(app); setupTeamcatApiProxy(app); diff --git a/apps/frackend/src/session.ts b/apps/frackend/src/session.ts deleted file mode 100644 index aed0ece94..000000000 --- a/apps/frackend/src/session.ts +++ /dev/null @@ -1,40 +0,0 @@ -import cookieParser from "cookie-parser"; -import { Express } from "express"; -import session, { SessionOptions } from "express-session"; -import { v4 as uuidv4 } from "uuid"; - -import config from "./config.js"; - -declare module "express-session" { - interface SessionData { - [sessionId: string]: { - expiresAt: number; - accessToken: string; - refreshToken: string; - }; - } -} - -// A valid session last 10 hours -const SESSION_MAX_AGE_MILLISECONDS = 10 * 60 * 60 * 1000; - -export const setupSession = (app: Express) => { - app.set("trust proxy", 1); - app.use(cookieParser()); - - const options: SessionOptions = { - cookie: { - maxAge: SESSION_MAX_AGE_MILLISECONDS, - sameSite: "lax", - httpOnly: true, - secure: config.app.nodeEnv === "production", - }, - secret: uuidv4(), - name: "nom-ui-session", - resave: false, - saveUninitialized: true, - unset: "destroy", - }; - - app.use(session(options)); -}; diff --git a/apps/frackend/src/sessionCache.ts b/apps/frackend/src/sessionCache.ts new file mode 100644 index 000000000..92f0fcb71 --- /dev/null +++ b/apps/frackend/src/sessionCache.ts @@ -0,0 +1,66 @@ +import * as crypto from "node:crypto"; +import { IncomingMessage } from "node:http"; + +import NodeCache from "node-cache"; + +export const sessionCache = new NodeCache({ + stdTTL: 60 * 60, // 1 hour +}); + +type SessionCacheValue = { + [scope: string]: OboToken; +}; + +type OboToken = { + expiresAt: number; + accessToken: string; + refreshToken: string; +}; + +export function setOboTokenForRequest( + request: IncomingMessage, + oboToken: OboToken, + scope: string, +) { + const hashedAuthHeader = getHashedAuthHeader(request); + + if (!hashedAuthHeader) { + return; + } + + const cachedValue = sessionCache.get(hashedAuthHeader); + + if (!cachedValue) { + return; + } + + cachedValue[scope] = oboToken; + + sessionCache.set(hashedAuthHeader, cachedValue); +} + +export function getOboTokenForRequest(request: IncomingMessage, scope: string) { + const hashedAuthHeader = getHashedAuthHeader(request); + + if (!hashedAuthHeader) { + return; + } + + const cachedValue = sessionCache.get(hashedAuthHeader); + + if (!cachedValue) { + return; + } + + return cachedValue[scope]; +} + +function getHashedAuthHeader(request: IncomingMessage) { + const authToken = request.headers["authorization"]; + + if (!authToken) { + return; + } + + return crypto.createHash("md5").update(authToken).digest("hex"); +}