diff --git a/.env.template b/.env.template index 219faa18..56d79ede 100644 --- a/.env.template +++ b/.env.template @@ -16,3 +16,4 @@ API_BACKEND_URL= # Replace this value with a strong, randomly generated string (at least 32 characters). # Example for generation in Node.js: require('crypto').randomBytes(32).toString('hex') COOKIE_SECRET= +SESSION_SECRET= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c0b8f4da..45eb42d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@fastify/cookie": "^11.0.2", "@fastify/env": "^5.0.2", "@fastify/http-proxy": "^11.1.2", + "@fastify/secure-session": "^8.2.0", "@fastify/sensible": "^6.0.3", "@fastify/session": "^11.1.0", "@fastify/static": "^8.1.1", @@ -1419,6 +1420,30 @@ "undici": "^7.0.0" } }, + "node_modules/@fastify/secure-session": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@fastify/secure-session/-/secure-session-8.2.0.tgz", + "integrity": "sha512-E1linEHVV86c0Gt+ohujcuRsCeedhD2M3X5+a2aU9Ln0YDC0bbuA7bE6QQzf/HAacOpt9+CJqV5NqdlQr9ui0A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/cookie": "^11.0.1", + "fastify-plugin": "^5.0.0", + "sodium-native": "^4.0.10" + }, + "bin": { + "secure-session": "genkey.js" + } + }, "node_modules/@fastify/send": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.0.0.tgz", @@ -5393,6 +5418,74 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-addon-resolve": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.4.tgz", + "integrity": "sha512-unn6Vy/Yke6F99vg/7tcrvM2KUvIhTNniaSqDbam4AWkd4NhvDVSrQiRYVlNzUV2P7SPobkCK7JFVxrJk9btCg==", + "license": "Apache-2.0", + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.11.1.tgz", + "integrity": "sha512-DCxeT9i8sTs3vUMA3w321OX/oXtNEu5EjObQOnTmCdNp5RXHBAvAaBDHvAi9ta0q/948QPz+co6SsGi6aQMYRg==", + "license": "Apache-2.0", + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-semver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.1.tgz", + "integrity": "sha512-UtggzHLiTrmFOC/ogQ+Hy7VfoKoIwrP1UFcYtTxoCUdLtsIErT8+SWtOC2DH/snT9h+xDrcBEPcwKei1mzemgg==", + "license": "Apache-2.0" + }, + "node_modules/bare-url": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.1.6.tgz", + "integrity": "sha512-FgjDeR+/yDH34By4I0qB5NxAoWv7dOTYcOXwn73kr+c93HyC2lU6tnjifqUe33LKMJcDyCYPQjEAqgOQiXkE2Q==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -12359,6 +12452,19 @@ "throttleit": "^1.0.0" } }, + "node_modules/require-addon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.1.0.tgz", + "integrity": "sha512-KbXAD5q2+v1GJnkzd8zzbOxchTkStSyJZ9QwoCq3QwEXAaIlG3wDYRZGzVD357jmwaGY7hr5VaoEAL0BkF0Kvg==", + "license": "Apache-2.0", + "dependencies": { + "bare-addon-resolve": "^1.3.0", + "bare-url": "^2.1.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -13439,6 +13545,15 @@ "tslib": "^2.0.3" } }, + "node_modules/sodium-native": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", + "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", + "license": "MIT", + "dependencies": { + "require-addon": "^1.1.0" + } + }, "node_modules/sonic-boom": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", diff --git a/package.json b/package.json index df524fd7..7d63cb5e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@fastify/env": "^5.0.2", "@fastify/http-proxy": "^11.1.2", "@fastify/sensible": "^6.0.3", + "@fastify/secure-session": "^8.2.0", "@fastify/session": "^11.1.0", "@fastify/static": "^8.1.1", "@fastify/vite": "^8.1.3", @@ -83,4 +84,4 @@ "vite": "^6.3.4", "vitest": "^3.1.4" } -} +} \ No newline at end of file diff --git a/server/app.js b/server/app.js index ba1284a0..dcae9811 100644 --- a/server/app.js +++ b/server/app.js @@ -2,14 +2,18 @@ import path, { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import AutoLoad from "@fastify/autoload"; import envPlugin from "./config/env.js"; +import encryptedSession from "./encrypted-session.js"; export const options = {}; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -export default async function(fastify, opts) { +export default async function (fastify, opts) { await fastify.register(envPlugin); + fastify.register(encryptedSession, { + ...opts, + }); await fastify.register(AutoLoad, { dir: join(__dirname, "plugins"), @@ -20,4 +24,6 @@ export default async function(fastify, opts) { dir: join(__dirname, "routes"), options: { ...opts }, }); -} + + +} \ No newline at end of file diff --git a/server/config/env.js b/server/config/env.js index 42c7cb0f..7b023b21 100644 --- a/server/config/env.js +++ b/server/config/env.js @@ -11,6 +11,7 @@ const schema = { 'OIDC_SCOPES', 'POST_LOGIN_REDIRECT', 'COOKIE_SECRET', + 'SESSION_SECRET', 'API_BACKEND_URL', ], properties: { @@ -22,6 +23,7 @@ const schema = { OIDC_SCOPES: { type: 'string' }, POST_LOGIN_REDIRECT: { type: 'string' }, COOKIE_SECRET: { type: 'string' }, + SESSION_SECRET: { type: 'string' }, API_BACKEND_URL: { type: 'string' }, // System variables diff --git a/server/encrypted-session.js b/server/encrypted-session.js new file mode 100644 index 00000000..0a9b8034 --- /dev/null +++ b/server/encrypted-session.js @@ -0,0 +1,232 @@ +import secureSession from "@fastify/secure-session"; +import fp from "fastify-plugin"; +import fastifyCookie from "@fastify/cookie"; +import fastifySession from '@fastify/session'; +import crypto from "node:crypto" + + + +export const COOKIE_NAME_ENCRYPTION_KEY = "session_encryption_key"; +export const COOKIE_NAME_SESSION = "session-cookie"; + +export const SECURE_SESSION_NAME = "encryptedSessionInternal"; +export const UNDERLYING_SESSION_NAME = "underlyingSessionNotPerUserEncrypted"; + +// This is the key used to store the encryption key in the secure session cookie +export const SECURE_COOKIE_KEY_ENCRYPTION_KEY = "encryptionKey"; + +export const REQUEST_DECORATOR = "encryptedSession"; + +async function encryptedSession(fastify) { + const { COOKIE_SECRET, SESSION_SECRET, NODE_ENV } = fastify.config; + + await fastify.register(fastifyCookie); + + fastify.register(secureSession, { + secret: Buffer.from(COOKIE_SECRET, "hex"), + cookieName: COOKIE_NAME_ENCRYPTION_KEY, + sessionName: SECURE_SESSION_NAME, + cookie: { + path: "/", + httpOnly: true, + sameSite: "lax", + secure: NODE_ENV === "production", + maxAge: 60 * 60 * 24 * 7, // 7 days + }, + }); + + + fastify.register(fastifySession, { + secret: SESSION_SECRET, + cookieName: COOKIE_NAME_SESSION, + // sessionName: UNDERLYING_SESSION_NAME, //NOT POSSIBLE to change the name it is decorated on the request object + cookie: { + path: "/", + httpOnly: true, + sameSite: "lax", + secure: NODE_ENV === "production", + maxAge: 60 * 60 * 24 * 7, // 7 days + }, + }); + + fastify.addHook('onRequest', (request, _reply, next) => { + //we use secure-session cookie to get the encryption key and decrypt the store + if (!request[SECURE_SESSION_NAME].get(SECURE_COOKIE_KEY_ENCRYPTION_KEY)) { + request.log.info({ "plugin": "encrypted-session" }, "user-side encryption key not found, creating new one"); + + let newEncryptionKey = generateSecureEncryptionKey(); + request[SECURE_SESSION_NAME].set(SECURE_COOKIE_KEY_ENCRYPTION_KEY, newEncryptionKey.toString('base64')); + request[REQUEST_DECORATOR] = createStore() + newEncryptionKey = undefined + } else { + request.log.info({ "plugin": "encrypted-session" }, "user-side encryption key found, using existing one"); + + const loadedEncryptionKey = Buffer.from(request[SECURE_SESSION_NAME].get(SECURE_COOKIE_KEY_ENCRYPTION_KEY), "base64"); + + const encryptedStore = request.session.get("encryptedStore"); + if (encryptedStore) { + try { + const { cipherText, iv, tag } = encryptedStore; + + const decryptedCypherText = decryptSymetric(cipherText, iv, tag, loadedEncryptionKey); + const decryptedStore = JSON.parse(decryptedCypherText); + request[REQUEST_DECORATOR] = createStore(decryptedStore); + } catch (error) { + request.log.error({ "plugin": "encrypted-session" }, "Failed to parse encrypted session store", error); + request[REQUEST_DECORATOR] = createStore(); + } + } else { + // we could not parse the encrypted store, so we create a new one and it would overwrite the previously stored store. + request.log.info({ "plugin": "encrypted-session" }, "No encrypted store found, creating new empty store"); + request[REQUEST_DECORATOR] = createStore(); + } + } + + next() + }) + + //TODO maybe move to onResponse after res is send. Lifecycle Doc https://fastify.dev/docs/latest/Reference/Lifecycle/ + // onSend is called before the response is send. Here we take encrypt the Session object and store it in the fastify-session. + // Then we also want to make sure the unencrypted object is removed from memory + fastify.addHook('onSend', async (request, reply, _payload) => { + const encryptionKey = Buffer.from(request[SECURE_SESSION_NAME].get(SECURE_COOKIE_KEY_ENCRYPTION_KEY), "base64"); + if (!encryptionKey) { + // if no encryption key is found in the secure session, we cannot encrypt the store. This should not happen since an encrption key is generated when the request arrived + request.log.error({ "plugin": "encrypted-session" }, "No encryption key found in secure session, cannot encrypt store"); + throw new Error("No encryption key found in secure session, cannot encrypt store"); + } + + //we store everything in one value in the session, that might be problematic for future redis with expiration times per key. we might want to split this + const stringifiedData = request[REQUEST_DECORATOR].stringify(); + const { cipherText, iv, tag } = encryptSymetric(stringifiedData, encryptionKey); + + //remove unencrypted data from memory + delete request[REQUEST_DECORATOR]; + request[REQUEST_DECORATOR] = null; + + request.session.set("encryptedStore", { + cipherText, + iv, + tag, + }); + await request.session.save() + request.log.info("store encrypted and set into request.session.encryptedStore"); + }) +} + +export default fp(encryptedSession); + +// use a closure to encapsulate the session data so noone can reference it and we are the only ones keeping a reference +function createStore(previousValue) { + let unencryptedStore = {}; // Private variable + if (previousValue) { + unencryptedStore = previousValue; + } + return { + set(key, value) { + unencryptedStore[key] = value; + }, + get(key) { + return unencryptedStore[key]; + }, + delete(key) { + delete unencryptedStore[key]; + }, + stringify() { + return JSON.stringify(unencryptedStore); + }, + clear() { + unencryptedStore = {}; // Clear all data + }, + }; +} + +// generates a secure encryption key for aes-256-gcm. +// Returns a buffer of 32 bytes (256 bits). +function generateSecureEncryptionKey() { + // Generates a secure random encryption key of 32 bytes (256 bits) + return crypto.randomBytes(32); +} + +// uses authenticated symetric encryption (aes-256-gcm) to encrypt the plaintext with the key. +// If no adequate key is given, it throws an error +// The key needs to be 32bytes (256bits) as type buffer. Needs to be cryptographically secure random generated e.g. with `crypto.randomBytes(32)` +// it outputs cipherText (bas64 encoded string), the initialisation vector (iv) (hex string) and the authentication tag (hex string). +function encryptSymetric(plaintext, key) { + if (key == undefined) { + throw new Error("Key must be provided"); + } + if (key.length < 32) { + throw new Error("Key must be at least 32 byte = 256 bits long"); + } + + if (!(key instanceof Buffer)) { + throw new Error("Key must be a Buffer"); + } + + if (plaintext == undefined) { + throw new Error("Plaintext must be provided"); + } + + if (typeof plaintext !== "string") { + throw new Error("Plaintext must be a string utf8 encoded"); + } + + if (!crypto.getCiphers().includes("aes-256-gcm")) { + throw new Error("Cipher suite aes-256-gcm is not available"); + } + + // initialisation vector. Needs to be stored along the cipherText. + // MUST NOT be reused and MUST be randomly generated for EVERY encryption operation. Otherwise using the same key would be insecure. + const iv = crypto.randomBytes(12); + + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); + let cipherText = cipher.update(plaintext, 'utf8', 'base64'); + cipherText += cipher.final('base64'); + + // the authentication tag is used to verify the integrity of the ciphertext (that it has not been tampered with). + // stored alongside the ciphertext and iv as it can only be changed with the secret key + const tag = cipher.getAuthTag(); + + return { + cipherText, + iv: iv.toString('base64'), + tag: tag.toString('base64'), + } +} + +// uses authenticated symetric encryption (aes-256-gcm) to decrypt the ciphertext with the key. +// requires the ciphertext, the initialisation vector (iv)(hex string), the authentication tag (tag) (hex string) and the key (buffer) to be provided. +//it thows an error if the decryption or tag verification fails +function decryptSymetric(cipherText, iv, tag, key) { + if (key == undefined) { + throw new Error("Key must be provided"); + } + if (key.length < 32) { + throw new Error("Key must be at least 32bye = 256 bits long"); + } + + if (!(key instanceof Buffer)) { + throw new Error("Key must be a Buffer"); + } + + if (cipherText == undefined) { + throw new Error("Ciphertext must be provided"); + } + + if (typeof cipherText !== "string") { + throw new Error("Ciphertext must be a string utf8 encoded"); + } + + if (!crypto.getCiphers().includes("aes-256-gcm")) { + throw new Error("Cipher suite aes-256-gcm is not available"); + } + + const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, 'base64')); + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let decrypted = decipher.update(cipherText, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; +} \ No newline at end of file diff --git a/server/plugins/auth-utils.js b/server/plugins/auth-utils.js index f5fb8649..e8412645 100644 --- a/server/plugins/auth-utils.js +++ b/server/plugins/auth-utils.js @@ -77,7 +77,7 @@ async function authUtilsPlugin(fastify) { request.log.info("Preparing OIDC login redirect."); const { redirectTo } = request.query; - request.session.set("postLoginRedirectRoute", redirectTo); + request.encryptedSession.set("postLoginRedirectRoute", redirectTo); const { clientId, redirectUri, scopes } = oidcConfig; @@ -85,12 +85,12 @@ async function authUtilsPlugin(fastify) { const codeVerifier = crypto.randomBytes(32).toString("base64url"); const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url"); - request.session.set("oauthState", state); - request.session.set("codeVerifier", codeVerifier); + request.encryptedSession.set("oauthState", state); + request.encryptedSession.set("codeVerifier", codeVerifier); request.log.info({ stateSet: Boolean(state), verifierSet: Boolean(codeVerifier), - }, "OAuth state and code verifier set in session."); + }, "OAuth state and code verifier set in encryptedSession."); const url = new URL(authorizationEndpoint); url.searchParams.set("response_type", "code"); @@ -116,7 +116,7 @@ async function authUtilsPlugin(fastify) { request.log.error("Missing authorization code in callback."); throw new AuthenticationError("Missing code in callback."); } - if (state !== request.session.get("oauthState")) { + if (state !== request.encryptedSession.get("oauthState")) { request.log.error("Invalid state in callback."); throw new AuthenticationError("Invalid state in callback."); } @@ -126,7 +126,7 @@ async function authUtilsPlugin(fastify) { code, redirect_uri: redirectUri, client_id: clientId, - code_verifier: request.session.get("codeVerifier"), + code_verifier: request.encryptedSession.get("codeVerifier"), }); const response = await fetch(tokenEndpoint, { @@ -146,7 +146,7 @@ async function authUtilsPlugin(fastify) { refreshToken: tokens.refresh_token, expiresAt: null, userInfo: extractUserInfoFromIdToken(request, tokens.id_token), - postLoginRedirectRoute: request.session.get("postLoginRedirectRoute") || "", + postLoginRedirectRoute: request.encryptedSession.get("postLoginRedirectRoute") || "", }; if (tokens.expires_in && typeof tokens.expires_in === "number") { diff --git a/server/plugins/http-proxy.js b/server/plugins/http-proxy.js index 9ab3bf58..21607b87 100644 --- a/server/plugins/http-proxy.js +++ b/server/plugins/http-proxy.js @@ -19,14 +19,14 @@ function proxyPlugin(fastify) { const keyRefreshToken = useCrate ? "onboarding_refreshToken" : "mcp_refreshToken"; // Check if there is an access token - const accessToken = request.session.get(keyAccessToken); + const accessToken = request.encryptedSession.get(keyAccessToken); if (!accessToken) { request.log.error("Missing access token."); return reply.unauthorized("Missing access token."); } // Check if the access token is expired or about to expire - const expiresAt = request.session.get(keyTokenExpiresAt); + const expiresAt = request.encryptedSession.get(keyTokenExpiresAt); const now = Date.now(); const REFRESH_BUFFER_SECONDS = 20; // to allow for network latency if (!expiresAt || now < expiresAt - REFRESH_BUFFER_SECONDS) { @@ -37,10 +37,10 @@ function proxyPlugin(fastify) { request.log.info({ expiresAt: new Date(expiresAt).toISOString() }, "Access token is expired or about to expire; attempting refresh."); // Check if there is a refresh token - const refreshToken = request.session.get(keyRefreshToken); + const refreshToken = request.encryptedSession.get(keyRefreshToken); if (!refreshToken) { - request.log.error("Missing refresh token; deleting session."); - request.session.destroy(); + request.log.error("Missing refresh token; deleting encryptedSession."); + request.encryptedSession.clear();//TODO: also clear user encrpytion key? return reply.unauthorized("Session expired without token refresh capability."); } @@ -54,23 +54,23 @@ function proxyPlugin(fastify) { }, issuerConfiguration.tokenEndpoint); if (!refreshedTokenData || !refreshedTokenData.accessToken) { request.log.error("Token refresh failed (no access token); deleting session."); - request.session.destroy(); + request.encryptedSession.clear();//TODO: also clear user encrpytion key? return reply.unauthorized("Session expired and token refresh failed."); } request.log.info("Token refresh successful; updating the session."); - request.session.set(keyAccessToken, refreshedTokenData.accessToken); + request.encryptedSession.set(keyAccessToken, refreshedTokenData.accessToken); if (refreshedTokenData.refreshToken) { - request.session.set(keyRefreshToken, refreshedTokenData.refreshToken); + request.encryptedSession.set(keyRefreshToken, refreshedTokenData.refreshToken); } else { - request.session.delete(keyRefreshToken); + request.encryptedSession.delete(keyRefreshToken); } if (refreshedTokenData.expiresIn) { const newExpiresAt = Date.now() + (refreshedTokenData.expiresIn * 1000); - request.session.set(keyTokenExpiresAt, newExpiresAt); + request.encryptedSession.set(keyTokenExpiresAt, newExpiresAt); } else { - request.session.delete(keyTokenExpiresAt); + request.encryptedSession.delete(keyTokenExpiresAt); } request.log.info("Token refresh successful and session updated; continuing with the HTTP request."); @@ -86,7 +86,7 @@ function proxyPlugin(fastify) { replyOptions: { rewriteRequestHeaders: (req, headers) => { const useCrate = req.headers["x-use-crate"]; - const accessToken = useCrate ? req.session.get("onboarding_accessToken") : `${req.session.get("onboarding_accessToken")},${req.session.get("mcp_accessToken")}`; + const accessToken = useCrate ? req.encryptedSession.get("onboarding_accessToken") : `${req.encryptedSession.get("onboarding_accessToken")},${req.encryptedSession.get("mcp_accessToken")}`; return { ...headers, diff --git a/server/plugins/session.js b/server/plugins/session.js deleted file mode 100644 index 213e9a17..00000000 --- a/server/plugins/session.js +++ /dev/null @@ -1,23 +0,0 @@ -import fastifySession from "@fastify/session"; -import fp from "fastify-plugin"; -import fastifyCookie from "@fastify/cookie"; - - -async function secureSessionPlugin(fastify) { - const { COOKIE_SECRET, NODE_ENV } = fastify.config; - - await fastify.register(fastifyCookie); - - fastify.register(fastifySession, { - secret: COOKIE_SECRET, - cookie: { - path: "/", - httpOnly: true, - sameSite: "lax", - secure: NODE_ENV === "production", - maxAge: 60 * 60 * 24 * 7, // 7 days - }, - }); -} - -export default fp(secureSessionPlugin); diff --git a/server/routes/auth-mcp.js b/server/routes/auth-mcp.js index 65d245d7..67809cc3 100644 --- a/server/routes/auth-mcp.js +++ b/server/routes/auth-mcp.js @@ -27,13 +27,13 @@ async function authPlugin(fastify) { redirectUri: OIDC_REDIRECT_URI, }, mcpIssuerConfiguration.tokenEndpoint); - req.session.set("mcp_accessToken", callbackResult.accessToken); - req.session.set("mcp_refreshToken", callbackResult.refreshToken); + req.encryptedSession.set("mcp_accessToken", callbackResult.accessToken); + req.encryptedSession.set("mcp_refreshToken", callbackResult.refreshToken); if (callbackResult.expiresAt) { - req.session.set("mcp_tokenExpiresAt", callbackResult.expiresAt); + req.encryptedSession.set("mcp_tokenExpiresAt", callbackResult.expiresAt); } else { - req.session.delete("mcp_tokenExpiresAt"); + req.encryptedSession.delete("mcp_tokenExpiresAt"); } reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); @@ -48,7 +48,7 @@ async function authPlugin(fastify) { }); fastify.get("/auth/mcp/me", async (req, reply) => { - const accessToken = req.session.get("mcp_accessToken"); + const accessToken = req.encryptedSession.get("mcp_accessToken"); const isAuthenticated = Boolean(accessToken); reply.send({ isAuthenticated }); diff --git a/server/routes/auth-onboarding.js b/server/routes/auth-onboarding.js index 9dc668a8..378574ae 100644 --- a/server/routes/auth-onboarding.js +++ b/server/routes/auth-onboarding.js @@ -29,14 +29,14 @@ async function authPlugin(fastify) { redirectUri: OIDC_REDIRECT_URI, }, issuerConfiguration.tokenEndpoint); - req.session.set("onboarding_accessToken", callbackResult.accessToken); - req.session.set("onboarding_refreshToken", callbackResult.refreshToken); - req.session.set("onboarding_userInfo", callbackResult.userInfo); + req.encryptedSession.set("onboarding_accessToken", callbackResult.accessToken); + req.encryptedSession.set("onboarding_refreshToken", callbackResult.refreshToken); + req.encryptedSession.set("onboarding_userInfo", callbackResult.userInfo); if (callbackResult.expiresAt) { - req.session.set("onboarding_tokenExpiresAt", callbackResult.expiresAt); + req.encryptedSession.set("onboarding_tokenExpiresAt", callbackResult.expiresAt); } else { - req.session.delete("onboarding_tokenExpiresAt"); + req.encryptedSession.delete("onboarding_tokenExpiresAt"); } reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); @@ -52,8 +52,8 @@ async function authPlugin(fastify) { fastify.get("/auth/onboarding/me", async (req, reply) => { - const accessToken = req.session.get("onboarding_accessToken"); - const userInfo = req.session.get("onboarding_userInfo"); + const accessToken = req.encryptedSession.get("onboarding_accessToken"); + const userInfo = req.encryptedSession.get("onboarding_userInfo"); const isAuthenticated = Boolean(accessToken); const user = isAuthenticated ? userInfo : null; @@ -62,7 +62,7 @@ async function authPlugin(fastify) { fastify.post("/auth/logout", async (req, reply) => { // TODO: Idp sign out flow - req.session.destroy(); + req.encryptedSession.clear(); reply.send({ message: "Logged out" }); }); }