128 lines
3.6 KiB
JavaScript
128 lines
3.6 KiB
JavaScript
/**
|
|
* Redis-backed session store.
|
|
* Sessions are created after Keycloak authentication. We generate our own session tokens
|
|
* and use Redis as the source of truth. Keycloak tokens are stored for refresh.
|
|
*/
|
|
import crypto from 'crypto';
|
|
import log4js from 'log4js';
|
|
import config from '../../config.js';
|
|
import { redisServer } from '../../database/redis.js';
|
|
|
|
const logger = log4js.getLogger('SessionStore');
|
|
logger.level = config.server.logLevel;
|
|
|
|
const SESSION_KEY_PREFIX = 'session:';
|
|
|
|
/**
|
|
* Generate a cryptographically secure session token
|
|
*/
|
|
function generateSessionToken() {
|
|
return crypto.randomBytes(32).toString('hex');
|
|
}
|
|
|
|
/**
|
|
* Get TTL in seconds from expiresAt timestamp
|
|
*/
|
|
function getTtlSeconds(expiresAt) {
|
|
const now = Date.now();
|
|
const ttlMs = expiresAt - now;
|
|
return Math.max(Math.ceil(ttlMs / 1000), 60); // minimum 60 seconds
|
|
}
|
|
|
|
/**
|
|
* Create a new session in Redis after Keycloak authentication
|
|
* @param {Object} params
|
|
* @param {Object} params.user - User object (from createOrUpdateUser)
|
|
* @param {Object} params.keycloakTokens - { access_token, refresh_token, id_token, expires_at }
|
|
* @returns {{ sessionToken: string, expiresAt: number }}
|
|
*/
|
|
export async function createSession({ user, keycloakTokens }) {
|
|
const sessionToken = generateSessionToken();
|
|
const expiresAt = keycloakTokens.expires_at;
|
|
|
|
const sessionData = {
|
|
sessionToken,
|
|
user: userToSessionUser(user),
|
|
keycloakTokens: {
|
|
access_token: keycloakTokens.access_token,
|
|
refresh_token: keycloakTokens.refresh_token,
|
|
id_token: keycloakTokens.id_token,
|
|
expires_at: keycloakTokens.expires_at,
|
|
},
|
|
expiresAt,
|
|
};
|
|
|
|
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
const ttlSeconds = getTtlSeconds(expiresAt);
|
|
|
|
await redisServer.setKey(key, sessionData, ttlSeconds);
|
|
logger.debug(`Created session for user ${user.username}, expires in ${ttlSeconds}s`);
|
|
|
|
return { sessionToken, expiresAt };
|
|
}
|
|
|
|
/**
|
|
* Get session by token. Returns null if not found or expired.
|
|
*/
|
|
export async function getSession(sessionToken) {
|
|
if (!sessionToken) return null;
|
|
|
|
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
const session = await redisServer.getKey(key);
|
|
|
|
if (!session) return null;
|
|
if (session.expiresAt && session.expiresAt <= Date.now()) {
|
|
await redisServer.deleteKey(key);
|
|
return null;
|
|
}
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Update session with new Keycloak tokens (after refresh)
|
|
*/
|
|
export async function updateSessionKeycloakTokens(sessionToken, keycloakTokens) {
|
|
const session = await getSession(sessionToken);
|
|
if (!session) return null;
|
|
|
|
const updatedSession = {
|
|
...session,
|
|
keycloakTokens: {
|
|
access_token: keycloakTokens.access_token,
|
|
refresh_token: keycloakTokens.refresh_token,
|
|
id_token: keycloakTokens.id_token || session.keycloakTokens?.id_token,
|
|
expires_at: keycloakTokens.expires_at,
|
|
},
|
|
expiresAt: keycloakTokens.expires_at,
|
|
};
|
|
|
|
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
const ttlSeconds = getTtlSeconds(keycloakTokens.expires_at);
|
|
await redisServer.setKey(key, updatedSession, ttlSeconds);
|
|
|
|
return updatedSession;
|
|
}
|
|
|
|
/**
|
|
* Delete a session (logout)
|
|
*/
|
|
export async function deleteSession(sessionToken) {
|
|
if (!sessionToken) return;
|
|
const key = SESSION_KEY_PREFIX + sessionToken;
|
|
await redisServer.deleteKey(key);
|
|
logger.debug(`Deleted session for token ${sessionToken.substring(0, 12)}...`);
|
|
}
|
|
|
|
/**
|
|
* Normalize user object for session storage (ensure _id is serializable for Redis)
|
|
*/
|
|
function userToSessionUser(user) {
|
|
if (!user) return null;
|
|
const u = { ...user };
|
|
if (u._id && typeof u._id === 'object' && u._id.toString) {
|
|
u._id = u._id;
|
|
}
|
|
return JSON.parse(JSON.stringify(u));
|
|
}
|