@dwp/govuk-casa
Version:
A framework for building GOVUK Collect-And-Submit-Applications
170 lines • 6.98 kB
JavaScript
;
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