From 1e752454f8544e40da5485bb6810a36fe0f8917b Mon Sep 17 00:00:00 2001 From: Moritz Reich Date: Wed, 9 Jul 2025 14:34:08 +0200 Subject: [PATCH 1/4] style: Update code formatting for consistency and readability & fixed fastify routes --- .prettierrc | 2 +- server/app.js | 18 ++--- server/encrypted-session.js | 120 +++++++++++++++--------------- server/plugins/auth-utils.js | 124 +++++++++++++++---------------- server/plugins/http-proxy.js | 71 ++++++++++-------- server/plugins/sensible.js | 4 +- server/routes/auth-mcp.js | 59 +++++++++------ server/routes/auth-onboarding.js | 73 ++++++++++-------- server/routes/feedback.js | 5 +- server/routes/root.js | 6 +- 10 files changed, 257 insertions(+), 225 deletions(-) diff --git a/.prettierrc b/.prettierrc index 0fe9c2d6..88295f5e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,6 @@ "semi": true, "tabWidth": 2, "trailingComma": "all", - "printWidth": 80, + "printWidth": 120, "singleQuote": true } diff --git a/server/app.js b/server/app.js index dcae9811..39f92085 100644 --- a/server/app.js +++ b/server/app.js @@ -1,8 +1,8 @@ -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"; +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 = {}; @@ -16,14 +16,12 @@ export default async function (fastify, opts) { }); await fastify.register(AutoLoad, { - dir: join(__dirname, "plugins"), + dir: join(__dirname, 'plugins'), options: { ...opts }, }); await fastify.register(AutoLoad, { - dir: join(__dirname, "routes"), + dir: join(__dirname, 'routes'), options: { ...opts }, }); - - -} \ No newline at end of file +} diff --git a/server/encrypted-session.js b/server/encrypted-session.js index 0da849c4..8013b48b 100644 --- a/server/encrypted-session.js +++ b/server/encrypted-session.js @@ -1,24 +1,23 @@ -import secureSession from "@fastify/secure-session"; -import fp from "fastify-plugin"; -import fastifyCookie from "@fastify/cookie"; +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" +import crypto from 'node:crypto'; - -// name of the request decorator this plugin exposes. Using request.encryptedSession can be used with set, get, clear delete +// name of the request decorator this plugin exposes. Using request.encryptedSession can be used with set, get, clear delete // functions and the encryption will then be handled in this plugin. -export const REQUEST_DECORATOR = "encryptedSession"; +export const REQUEST_DECORATOR = 'encryptedSession'; // name of the request decorator of the secure-session library that stores its session data in an encrypted cookie on user side. -export const ENCRYPTED_COOKIE_REQUEST_DECORATOR = "encryptedSessionInternal"; +export const ENCRYPTED_COOKIE_REQUEST_DECORATOR = 'encryptedSessionInternal'; // name of the request decorator of the session library that is used as underlying store for this library. -export const UNDERLYING_SESSION_NAME_REQUEST_DECORATOR = "underlyingSessionNotPerUserEncrypted"; +export const UNDERLYING_SESSION_NAME_REQUEST_DECORATOR = 'underlyingSessionNotPerUserEncrypted'; // name of the secure-session cookie that stores the encryption key on user side. -export const ENCRYPTION_KEY_COOKIE_NAME = "session_encryption_key"; +export const ENCRYPTION_KEY_COOKIE_NAME = 'session_encryption_key'; // the key used to store the encryption key in the secure-session cookie on user side. -export const ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY = "encryptionKey"; +export const ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY = 'encryptionKey'; // name of the cookie that stores the session identifier on user side. -export const SESSION_COOKIE_NAME = "session-cookie"; +export const SESSION_COOKIE_NAME = 'session-cookie'; async function encryptedSession(fastify) { const { COOKIE_SECRET, SESSION_SECRET, NODE_ENV } = fastify.config; @@ -26,14 +25,14 @@ async function encryptedSession(fastify) { await fastify.register(fastifyCookie); fastify.register(secureSession, { - secret: Buffer.from(COOKIE_SECRET, "hex"), + secret: Buffer.from(COOKIE_SECRET, 'hex'), cookieName: ENCRYPTION_KEY_COOKIE_NAME, sessionName: ENCRYPTED_COOKIE_REQUEST_DECORATOR, cookie: { - path: "/", + path: '/', httpOnly: true, - sameSite: "lax", - secure: NODE_ENV === "production", + sameSite: 'lax', + secure: NODE_ENV === 'production', maxAge: 60 * 60 * 24 * 7, // 7 days }, }); @@ -42,10 +41,10 @@ async function encryptedSession(fastify) { cookieName: SESSION_COOKIE_NAME, // sessionName: UNDERLYING_SESSION_NAME, //NOT POSSIBLE to change the name it is decorated on the request object cookie: { - path: "/", + path: '/', httpOnly: true, - sameSite: "lax", - secure: NODE_ENV === "production", + sameSite: 'lax', + secure: NODE_ENV === 'production', maxAge: 60 * 60 * 24 * 7, // 7 days }, }); @@ -53,18 +52,18 @@ async function encryptedSession(fastify) { fastify.addHook('onRequest', (request, _reply, next) => { const userEncryptionKey = getUserEncryptionKeyFromUserCookie(request); if (!userEncryptionKey) { - request.log.info({ "plugin": "encrypted-session" }, "user-side encryption key not found, creating new one"); + request.log.info({ plugin: 'encrypted-session' }, 'user-side encryption key not found, creating new one'); let newEncryptionKey = generateSecureEncryptionKey(); setUserEncryptionKeyIntoUserCookie(request, newEncryptionKey); - request[REQUEST_DECORATOR] = createStore() - newEncryptionKey = undefined + request[REQUEST_DECORATOR] = createStore(); + newEncryptionKey = undefined; } else { - request.log.info({ "plugin": "encrypted-session" }, "user-side encryption key found, using existing one"); + request.log.info({ plugin: 'encrypted-session' }, 'user-side encryption key found, using existing one'); - const loadedEncryptionKey = Buffer.from(userEncryptionKey, "base64"); + const loadedEncryptionKey = Buffer.from(userEncryptionKey, 'base64'); - const encryptedStore = request.session.get("encryptedStore"); + const encryptedStore = request.session.get('encryptedStore'); if (encryptedStore) { try { const { cipherText, iv, tag } = encryptedStore; @@ -73,28 +72,31 @@ async function encryptedSession(fastify) { 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.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.log.info({ plugin: 'encrypted-session' }, 'No encrypted store found, creating new empty store'); request[REQUEST_DECORATOR] = createStore(); } } - next() - }) + 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(getUserEncryptionKeyFromUserCookie(request), "base64"); + fastify.addHook('onSend', async (request, _reply, payload) => { + const encryptionKey = Buffer.from(getUserEncryptionKeyFromUserCookie(request), '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"); + 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 @@ -105,17 +107,19 @@ async function encryptedSession(fastify) { delete request[REQUEST_DECORATOR]; request[REQUEST_DECORATOR] = null; - request.session.set("encryptedStore", { + request.session.set('encryptedStore', { cipherText, iv, tag, }); - await request.session.save() - request.log.info("store encrypted and set into request.session.encryptedStore"); - }) + await request.session.save(); + request.log.info('store encrypted and set into request.session.encryptedStore'); + + return payload; + }); function getUserEncryptionKeyFromUserCookie(request) { - return request[ENCRYPTED_COOKIE_REQUEST_DECORATOR].get(ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY) + return request[ENCRYPTED_COOKIE_REQUEST_DECORATOR].get(ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY); } function setUserEncryptionKeyIntoUserCookie(request, key) { @@ -163,33 +167,33 @@ function generateSecureEncryptionKey() { // 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"); + throw new Error('Key must be provided'); } if (key.length < 32) { - throw new Error("Key must be at least 32 byte = 256 bits long"); + 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"); + throw new Error('Key must be a Buffer'); } if (plaintext == undefined) { - throw new Error("Plaintext must be provided"); + throw new Error('Plaintext must be provided'); } - if (typeof plaintext !== "string") { - throw new Error("Plaintext must be a string utf8 encoded"); + 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"); + 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); + const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let cipherText = cipher.update(plaintext, 'utf8', 'base64'); cipherText += cipher.final('base64'); @@ -201,7 +205,7 @@ function encryptSymetric(plaintext, key) { cipherText, iv: iv.toString('base64'), tag: tag.toString('base64'), - } + }; } // uses authenticated symetric encryption (aes-256-gcm) to decrypt the ciphertext with the key. @@ -209,33 +213,33 @@ function encryptSymetric(plaintext, key) { //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"); + throw new Error('Key must be provided'); } if (key.length < 32) { - throw new Error("Key must be at least 32 byte = 256 bits long"); + 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"); + throw new Error('Key must be a Buffer'); } if (cipherText == undefined) { - throw new Error("Ciphertext must be provided"); + throw new Error('Ciphertext must be provided'); } - if (typeof cipherText !== "string") { - throw new Error("Ciphertext must be a string utf8 encoded"); + 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"); + 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')); + 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 78b6e58b..930e086a 100644 --- a/server/plugins/auth-utils.js +++ b/server/plugins/auth-utils.js @@ -1,18 +1,17 @@ -import fp from "fastify-plugin"; -import crypto from "node:crypto"; - +import fp from 'fastify-plugin'; +import crypto from 'node:crypto'; export class AuthenticationError extends Error { constructor(message) { super(message); this.name = this.constructor.name; - this.code = "ERR_AUTHENTICATION"; + this.code = 'ERR_AUTHENTICATION'; Error.captureStackTrace(this, this.constructor); } } async function getRemoteOpenIdConfiguration(issuerBaseUrl) { - const url = new URL("/.well-known/openid-configuration", issuerBaseUrl).toString(); + const url = new URL('/.well-known/openid-configuration', issuerBaseUrl).toString(); const res = await fetch(url); if (!res.ok) { throw new AuthenticationError(`OIDC discovery failed: ${res.status} ${res.statusText}`); @@ -23,13 +22,12 @@ async function getRemoteOpenIdConfiguration(issuerBaseUrl) { function isAllowedRedirectTo(value) { if (!value) return true; const first = value.charAt(0); - return first === "/" || first === "#"; + return first === '/' || first === '#'; } - async function authUtilsPlugin(fastify) { - fastify.decorate("discoverIssuerConfiguration", async (issuerBaseUrl) => { - fastify.log.info({ issuer: issuerBaseUrl }, "Discovering OpenId configuration."); + fastify.decorate('discoverIssuerConfiguration', async (issuerBaseUrl) => { + fastify.log.info({ issuer: issuerBaseUrl }, 'Discovering OpenId configuration.'); const remoteConfiguration = await getRemoteOpenIdConfiguration(issuerBaseUrl); @@ -38,39 +36,39 @@ async function authUtilsPlugin(fastify) { tokenEndpoint: remoteConfiguration.token_endpoint, }; - fastify.log.info({ issuer: issuerBaseUrl, requiredConfiguration }, "OpenId configuration discovered."); + fastify.log.info({ issuer: issuerBaseUrl, requiredConfiguration }, 'OpenId configuration discovered.'); return requiredConfiguration; }); - fastify.decorate("refreshAuthTokens", async (currentRefreshToken, oidcConfig, tokenEndpoint) => { - fastify.log.info("Refreshing tokens."); + fastify.decorate('refreshAuthTokens', async (currentRefreshToken, oidcConfig, tokenEndpoint) => { + fastify.log.info('Refreshing tokens.'); const { clientId, scopes } = oidcConfig; const body = new URLSearchParams({ - grant_type: "refresh_token", + grant_type: 'refresh_token', refresh_token: currentRefreshToken, client_id: clientId, scope: scopes, }); const response = await fetch(tokenEndpoint, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', }, body: body.toString(), }); const responseBodyText = await response.text(); if (!response.ok) { - fastify.log.error({ status: response.status, idpResponseBody: responseBodyText }, "Token refresh failed."); - throw new AuthenticationError("Token refresh failed."); + fastify.log.error({ status: response.status, idpResponseBody: responseBodyText }, 'Token refresh failed.'); + throw new AuthenticationError('Token refresh failed.'); } const newTokens = JSON.parse(responseBodyText); - fastify.log.info("Token refresh successful; received new tokens."); + fastify.log.info('Token refresh successful; received new tokens.'); return { accessToken: newTokens.access_token, @@ -79,74 +77,77 @@ async function authUtilsPlugin(fastify) { }; }); - fastify.decorate("prepareOidcLoginRedirect", (request, oidcConfig, authorizationEndpoint) => { - request.log.info("Preparing OIDC login redirect."); + fastify.decorate('prepareOidcLoginRedirect', (request, oidcConfig, authorizationEndpoint) => { + request.log.info('Preparing OIDC login redirect.'); const { redirectTo } = request.query; if (!isAllowedRedirectTo(redirectTo)) { request.log.error(`Invalid redirectTo: "${redirectTo}".`); - throw new AuthenticationError("Invalid redirectTo."); + throw new AuthenticationError('Invalid redirectTo.'); } - request.encryptedSession.set("postLoginRedirectRoute", redirectTo); + request.encryptedSession.set('postLoginRedirectRoute', redirectTo); const { clientId, redirectUri, scopes } = oidcConfig; - const state = crypto.randomBytes(16).toString("hex"); - const codeVerifier = crypto.randomBytes(32).toString("base64url"); - const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url"); + const state = crypto.randomBytes(16).toString('hex'); + const codeVerifier = crypto.randomBytes(32).toString('base64url'); + const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url'); - 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 encryptedSession."); + 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 encryptedSession.', + ); const url = new URL(authorizationEndpoint); - url.searchParams.set("response_type", "code"); - url.searchParams.set("client_id", clientId); - url.searchParams.set("redirect_uri", redirectUri); - url.searchParams.set("scope", scopes); - url.searchParams.set("state", state); - url.searchParams.set("code_challenge", codeChallenge); - url.searchParams.set("code_challenge_method", "S256"); + url.searchParams.set('response_type', 'code'); + url.searchParams.set('client_id', clientId); + url.searchParams.set('redirect_uri', redirectUri); + url.searchParams.set('scope', scopes); + url.searchParams.set('state', state); + url.searchParams.set('code_challenge', codeChallenge); + url.searchParams.set('code_challenge_method', 'S256'); - request.log.info("Prepared OIDC login redirect."); + request.log.info('Prepared OIDC login redirect.'); return url.toString(); }); - fastify.decorate("handleOidcCallback", async (request, oidcConfig, tokenEndpoint) => { - request.log.info("Handling OIDC callback to retrieve the tokens."); + fastify.decorate('handleOidcCallback', async (request, oidcConfig, tokenEndpoint) => { + request.log.info('Handling OIDC callback to retrieve the tokens.'); const { clientId, redirectUri } = oidcConfig; const { code, state } = request.query; if (!code) { - request.log.error("Missing authorization code in callback."); - throw new AuthenticationError("Missing code in callback."); + request.log.error('Missing authorization code in callback.'); + throw new AuthenticationError('Missing code in callback.'); } - if (state !== request.encryptedSession.get("oauthState")) { - request.log.error("Invalid state in callback."); - throw new AuthenticationError("Invalid state in callback."); + if (state !== request.encryptedSession.get('oauthState')) { + request.log.error('Invalid state in callback.'); + throw new AuthenticationError('Invalid state in callback.'); } const body = new URLSearchParams({ - grant_type: "authorization_code", + grant_type: 'authorization_code', code, redirect_uri: redirectUri, client_id: clientId, - code_verifier: request.encryptedSession.get("codeVerifier"), + code_verifier: request.encryptedSession.get('codeVerifier'), }); const response = await fetch(tokenEndpoint, { - method: "POST", - headers: { "content-type": "application/x-www-form-urlencoded" }, + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, body, }); if (!response.ok) { - request.log.error({ status: response.status, body: await response.text() }, "Token exchange failed."); - throw new AuthenticationError("Token exchange failed."); + request.log.error({ status: response.status, body: await response.text() }, 'Token exchange failed.'); + throw new AuthenticationError('Token exchange failed.'); } const tokens = await response.json(); @@ -156,35 +157,34 @@ async function authUtilsPlugin(fastify) { refreshToken: tokens.refresh_token, expiresAt: null, userInfo: extractUserInfoFromIdToken(request, tokens.id_token), - postLoginRedirectRoute: request.encryptedSession.get("postLoginRedirectRoute") || "", + postLoginRedirectRoute: request.encryptedSession.get('postLoginRedirectRoute') || '', }; - if (tokens.expires_in && typeof tokens.expires_in === "number") { - const expiresAt = Date.now() + (tokens.expires_in * 1000); + if (tokens.expires_in && typeof tokens.expires_in === 'number') { + const expiresAt = Date.now() + tokens.expires_in * 1000; result.expiresAt = expiresAt; } - request.log.info("OIDC callback succeeded; tokens retrieved."); + request.log.info('OIDC callback succeeded; tokens retrieved.'); return result; }); - } function extractUserInfoFromIdToken(request, idToken) { - request.log.info("Extracting user info from ID token."); + request.log.info('Extracting user info from ID token.'); if (!idToken) { - request.log.warn("No ID token provided."); + request.log.warn('No ID token provided.'); return null; } const payloadBase64 = idToken.split('.')[1]; const decodedPayload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8')); - request.log.info("User info extracted from ID token."); + request.log.info('User info extracted from ID token.'); return { email: decodedPayload.email, - } + }; } export default fp(authUtilsPlugin); diff --git a/server/plugins/http-proxy.js b/server/plugins/http-proxy.js index e0700a0e..64d759cf 100644 --- a/server/plugins/http-proxy.js +++ b/server/plugins/http-proxy.js @@ -1,28 +1,28 @@ -import fp from "fastify-plugin"; -import httpProxy from "@fastify/http-proxy"; -import { AuthenticationError } from "./auth-utils.js"; +import fp from 'fastify-plugin'; +import httpProxy from '@fastify/http-proxy'; +import { AuthenticationError } from './auth-utils.js'; function proxyPlugin(fastify) { const { API_BACKEND_URL } = fastify.config; const { OIDC_CLIENT_ID, OIDC_SCOPES } = fastify.config; fastify.register(httpProxy, { - prefix: "/onboarding", + prefix: '/onboarding', upstream: API_BACKEND_URL, preHandler: async (request, reply) => { - request.log.info("Entering HTTP proxy preHandler."); + request.log.info('Entering HTTP proxy preHandler.'); - const useCrate = request.headers["x-use-crate"]; + const useCrate = request.headers['x-use-crate']; - const keyAccessToken = useCrate ? "onboarding_accessToken" : "mcp_accessToken"; - const keyTokenExpiresAt = useCrate ? "onboarding_tokenExpiresAt" : "mcp_tokenExpiresAt"; - const keyRefreshToken = useCrate ? "onboarding_refreshToken" : "mcp_refreshToken"; + const keyAccessToken = useCrate ? 'onboarding_accessToken' : 'mcp_accessToken'; + const keyTokenExpiresAt = useCrate ? 'onboarding_tokenExpiresAt' : 'mcp_tokenExpiresAt'; + const keyRefreshToken = useCrate ? 'onboarding_refreshToken' : 'mcp_refreshToken'; // Check if there is an access token const accessToken = request.encryptedSession.get(keyAccessToken); if (!accessToken) { - request.log.error("Missing access token."); - return reply.unauthorized("Missing access token."); + request.log.error('Missing access token.'); + return reply.unauthorized('Missing access token.'); } // Check if the access token is expired or about to expire @@ -30,35 +30,42 @@ function proxyPlugin(fastify) { const now = Date.now(); const REFRESH_BUFFER_MILLISECONDS = 20 * 1000; // to allow for network latency if (!expiresAt || now < expiresAt - REFRESH_BUFFER_MILLISECONDS) { - request.log.info("Access token is still valid; no refresh needed."); + request.log.info('Access token is still valid; no refresh needed.'); return; } - request.log.info({ expiresAt: new Date(expiresAt).toISOString() }, "Access token is expired or about to expire; attempting refresh."); + 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.encryptedSession.get(keyRefreshToken); if (!refreshToken) { - 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."); + 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.'); } // Attempt to refresh the tokens try { const issuerConfiguration = fastify.issuerConfiguration; - const refreshedTokenData = await fastify.refreshAuthTokens(refreshToken, { - clientId: OIDC_CLIENT_ID, - scopes: OIDC_SCOPES, - }, issuerConfiguration.tokenEndpoint); + const refreshedTokenData = await fastify.refreshAuthTokens( + refreshToken, + { + clientId: OIDC_CLIENT_ID, + scopes: OIDC_SCOPES, + }, + issuerConfiguration.tokenEndpoint, + ); if (!refreshedTokenData || !refreshedTokenData.accessToken) { - request.log.error("Token refresh failed (no access token); deleting session."); - request.encryptedSession.clear();//TODO: also clear user encrpytion key? - return reply.unauthorized("Session expired and token refresh failed."); + request.log.error('Token refresh failed (no access token); deleting session.'); + 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.log.info('Token refresh successful; updating the session.'); request.encryptedSession.set(keyAccessToken, refreshedTokenData.accessToken); if (refreshedTokenData.refreshToken) { @@ -67,17 +74,17 @@ function proxyPlugin(fastify) { request.encryptedSession.delete(keyRefreshToken); } if (refreshedTokenData.expiresIn) { - const newExpiresAt = Date.now() + (refreshedTokenData.expiresIn * 1000); + const newExpiresAt = Date.now() + refreshedTokenData.expiresIn * 1000; request.encryptedSession.set(keyTokenExpiresAt, newExpiresAt); } else { request.encryptedSession.delete(keyTokenExpiresAt); } - request.log.info("Token refresh successful and session updated; continuing with the HTTP request."); + request.log.info('Token refresh successful and session updated; continuing with the HTTP request.'); } catch (error) { if (error instanceof AuthenticationError) { - request.log.error("AuthenticationError during token refresh: %s", error); - return reply.unauthorized("Error during token refresh."); + request.log.error('AuthenticationError during token refresh: %s', error); + return reply.unauthorized('Error during token refresh.'); } else { throw error; } @@ -85,13 +92,15 @@ function proxyPlugin(fastify) { }, replyOptions: { rewriteRequestHeaders: (req, headers) => { - const useCrate = req.headers["x-use-crate"]; - const accessToken = useCrate ? req.encryptedSession.get("onboarding_accessToken") : `${req.encryptedSession.get("onboarding_accessToken")},${req.encryptedSession.get("mcp_accessToken")}`; + const useCrate = req.headers['x-use-crate']; + const accessToken = useCrate + ? req.encryptedSession.get('onboarding_accessToken') + : `${req.encryptedSession.get('onboarding_accessToken')},${req.encryptedSession.get('mcp_accessToken')}`; return { ...headers, authorization: accessToken, - } + }; }, }, }); diff --git a/server/plugins/sensible.js b/server/plugins/sensible.js index 9f9a1e63..cac0adbf 100644 --- a/server/plugins/sensible.js +++ b/server/plugins/sensible.js @@ -1,5 +1,5 @@ -import fp from "fastify-plugin"; -import sensible from "@fastify/sensible"; +import fp from 'fastify-plugin'; +import sensible from '@fastify/sensible'; function sensiblePlugin(fastify) { fastify.register(sensible, { errorHandler: false }); diff --git a/server/routes/auth-mcp.js b/server/routes/auth-mcp.js index 67809cc3..245000c8 100644 --- a/server/routes/auth-mcp.js +++ b/server/routes/auth-mcp.js @@ -1,6 +1,5 @@ -import fp from "fastify-plugin"; -import { AuthenticationError } from "../plugins/auth-utils.js"; - +import fp from 'fastify-plugin'; +import { AuthenticationError } from '../plugins/auth-utils.js'; async function authPlugin(fastify) { const { OIDC_ISSUER, OIDC_CLIENT_ID_MCP, OIDC_REDIRECT_URI, OIDC_SCOPES, POST_LOGIN_REDIRECT } = fastify.config; @@ -8,50 +7,64 @@ async function authPlugin(fastify) { // Make MCP issuer configuration globally available // TODO: This is a temporary solution until we have a proper way to manage multiple issuers const mcpIssuerConfiguration = await fastify.discoverIssuerConfiguration(OIDC_ISSUER); - fastify.decorate("mcpIssuerConfiguration", mcpIssuerConfiguration); + fastify.decorate('mcpIssuerConfiguration', mcpIssuerConfiguration); - fastify.get("/auth/mcp/login", async (req, reply) => { - const redirectUri = fastify.prepareOidcLoginRedirect(req, { - clientId: OIDC_CLIENT_ID_MCP, - redirectUri: OIDC_REDIRECT_URI, - scopes: OIDC_SCOPES, - }, mcpIssuerConfiguration.authorizationEndpoint); + fastify.get('/auth/mcp/login', async (req, reply) => { + const redirectUri = fastify.prepareOidcLoginRedirect( + req, + { + clientId: OIDC_CLIENT_ID_MCP, + redirectUri: OIDC_REDIRECT_URI, + scopes: OIDC_SCOPES, + }, + mcpIssuerConfiguration.authorizationEndpoint, + ); reply.redirect(redirectUri); + + return reply; }); - fastify.get("/auth/mcp/callback", async (req, reply) => { + fastify.get('/auth/mcp/callback', async (req, reply) => { try { - const callbackResult = await fastify.handleOidcCallback(req, { - clientId: OIDC_CLIENT_ID_MCP, - redirectUri: OIDC_REDIRECT_URI, - }, mcpIssuerConfiguration.tokenEndpoint); + const callbackResult = await fastify.handleOidcCallback( + req, + { + clientId: OIDC_CLIENT_ID_MCP, + redirectUri: OIDC_REDIRECT_URI, + }, + mcpIssuerConfiguration.tokenEndpoint, + ); - req.encryptedSession.set("mcp_accessToken", callbackResult.accessToken); - req.encryptedSession.set("mcp_refreshToken", callbackResult.refreshToken); + req.encryptedSession.set('mcp_accessToken', callbackResult.accessToken); + req.encryptedSession.set('mcp_refreshToken', callbackResult.refreshToken); if (callbackResult.expiresAt) { - req.encryptedSession.set("mcp_tokenExpiresAt", callbackResult.expiresAt); + req.encryptedSession.set('mcp_tokenExpiresAt', callbackResult.expiresAt); } else { - req.encryptedSession.delete("mcp_tokenExpiresAt"); + req.encryptedSession.delete('mcp_tokenExpiresAt'); } reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); } catch (error) { if (error instanceof AuthenticationError) { - req.log.error("AuthenticationError during OIDC callback: %s", error); - return reply.serviceUnavailable("Error during OIDC callback."); + req.log.error('AuthenticationError during OIDC callback: %s', error); + return reply.serviceUnavailable('Error during OIDC callback.'); } else { throw error; } } + + return reply; }); - fastify.get("/auth/mcp/me", async (req, reply) => { - const accessToken = req.encryptedSession.get("mcp_accessToken"); + fastify.get('/auth/mcp/me', async (req, reply) => { + const accessToken = req.encryptedSession.get('mcp_accessToken'); const isAuthenticated = Boolean(accessToken); reply.send({ isAuthenticated }); + + return reply; }); } diff --git a/server/routes/auth-onboarding.js b/server/routes/auth-onboarding.js index 378574ae..3b349f80 100644 --- a/server/routes/auth-onboarding.js +++ b/server/routes/auth-onboarding.js @@ -1,69 +1,78 @@ -import fp from "fastify-plugin"; -import { AuthenticationError } from "../plugins/auth-utils.js"; - +import fp from 'fastify-plugin'; +import { AuthenticationError } from '../plugins/auth-utils.js'; async function authPlugin(fastify) { const { OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_REDIRECT_URI, OIDC_SCOPES, POST_LOGIN_REDIRECT } = fastify.config; - // Make onboarding issuer configuration globally available const issuerConfiguration = await fastify.discoverIssuerConfiguration(OIDC_ISSUER); - fastify.decorate("issuerConfiguration", issuerConfiguration); + fastify.decorate('issuerConfiguration', issuerConfiguration); - - fastify.get("/auth/onboarding/login", async (req, reply) => { - const redirectUri = fastify.prepareOidcLoginRedirect(req, { - clientId: OIDC_CLIENT_ID, - redirectUri: OIDC_REDIRECT_URI, - scopes: OIDC_SCOPES, - }, issuerConfiguration.authorizationEndpoint); + fastify.get('/auth/onboarding/login', async (req, reply) => { + const redirectUri = fastify.prepareOidcLoginRedirect( + req, + { + clientId: OIDC_CLIENT_ID, + redirectUri: OIDC_REDIRECT_URI, + scopes: OIDC_SCOPES, + }, + issuerConfiguration.authorizationEndpoint, + ); reply.redirect(redirectUri); }); - - fastify.get("/auth/onboarding/callback", async (req, reply) => { + fastify.get('/auth/onboarding/callback', async (req, reply) => { try { - const callbackResult = await fastify.handleOidcCallback(req, { - clientId: OIDC_CLIENT_ID, - redirectUri: OIDC_REDIRECT_URI, - }, issuerConfiguration.tokenEndpoint); - - req.encryptedSession.set("onboarding_accessToken", callbackResult.accessToken); - req.encryptedSession.set("onboarding_refreshToken", callbackResult.refreshToken); - req.encryptedSession.set("onboarding_userInfo", callbackResult.userInfo); + const callbackResult = await fastify.handleOidcCallback( + req, + { + clientId: OIDC_CLIENT_ID, + redirectUri: OIDC_REDIRECT_URI, + }, + issuerConfiguration.tokenEndpoint, + ); + + 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.encryptedSession.set("onboarding_tokenExpiresAt", callbackResult.expiresAt); + req.encryptedSession.set('onboarding_tokenExpiresAt', callbackResult.expiresAt); } else { - req.encryptedSession.delete("onboarding_tokenExpiresAt"); + req.encryptedSession.delete('onboarding_tokenExpiresAt'); } reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); } catch (error) { if (error instanceof AuthenticationError) { - req.log.error("AuthenticationError during OIDC callback: %s", error); - return reply.serviceUnavailable("Error during OIDC callback."); + req.log.error('AuthenticationError during OIDC callback: %s', error); + return reply.serviceUnavailable('Error during OIDC callback.'); } else { throw error; } } - }); + return reply; + }); - fastify.get("/auth/onboarding/me", async (req, reply) => { - const accessToken = req.encryptedSession.get("onboarding_accessToken"); - const userInfo = req.encryptedSession.get("onboarding_userInfo"); + fastify.get('/auth/onboarding/me', async (req, reply) => { + const accessToken = req.encryptedSession.get('onboarding_accessToken'); + const userInfo = req.encryptedSession.get('onboarding_userInfo'); const isAuthenticated = Boolean(accessToken); const user = isAuthenticated ? userInfo : null; reply.send({ isAuthenticated, user }); + + return reply; }); - fastify.post("/auth/logout", async (req, reply) => { + fastify.post('/auth/logout', async (req, reply) => { // TODO: Idp sign out flow req.encryptedSession.clear(); - reply.send({ message: "Logged out" }); + reply.send({ message: 'Logged out' }); + + return reply; }); } diff --git a/server/routes/feedback.js b/server/routes/feedback.js index 005a5c4b..6bf78f4d 100644 --- a/server/routes/feedback.js +++ b/server/routes/feedback.js @@ -1,5 +1,5 @@ import fetch from 'node-fetch'; -import fp from "fastify-plugin"; +import fp from 'fastify-plugin'; async function feedbackRoute(fastify) { const { FEEDBACK_SLACK_URL } = fastify.config; @@ -23,7 +23,7 @@ async function feedbackRoute(fastify) { if (!res.ok) { return reply.status(500).send({ error: 'Slack API error' }); } - return reply.send({ message: res, }); + return reply.send({ message: res }); } catch (err) { fastify.log.error('Slack error:', err); return reply.status(500).send({ error: 'Request failed' }); @@ -32,4 +32,3 @@ async function feedbackRoute(fastify) { } export default fp(feedbackRoute); - diff --git a/server/routes/root.js b/server/routes/root.js index d064521c..5ad7e594 100644 --- a/server/routes/root.js +++ b/server/routes/root.js @@ -1,8 +1,8 @@ -import fp from "fastify-plugin"; +import fp from 'fastify-plugin'; function rootRoutes(fastify) { - fastify.get("/", async (_request, reply) => { - reply.code(200).send({ status: "ok", timestamp: new Date().toISOString() }); + fastify.get('/', async (_request, reply) => { + return reply.code(200).send({ status: 'ok', timestamp: new Date().toISOString() }); }); } From 701cab81f92b54c936cf420295907f6e1d5a2de9 Mon Sep 17 00:00:00 2001 From: Moritz Reich Date: Wed, 9 Jul 2025 14:43:50 +0200 Subject: [PATCH 2/4] fix: Simplify response handling in the /auth/mcp/me route --- server/routes/auth-mcp.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/routes/auth-mcp.js b/server/routes/auth-mcp.js index 245000c8..53c1fada 100644 --- a/server/routes/auth-mcp.js +++ b/server/routes/auth-mcp.js @@ -62,9 +62,7 @@ async function authPlugin(fastify) { const accessToken = req.encryptedSession.get('mcp_accessToken'); const isAuthenticated = Boolean(accessToken); - reply.send({ isAuthenticated }); - - return reply; + return reply.send({ isAuthenticated }); }); } From f63e800a3e3447c7855d6e7894f8c217ca041516 Mon Sep 17 00:00:00 2001 From: Moritz Reich Date: Wed, 9 Jul 2025 14:45:42 +0200 Subject: [PATCH 3/4] fix: Simplify reply handling in auth routes for consistent return statements --- server/routes/auth-mcp.js | 8 ++------ server/routes/auth-onboarding.js | 12 +++--------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/server/routes/auth-mcp.js b/server/routes/auth-mcp.js index 53c1fada..b646fd07 100644 --- a/server/routes/auth-mcp.js +++ b/server/routes/auth-mcp.js @@ -20,9 +20,7 @@ async function authPlugin(fastify) { mcpIssuerConfiguration.authorizationEndpoint, ); - reply.redirect(redirectUri); - - return reply; + return reply.redirect(redirectUri); }); fastify.get('/auth/mcp/callback', async (req, reply) => { @@ -45,7 +43,7 @@ async function authPlugin(fastify) { req.encryptedSession.delete('mcp_tokenExpiresAt'); } - reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); + return reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); } catch (error) { if (error instanceof AuthenticationError) { req.log.error('AuthenticationError during OIDC callback: %s', error); @@ -54,8 +52,6 @@ async function authPlugin(fastify) { throw error; } } - - return reply; }); fastify.get('/auth/mcp/me', async (req, reply) => { diff --git a/server/routes/auth-onboarding.js b/server/routes/auth-onboarding.js index 3b349f80..865370f9 100644 --- a/server/routes/auth-onboarding.js +++ b/server/routes/auth-onboarding.js @@ -43,7 +43,7 @@ async function authPlugin(fastify) { req.encryptedSession.delete('onboarding_tokenExpiresAt'); } - reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); + return reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); } catch (error) { if (error instanceof AuthenticationError) { req.log.error('AuthenticationError during OIDC callback: %s', error); @@ -52,8 +52,6 @@ async function authPlugin(fastify) { throw error; } } - - return reply; }); fastify.get('/auth/onboarding/me', async (req, reply) => { @@ -62,17 +60,13 @@ async function authPlugin(fastify) { const isAuthenticated = Boolean(accessToken); const user = isAuthenticated ? userInfo : null; - reply.send({ isAuthenticated, user }); - - return reply; + return reply.send({ isAuthenticated, user }); }); fastify.post('/auth/logout', async (req, reply) => { // TODO: Idp sign out flow req.encryptedSession.clear(); - reply.send({ message: 'Logged out' }); - - return reply; + return reply.send({ message: 'Logged out' }); }); } From e523bee01ad3b9e7419e8a730324430f2958b8bc Mon Sep 17 00:00:00 2001 From: Andreas Kienle Date: Wed, 9 Jul 2025 14:53:49 +0200 Subject: [PATCH 4/4] Update server/routes/auth-onboarding.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- server/routes/auth-onboarding.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/auth-onboarding.js b/server/routes/auth-onboarding.js index 865370f9..ef2afd5f 100644 --- a/server/routes/auth-onboarding.js +++ b/server/routes/auth-onboarding.js @@ -19,7 +19,7 @@ async function authPlugin(fastify) { issuerConfiguration.authorizationEndpoint, ); - reply.redirect(redirectUri); + return reply.redirect(redirectUri); }); fastify.get('/auth/onboarding/callback', async (req, reply) => {