UNPKG

@dwp/govuk-casa

Version:

A framework for building GOVUK Collect-And-Submit-Applications

170 lines 6.98 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = sessionMiddleware; // A last-modified cookie is used to control whether the end user sees a // session-timeout page, or they are simply given a new session without // interrupting their journey. const express_session_1 = __importStar(require("express-session")); const logger_js_1 = __importDefault(require("../lib/logger.js")); const utils_js_1 = require("../lib/utils.js"); /** * @typedef {import("express").RequestHandler} RequestHandler * @access private */ const log = (0, logger_js_1.default)("middleware:session"); const sessionExpiryMiddleware = (ttl, getCookie, touchCookie, removeCookie) => (req, res, next) => { const lastModified = getCookie(req); const age = Math.floor(Date.now() * 0.001) - lastModified; if (lastModified === 0) { // New session, or grace period cookie no longer available after // expiring; generate a new session, and create grace-period cookie. // This will invalidate any CSRF tokens, so by letting the request POST // requests through the user may see a 500 error response. log.info("Session is new, or grace period has expired. Regenerating session."); req.session.regenerate((err) => { if (err) { next(err); } else { touchCookie(res); if (req.method === "POST") { log.info("The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point."); res.redirect(302, (0, utils_js_1.validateUrlPath)(`${req.baseUrl}/`)); } else { next(); } } }); } else if (age > ttl) { // Cookie has become stale and server session will have been removed; // redirect to session-timeout log.info("Session has timed out within grace period. Destroying session and redirecting to timeout page."); const language = req.session.language ?? "en"; req.session.destroy((err) => { if (err) { next(err); } else { removeCookie(res); const params = new URLSearchParams({ referrer: req.originalUrl, lang: language, }); res.redirect(302, (0, utils_js_1.validateUrlPath)(`${req.baseUrl}/session-timeout`) + `?${params.toString()}`); } }); } else { // Touch cookie and continue touchCookie(res); next(); } }; /** * Produces three middleware functions: * * - Set the session cookie * - Parse request cookies * - Handle expiry of server-side session * * @param {object} opts Options * @param {RequestHandler} opts.cookieParserMiddleware Cookie parsing middleware * @param {string} opts.secret Session encryption secret * @param {string} opts.name Session cookie name * @param {boolean} opts.secure Secure cookies only * @param {number} opts.ttl Session data time-to-live * @param {boolean | string} [opts.cookieSameSite] Cooke SameSite setting * @param {string} [opts.cookiePath] Cookie path * @param {object} [opts.store] Storage instance * @returns {RequestHandler[]} Middleware functions */ function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, cookieSameSite = true, cookiePath = "/", store = new express_session_1.MemoryStore(), }) { const commonCookieOptions = { httpOnly: true, path: cookiePath, secure, }; if (cookieSameSite !== false) { commonCookieOptions.sameSite = cookieSameSite === true ? "Strict" : cookieSameSite; } const ttlGrace = 1800; // user will see session-timeout if session expires within 30mins const touchCookieName = `${name}.t`; const touchCookieOptions = { ...commonCookieOptions, maxAge: (ttl + ttlGrace) * 1000, signed: true, }; const getCookie = (req) => { // Disabled eslint as `touchCookieName` is a constant, known value const lastModified = Date.parse( /* eslint-disable-next-line security/detect-object-injection */ String(req.signedCookies[touchCookieName] ?? "1970-01-01T00:00:00+0000")); return Number.isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001); }; const touchCookie = (res) => { // Touch cookie expiry is a short period after the session ttl. This gives // a small period of time where a user will see the session-timeout message, // which is important to avoid the confusion of simply being redirected back // to the start of their journey. res.cookie(touchCookieName, new Date(Date.now()).toUTCString(), touchCookieOptions); }; const removeCookie = (res) => { res.clearCookie(touchCookieName, touchCookieOptions); }; return [ (0, express_session_1.default)({ secret, name, saveUninitialized: false, resave: false, cookie: { ...commonCookieOptions, maxAge: null, }, store, }), cookieParserMiddleware, sessionExpiryMiddleware(ttl, getCookie, touchCookie, removeCookie), ]; } //# sourceMappingURL=session.js.map