Skip to content

Commit

Permalink
feat: Add X-Session-ID
Browse files Browse the repository at this point in the history
  • Loading branch information
letehaha committed Dec 25, 2024
1 parent be94d1a commit 33723df
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ APPLICATION_DB_DIALECT=postgres
# for .env.test use docker/test/docker-compose redis service name
APPLICATION_REDIS_HOST=127.0.0.1

APP_SESSION_ID_SECRET=

# Tests configurations
# e2e tests are running in parallel, so we need a strict amount of workers,
# so then we can dynamically create the same amount of DBs
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/check-source-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
envkey_APPLICATION_DB_PORT: ${{ secrets.APPLICATION_DB_PORT }}
envkey_APPLICATION_DB_DIALECT: ${{ secrets.APPLICATION_DB_DIALECT }}
envkey_APPLICATION_REDIS_HOST: ${{ secrets.APPLICATION_REDIS_HOST }}
envkey_APP_SESSION_ID_SECRET: ${{ secrets.APP_SESSION_ID_SECRET }}
envkey_JEST_WORKERS_AMOUNT: ${{ secrets.JEST_WORKERS_AMOUNT }}
envkey_SHOW_LOGS_IN_TESTS: ${{ secrets.SHOW_LOGS_IN_TESTS }}
envkey_API_LAYER_API_KEY: ${{ secrets.API_LAYER_API_KEY }}
Expand Down Expand Up @@ -80,6 +81,7 @@ jobs:
envkey_APPLICATION_DB_PORT: ${{ secrets.APPLICATION_DB_PORT }}
envkey_APPLICATION_DB_DIALECT: ${{ secrets.APPLICATION_DB_DIALECT }}
envkey_APPLICATION_REDIS_HOST: ${{ secrets.APPLICATION_REDIS_HOST }}
envkey_APP_SESSION_ID_SECRET: ${{ secrets.APP_SESSION_ID_SECRET }}
envkey_JEST_WORKERS_AMOUNT: ${{ secrets.JEST_WORKERS_AMOUNT }}
envkey_SHOW_LOGS_IN_TESTS: ${{ secrets.SHOW_LOGS_IN_TESTS }}
envkey_API_LAYER_API_KEY: ${{ secrets.API_LAYER_API_KEY }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/image-to-docker-hub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
envkey_APPLICATION_DB_PORT: ${{ secrets.APPLICATION_DB_PORT }}
envkey_APPLICATION_DB_DIALECT: ${{ secrets.APPLICATION_DB_DIALECT }}
envkey_APPLICATION_REDIS_HOST: ${{ secrets.APPLICATION_REDIS_HOST }}
envkey_APP_SESSION_ID_SECRET: ${{ secrets.APP_SESSION_ID_SECRET }}
envkey_JEST_WORKERS_AMOUNT: ${{ secrets.JEST_WORKERS_AMOUNT }}
envkey_SHOW_LOGS_IN_TESTS: ${{ secrets.SHOW_LOGS_IN_TESTS }}
envkey_API_LAYER_API_KEY: ${{ secrets.API_LAYER_API_KEY }}
Expand Down
3 changes: 3 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { supportedLocales } from './translations';

import middlewarePassword from './middlewares/passport';
import { requestIdMiddleware } from '@middlewares/request-id';
import { sessionMiddleware } from '@middlewares/session-id';

import { loadCurrencyRatesJob } from './crons/exchange-rates';

Expand Down Expand Up @@ -61,6 +62,7 @@ app.use(

return callback(null, true);
},
exposedHeaders: ['x-session-id', 'x-request-id'],
}),
);
app.use(express.json());
Expand All @@ -69,6 +71,7 @@ if (process.env.NODE_ENV !== 'test') {
app.use(morgan('dev'));
}
app.use(locale(supportedLocales));
app.use(sessionMiddleware);

