Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 52 additions & 76 deletions server/encrypted-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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() {
Expand Down
8 changes: 4 additions & 4 deletions server/plugins/auth-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand All @@ -88,16 +88,16 @@ 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;

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(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),
Expand Down
14 changes: 7 additions & 7 deletions server/plugins/http-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}

Expand All @@ -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.');
Expand Down
12 changes: 6 additions & 6 deletions server/routes/auth-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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 });
});
}

Expand Down
19 changes: 9 additions & 10 deletions server/routes/auth-onboarding.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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' });
});
}

Expand Down
Loading