@thermopylae/core.cookie-session
Version:
Cookie user session for HTTP interface.
133 lines (132 loc) • 6.64 kB
JavaScript
import { UserSessionManager } from '@thermopylae/lib.user-session';
import { UserSessionUtils } from '@thermopylae/core.user-session.commons';
import { Exception } from '@thermopylae/lib.exception';
import { serialize } from 'cookie';
import { createException } from "./error.js";
class CookieUserSessionMiddleware {
options;
sessionManager;
cookieSerializeOptions;
invalidateSessionCookieHeaderValue;
constructor(options) {
this.options = CookieUserSessionMiddleware.fillWithDefaults(options);
this.sessionManager = new UserSessionManager(this.options.sessionManager);
const cookieSerializeOptions = {
secure: true,
httpOnly: true,
sameSite: this.options.session.cookie.sameSite,
path: this.options.session.cookie.path,
domain: this.options.session.cookie.domain,
maxAge: undefined,
expires: undefined
};
cookieSerializeOptions.expires = new Date('Thu, 01 Jan 1970 00:00:00 GMT');
this.invalidateSessionCookieHeaderValue = serialize(this.options.session.cookie.name, '', cookieSerializeOptions);
delete cookieSerializeOptions.expires;
this.cookieSerializeOptions = Object.seal(cookieSerializeOptions);
}
get userSessionManager() {
return this.sessionManager;
}
async create(req, res, subject, sessionTtl) {
const context = UserSessionUtils.buildUserSessionContext(req);
const sessionId = await this.sessionManager.create(subject, context, sessionTtl);
try {
this.setSessionIdInResponseHeader(req.device && req.device.client && req.device.client.type, res, sessionId, sessionTtl);
}
catch (e) {
await this.sessionManager.delete(subject, sessionId);
throw e;
}
}
async verify(req, res, subject, unsetSessionCookie = true) {
const [sessionId, clientType] = this.extractSessionId(req);
try {
const [metaData, renewedSessionId] = await this.sessionManager.read(subject, sessionId, UserSessionUtils.buildUserSessionContext(req));
if (renewedSessionId != null) {
this.setSessionIdInResponseHeader(clientType, res, renewedSessionId, metaData.expiresAt - metaData.createdAt);
}
return metaData;
}
catch (e) {
if (unsetSessionCookie &&
clientType === 'browser' &&
e instanceof Exception &&
(e.code === "USER_SESSION_NOT_FOUND" || e.code === "USER_SESSION_EXPIRED" )) {
res.header('set-cookie', this.invalidateSessionCookieHeaderValue);
}
throw e;
}
}
async renew(req, res, subject, metaData) {
const [sessionId, clientType] = this.extractSessionId(req);
const renewedSessionId = await this.sessionManager.renew(subject, sessionId, metaData, UserSessionUtils.buildUserSessionContext(req));
if (renewedSessionId != null) {
this.setSessionIdInResponseHeader(clientType, res, renewedSessionId, metaData.expiresAt - metaData.createdAt);
}
}
async delete(req, res, subject, sessionId = undefined, unsetSessionCookie = true) {
let clientType;
if (sessionId == null) {
[sessionId, clientType] = this.extractSessionId(req);
}
await this.sessionManager.delete(subject, sessionId);
if (unsetSessionCookie && clientType === 'browser') {
res.header('set-cookie', this.invalidateSessionCookieHeaderValue);
}
}
extractSessionId(req) {
let sessionId;
if ((sessionId = req.cookie(this.options.session.cookie.name)) == null) {
return [this.options.sessionIdExtractor(req.header('authorization')), null];
}
const csrf = req.header(this.options.session.csrf.name);
if (csrf !== this.options.session.csrf.value) {
throw createException("CSRF_HEADER_INVALID_VALUE" , `CSRF header value '${csrf}' differs from the expected one.`);
}
return [sessionId, 'browser'];
}
setSessionIdInResponseHeader(clientType, res, sessionId, sessionTtl) {
if (clientType === 'browser') {
this.cookieSerializeOptions.maxAge = this.options.session.cookie.persistent ? sessionTtl || this.options.sessionManager.sessionTtl : undefined;
res.header('set-cookie', serialize(this.options.session.cookie.name, sessionId, this.cookieSerializeOptions));
if (this.options.session['cache-control']) {
res.header('cache-control', 'no-cache="set-cookie, set-cookie2"');
}
}
else {
res.header(this.options.session.header, sessionId);
}
}
static fillWithDefaults(options) {
if (options.session.cookie.name.startsWith('__Host-')) {
throw createException("SESSION_COOKIE_NAME_INVALID_FORMAT" , `Session cookie name is not allowed to start with '__Host-'. Given: ${options.session.cookie.name}.`);
}
if (options.session.cookie.name.startsWith('__Secure-')) {
throw createException("SESSION_COOKIE_NAME_INVALID_FORMAT" , `Session cookie name is not allowed to start with '__Secure-'. Given: ${options.session.cookie.name}.`);
}
if (!isLowerCase(options.session.cookie.name)) {
throw createException("SESSION_COOKIE_NAME_MUST_BE_LOWERCASE" , `Cookie name should be lowercase. Given: ${options.session.cookie.name}.`);
}
if (options.session.cookie.domain == null && options.session.cookie.path === '/') {
options.session.cookie.name = `__Host-${options.session.cookie.name}`;
}
else {
options.session.cookie.name = `__Secure-${options.session.cookie.name}`;
}
if (!isLowerCase(options.session.header)) {
throw createException("SESSION_ID_HEADER_NAME_MUST_BE_LOWERCASE" , `Session id header name needs to be lower case. Given: ${options.session.header}`);
}
if (!isLowerCase(options.session.csrf.name)) {
throw createException("CSRF_HEADER_NAME_MUST_BE_LOWERCASE" , `CSRF header name needs to be lower case. Given: ${options.session.csrf.name}`);
}
if (options.sessionIdExtractor == null) {
options.sessionIdExtractor = (authorization) => UserSessionUtils.extractTokenFromAuthorization(authorization, createException);
}
return options;
}
}
function isLowerCase(str) {
return str.toLowerCase() === str;
}
export { CookieUserSessionMiddleware };