UNPKG

@thermopylae/core.cookie-session

Version:
133 lines (132 loc) 6.64 kB
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 };