/**
* Routes include
Expand Down
31 changes: 31 additions & 0 deletions src/common/lib/cls/session-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import cls from 'cls-hooked';
import { Request } from 'express';
import { SESSION_ID_KEY_NAME } from '@common/types';

const NAMESPACE_NAME = 'session-id-namespace';

export const sessionIdNamespace = cls.createNamespace(NAMESPACE_NAME);

export const getNamespace = () => cls.getNamespace(NAMESPACE_NAME);

export const setNamespace = (key: string, value: Request) => {
const namespace = getNamespace();
if (namespace) {
namespace.set(key, value);
}
};

const getFromNamespace = <T>(key: 'req' | typeof SESSION_ID_KEY_NAME): T | null => {
const namespace = getNamespace();
return namespace ? namespace.get(key) : null;
};

// Helper function for getting the current request from CLS
export const getCurrentRequest = (): Request | null => {
return getFromNamespace<Request>('req');
};

// Helper function for getting the current requestId from CLS
export const getCurrentSessionId = (): string | null => {
return getFromNamespace<string>(SESSION_ID_KEY_NAME);
};
2 changes: 2 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export interface CustomResponse extends Express.Response {

export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
export type UnwrapArray<T> = T extends (infer U)[] ? U : T;

export const SESSION_ID_KEY_NAME = 'sessionId';
2 changes: 2 additions & 0 deletions src/js/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import winston, { format, transports } from 'winston';
import LokiTransport from 'winston-loki';
import { getCurrentRequestId } from '@common/lib/cls/logging';
import { getCurrentSessionId } from '@common/lib/cls/session-id';

const transportsArray: winston.transport[] = [new transports.Console()];
const { GRAFANA_LOKI_HOST, GRAFANA_LOKI_AUTH, GRAFANA_LOKI_USER_ID } = process.env;
Expand Down Expand Up @@ -54,6 +55,7 @@ const createLogger =
level: severity,
message,
requestId: getCurrentRequestId() ?? null,
sessionId: getCurrentSessionId() ?? null,
...meta,
});
}
Expand Down
77 changes: 77 additions & 0 deletions src/middlewares/session-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { createHmac } from 'crypto';
import { v4 as uuidv4 } from 'uuid';
import { NextFunction, Request, Response } from 'express';
import { logger } from '@js/utils';
import { SESSION_ID_KEY_NAME } from '@common/types';
import { sessionIdNamespace } from '@common/lib/cls/session-id';

const SECRET_KEY = process.env.APP_SESSION_ID_SECRET as string;

export const sessionMiddleware = async (req: Request, res: Response, next: NextFunction) => {
if (!SECRET_KEY) {
logger.error('"sessionMiddleware": secrey key is missing!');
return next();
}

try {
let sessionId: string | null = null;

const token = req.headers['x-session-id'];

// Take session id from the request's headers
if (token && typeof token === 'string') {
const decoded = verifySignedSessionId(token);

// if session id is valid, use it
// WARNING: might want to throw 403 error in the future, if session_id is used
// not only for logs
if (decoded) {
sessionId = token;
}
}

// if session id previously wasn't validated, generate a new one
if (!sessionId) {
sessionId = generateSignedSessionId();
}

if (sessionId) {
res.setHeader('X-Session-ID', sessionId);

sessionIdNamespace.run(() => {
sessionIdNamespace.set(SESSION_ID_KEY_NAME, sessionId);
sessionIdNamespace.set('req', req);

req[SESSION_ID_KEY_NAME] = sessionId;
next();
});
} else {
logger.warn('"sessionMiddleware": wasnt able to set sessionId for unexpected reasons.');
next();
}
} catch (err) {
logger.error('"sessionMiddleware" failed due to unexpected error');
next();
}
};

// Generate a signed sessionId
const generateSignedSessionId = () => {
const sessionId = uuidv4();
const signature = createHmac('sha256', SECRET_KEY).update(sessionId).digest('hex');
return `${sessionId}.${signature}`;
};

// Verify a signed sessionId
const verifySignedSessionId = (signedSessionId: string) => {
const [sessionId, signature] = signedSessionId.split('.');
if (!sessionId || !signature) {
return false;
}

const expectedSignature = createHmac('sha256', SECRET_KEY).update(sessionId).digest('hex');

if (!expectedSignature) logger.warn('Session ID validation failed.');

return signature === expectedSignature;
};
2 changes: 2 additions & 0 deletions src/typings/express.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { endpointsTypes } from 'shared-types';
import type { SESSION_ID_KEY_NAME } from '@common/types';

declare module 'express' {
interface Request {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body: endpointsTypes.BodyPayload<any>;
requestId?: string; // Optional requestId property
[SESSION_ID_KEY_NAME]?: string | null; // Optional sessionId property
}
}

0 comments on commit 33723df

Please sign in to comment.