@thermopylae/lib.user-session
Version:
Stateful implementation of the user session.
142 lines (141 loc) • 7.3 kB
JavaScript
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 };