UNPKG

@thermopylae/lib.user-session

Version:

Stateful implementation of the user session.

142 lines (141 loc) 7.3 kB
import safeUid from 'uid-safe'; import { createHash } from 'crypto'; import { createException } from "./error.js"; const RENEWED_SESSION_FLAG = -1; class UserSessionManager { options; renewedSessions; constructor(options) { this.options = UserSessionManager.fillWithDefaults(options); this.renewedSessions = new Map(); } async create(subject, context, sessionTtl) { const sessionId = await safeUid(this.options.idLength); if (!sessionTtl) { sessionTtl = this.options.sessionTtl; } const currentTimestamp = UserSessionManager.currentTimestamp(); context.createdAt = currentTimestamp; context.accessedAt = currentTimestamp; context.expiresAt = currentTimestamp + sessionTtl; await this.options.storage.insert(subject, sessionId, context, sessionTtl); return sessionId; } async read(subject, sessionId, context) { const sessionMetaData = await this.options.storage.read(subject, sessionId); if (sessionMetaData == null) { throw createException("USER_SESSION_NOT_FOUND" , `Session '${UserSessionManager.hash(sessionId)}' doesn't exist. Context: ${JSON.stringify(context)}.`); } this.options.readUserSessionHook(subject, sessionId, context, sessionMetaData); const currentTimestamp = UserSessionManager.currentTimestamp(); if (this.options.timeouts.renewal) { const sessionAge = currentTimestamp - sessionMetaData.createdAt; if (sessionAge >= this.options.timeouts.renewal) { return [sessionMetaData, await this.renew(subject, sessionId, sessionMetaData, context)]; } } if (this.options.timeouts.idle) { const timeSinceLastAccess = currentTimestamp - sessionMetaData.accessedAt; if (timeSinceLastAccess >= this.options.timeouts.idle) { await this.options.storage.delete(subject, sessionId); throw createException("USER_SESSION_EXPIRED" , `Session '${UserSessionManager.hash(sessionId)}' it's expired, because it was idle for ${timeSinceLastAccess} seconds. Context: ${JSON.stringify(context)}.`); } } sessionMetaData.accessedAt = currentTimestamp; await this.options.storage.updateAccessedAt(subject, sessionId, sessionMetaData); return [sessionMetaData, null]; } readAll(subject) { return this.options.storage.readAll(subject); } async renew(subject, sessionId, sessionMetaData, context) { if (this.renewedSessions.has(sessionId)) { this.options.renewSessionHooks.onRenewMadeAlreadyFromCurrentProcess(sessionId); return null; } if (sessionMetaData.accessedAt === RENEWED_SESSION_FLAG) { this.options.renewSessionHooks.onRenewMadeAlreadyFromAnotherProcess(sessionId); return null; } try { this.renewedSessions.set(sessionId, null); sessionMetaData.accessedAt = RENEWED_SESSION_FLAG; await this.options.storage.updateAccessedAt(subject, sessionId, sessionMetaData); } catch (e) { this.renewedSessions.delete(sessionId); throw e; } this.renewedSessions.set(sessionId, setTimeout(() => this.delete(subject, sessionId).catch((e) => this.options.renewSessionHooks.onOldSessionDeleteFailure(sessionId, e)), this.options.timeouts.oldSessionAvailabilityAfterRenewal * 1000)); return this.create(subject, context, sessionMetaData.expiresAt - sessionMetaData.createdAt); } async delete(subject, sessionId) { await this.options.storage.delete(subject, sessionId); const deleteOfRenewedSessionTimeout = this.renewedSessions.get(sessionId); if (deleteOfRenewedSessionTimeout != null) { clearTimeout(deleteOfRenewedSessionTimeout); this.renewedSessions.delete(sessionId); } } deleteAll(subject) { return this.options.storage.deleteAll(subject); } static hash(sessionId) { return createHash('sha1').update(sessionId).digest('base64'); } static fillWithDefaults(options) { if (options.idLength < 15) { throw createException("INVALID_SESSION_ID_LENGTH" , `Session id length can't be lower than 15 characters. Given: ${options.idLength}.`); } if (options.timeouts == null) { options.timeouts = {}; } else { if (options.timeouts.idle != null) { if (options.timeouts.idle <= 0 || options.timeouts.idle >= options.sessionTtl) { throw createException("INVALID_IDLE_TIMEOUT" , `Idle timeout needs to be >0 && <${options.sessionTtl}. Given ${options.timeouts.idle}`); } } if (options.timeouts.renewal != null) { if (options.timeouts.renewal <= 0 || options.timeouts.renewal >= options.sessionTtl) { throw createException("INVALID_RENEW_TIMEOUT" , `Renew timeout needs to be >0 && <${options.sessionTtl}. Given ${options.timeouts.renewal}`); } if (options.timeouts.oldSessionAvailabilityAfterRenewal == null) { throw createException("OLD_SESSION_AVAILABILITY_TIMEOUT_AFTER_RENEWAL_REQUIRED" , "'timeouts.oldSessionAvailabilityAfterRenewal' is a required property when 'timeouts.renewal' is set."); } if (options.timeouts.oldSessionAvailabilityAfterRenewal <= 0 || options.timeouts.oldSessionAvailabilityAfterRenewal >= options.sessionTtl) { throw createException("INVALID_OLD_SESSION_AVAILABILITY_TIMEOUT_AFTER_RENEWAL" , `'timeouts.oldSessionAvailabilityAfterRenewal' needs to be >0 && <${options.sessionTtl}. Given ${options.timeouts.oldSessionAvailabilityAfterRenewal}.`); } } } if (options.readUserSessionHook == null) { options.readUserSessionHook = UserSessionManager.readUserSessionHook; } if (options.renewSessionHooks == null) { options.renewSessionHooks = UserSessionManager.renewSessionHooks; } return options; } static currentTimestamp() { return Math.floor(new Date().getTime() / 1000); } static readUserSessionHook(subject, sessionId, context, sessionMetaData) { if (context.device && sessionMetaData.device) { if (context.device.type !== sessionMetaData.device.type || context.device.name !== sessionMetaData.device.name) { throw createException("FORBIDDEN_ACCESS_TO_USER_SESSION_FROM_DIFFERENT_DEVICE" , `Attempting to access session '${UserSessionManager.hash(sessionId)}' of the subject '${subject}' from a device which differs from the one session was created. Context: ${JSON.stringify(context)}.`); } } } static renewSessionHooks = { onOldSessionDeleteFailure() { return undefined; }, onRenewMadeAlreadyFromAnotherProcess() { return undefined; }, onRenewMadeAlreadyFromCurrentProcess() { return undefined; } }; } export { UserSessionManager, RENEWED_SESSION_FLAG };