diff --git a/server/encrypted-session.js b/server/encrypted-session.js index 8013b48b..677883fc 100644 --- a/server/encrypted-session.js +++ b/server/encrypted-session.js @@ -49,63 +49,47 @@ 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'); - - let newEncryptionKey = generateSecureEncryptionKey(); - setUserEncryptionKeyIntoUserCookie(request, newEncryptionKey); - 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(userEncryptionKey, '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(); - } + await fastify.decorateRequest(REQUEST_DECORATOR, { + getter() { + return createStore(this); } - - 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'); - 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'); - } +export default fp(encryptedSession); - //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); +function createStore(request) { + let unencryptedStore = {}; // Private variable + + //read previous values + let userEncryptionKey = getUserEncryptionKeyFromUserCookie(request); + if (!userEncryptionKey) { + request.log.info({ plugin: 'encrypted-session' }, 'user-side encryption key not found, creating new one'); - //remove unencrypted data from memory - delete request[REQUEST_DECORATOR]; - request[REQUEST_DECORATOR] = null; + userEncryptionKey = generateSecureEncryptionKey(); + setUserEncryptionKeyIntoUserCookie(request, userEncryptionKey); + } + + const loadedEncryptionKey = Buffer.from(userEncryptionKey, '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); + unencryptedStore = decryptedStore; + } catch (error) { + request.log.error({ plugin: 'encrypted-session' }, 'Failed to parse encrypted session store', error); + } + } 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'); + } + + async function save() { + const stringifiedData = JSON.stringify(unencryptedStore); + const { cipherText, iv, tag } = encryptSymetric(stringifiedData, loadedEncryptionKey); request.session.set('encryptedStore', { cipherText, @@ -114,46 +98,38 @@ async function encryptedSession(fastify) { }); 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); - } - - function setUserEncryptionKeyIntoUserCookie(request, key) { - request[ENCRYPTED_COOKIE_REQUEST_DECORATOR].set(ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY, key.toString('base64')); } -} - -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) { + async set(key, value) { unencryptedStore[key] = value; + await save() }, get(key) { return unencryptedStore[key]; }, - delete(key) { + async delete(key) { delete unencryptedStore[key]; + await save() }, - stringify() { - return JSON.stringify(unencryptedStore); + print() { + console.log("printing", unencryptedStore) }, - clear() { + async clear() { unencryptedStore = {}; // Clear all data + await save() }, }; } +function getUserEncryptionKeyFromUserCookie(request) { + return request[ENCRYPTED_COOKIE_REQUEST_DECORATOR].get(ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY); +} + +function setUserEncryptionKeyIntoUserCookie(request, key) { + request[ENCRYPTED_COOKIE_REQUEST_DECORATOR].set(ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY, key.toString('base64')); +} + // generates a secure encryption key for aes-256-gcm. // Returns a buffer of 32 bytes (256 bits). function generateSecureEncryptionKey() { diff --git a/server/plugins/auth-utils.js b/server/plugins/auth-utils.js index 5a1e0cc9..26e02b37 100644 --- a/server/plugins/auth-utils.js +++ b/server/plugins/auth-utils.js @@ -77,7 +77,7 @@ async function authUtilsPlugin(fastify) { }; }); - fastify.decorate('prepareOidcLoginRedirect', (request, oidcConfig, authorizationEndpoint, stateKey) => { + fastify.decorate('prepareOidcLoginRedirect', async (request, oidcConfig, authorizationEndpoint, stateKey) => { if (stateKey === undefined) { stateKey = 'oauthState'; } @@ -88,7 +88,7 @@ async function authUtilsPlugin(fastify) { request.log.error(`Invalid redirectTo: "${redirectTo}".`); throw new AuthenticationError('Invalid redirectTo.'); } - request.encryptedSession.set('postLoginRedirectRoute', redirectTo); + await request.encryptedSession.set('postLoginRedirectRoute', redirectTo); const { clientId, redirectUri, scopes } = oidcConfig; @@ -96,8 +96,8 @@ async function authUtilsPlugin(fastify) { const codeVerifier = crypto.randomBytes(32).toString('base64url'); const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url'); - request.encryptedSession.set(stateKey, state); - request.encryptedSession.set('codeVerifier', codeVerifier); + await request.encryptedSession.set(stateKey, state); + await request.encryptedSession.set('codeVerifier', codeVerifier); request.log.info( { stateSet: Boolean(state), diff --git a/server/plugins/http-proxy.js b/server/plugins/http-proxy.js index 64d759cf..eba36cbc 100644 --- a/server/plugins/http-proxy.js +++ b/server/plugins/http-proxy.js @@ -43,7 +43,7 @@ function proxyPlugin(fastify) { 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? + await request.encryptedSession.clear(); //TODO: also clear user encrpytion key? return reply.unauthorized('Session expired without token refresh capability.'); } @@ -61,23 +61,23 @@ function proxyPlugin(fastify) { ); if (!refreshedTokenData || !refreshedTokenData.accessToken) { request.log.error('Token refresh failed (no access token); deleting session.'); - request.encryptedSession.clear(); //TODO: also clear user encrpytion key? + await 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.encryptedSession.set(keyAccessToken, refreshedTokenData.accessToken); + await request.encryptedSession.set(keyAccessToken, refreshedTokenData.accessToken); if (refreshedTokenData.refreshToken) { - request.encryptedSession.set(keyRefreshToken, refreshedTokenData.refreshToken); + await request.encryptedSession.set(keyRefreshToken, refreshedTokenData.refreshToken); } else { - request.encryptedSession.delete(keyRefreshToken); + await request.encryptedSession.delete(keyRefreshToken); } if (refreshedTokenData.expiresIn) { const newExpiresAt = Date.now() + refreshedTokenData.expiresIn * 1000; - request.encryptedSession.set(keyTokenExpiresAt, newExpiresAt); + await request.encryptedSession.set(keyTokenExpiresAt, newExpiresAt); } else { - request.encryptedSession.delete(keyTokenExpiresAt); + await request.encryptedSession.delete(keyTokenExpiresAt); } request.log.info('Token refresh successful and session updated; continuing with the HTTP request.'); diff --git a/server/routes/auth-mcp.js b/server/routes/auth-mcp.js index 06c05156..0b0fe699 100644 --- a/server/routes/auth-mcp.js +++ b/server/routes/auth-mcp.js @@ -12,7 +12,7 @@ async function authPlugin(fastify) { fastify.decorate('mcpIssuerConfiguration', mcpIssuerConfiguration); fastify.get('/auth/mcp/login', async function (req, reply) { - const redirectUri = fastify.prepareOidcLoginRedirect( + const redirectUri = await fastify.prepareOidcLoginRedirect( req, { clientId: OIDC_CLIENT_ID_MCP, @@ -38,13 +38,13 @@ async function authPlugin(fastify) { stateSessionKey, ); - req.encryptedSession.set('mcp_accessToken', callbackResult.accessToken); - req.encryptedSession.set('mcp_refreshToken', callbackResult.refreshToken); + await req.encryptedSession.set('mcp_accessToken', callbackResult.accessToken); + await req.encryptedSession.set('mcp_refreshToken', callbackResult.refreshToken); if (callbackResult.expiresAt) { - req.encryptedSession.set('mcp_tokenExpiresAt', callbackResult.expiresAt); + await req.encryptedSession.set('mcp_tokenExpiresAt', callbackResult.expiresAt); } else { - req.encryptedSession.delete('mcp_tokenExpiresAt'); + await req.encryptedSession.delete('mcp_tokenExpiresAt'); } return reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); @@ -62,7 +62,7 @@ async function authPlugin(fastify) { const accessToken = req.encryptedSession.get('mcp_accessToken'); const isAuthenticated = Boolean(accessToken); - reply.send({ isAuthenticated }); + return reply.send({ isAuthenticated }); }); } diff --git a/server/routes/auth-onboarding.js b/server/routes/auth-onboarding.js index 08e516d2..1fb9c94d 100644 --- a/server/routes/auth-onboarding.js +++ b/server/routes/auth-onboarding.js @@ -11,7 +11,7 @@ async function authPlugin(fastify) { fastify.decorate('issuerConfiguration', issuerConfiguration); fastify.get('/auth/onboarding/login', async function (req, reply) { - const redirectUri = fastify.prepareOidcLoginRedirect( + const redirectUri = await fastify.prepareOidcLoginRedirect( req, { clientId: OIDC_CLIENT_ID, @@ -37,14 +37,14 @@ async function authPlugin(fastify) { stateSessionKey, ); - req.encryptedSession.set('onboarding_accessToken', callbackResult.accessToken); - req.encryptedSession.set('onboarding_refreshToken', callbackResult.refreshToken); - req.encryptedSession.set('onboarding_userInfo', callbackResult.userInfo); + await req.encryptedSession.set('onboarding_accessToken', callbackResult.accessToken); + await req.encryptedSession.set('onboarding_refreshToken', callbackResult.refreshToken); + await req.encryptedSession.set('onboarding_userInfo', callbackResult.userInfo); if (callbackResult.expiresAt) { - req.encryptedSession.set('onboarding_tokenExpiresAt', callbackResult.expiresAt); + await req.encryptedSession.set('onboarding_tokenExpiresAt', callbackResult.expiresAt); } else { - req.encryptedSession.delete('onboarding_tokenExpiresAt'); + await req.encryptedSession.delete('onboarding_tokenExpiresAt'); } return reply.redirect(POST_LOGIN_REDIRECT + callbackResult.postLoginRedirectRoute); @@ -64,14 +64,13 @@ async function authPlugin(fastify) { const isAuthenticated = Boolean(accessToken); const user = isAuthenticated ? userInfo : null; - - reply.send({ isAuthenticated, user }); + return reply.send({ isAuthenticated, user }); }); fastify.post('/auth/logout', async function (req, reply) { // TODO: Idp sign out flow - req.encryptedSession.clear(); - reply.send({ message: 'Logged out' }); + await req.encryptedSession.clear(); + return reply.send({ message: 'Logged out' }); }); }