UNPKG

@hmcts/rpx-xui-node-lib

Version:

Common nodejs library components for XUI

566 lines 26.7 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Strategy = void 0; const events = __importStar(require("events")); const passport_1 = __importDefault(require("passport")); const auth_constants_1 = require("../auth.constants"); const jwt_decode_1 = __importDefault(require("jwt-decode")); const common_1 = require("../../common"); const joi_1 = __importDefault(require("@hapi/joi")); const URL = __importStar(require("url")); const openid_client_1 = require("openid-client"); const csurf_1 = __importDefault(require("csurf")); class Strategy extends events.EventEmitter { constructor(strategyName, router, logger = (0, common_1.getLogger)('auth:strategy')) { super(); this.options = { authorizationURL: '', tokenURL: '', clientID: '', clientSecret: '', callbackURL: '', scope: '', logoutURL: '', useRoutes: true, sessionKey: '', //openID options discoveryEndpoint: '', issuerURL: '', responseTypes: [''], tokenEndpointAuthMethod: '', allowRolesRegex: '.', useCSRF: true, routeCredential: undefined, serviceOverride: false, }; /* istanbul ignore next */ this.initialiseStrategy = (options) => __awaiter(this, void 0, void 0, function* () { this.options = options; }); /** * The login route handler, will attempt to setup security state param and redirect user if not authenticated * @param req Request * @param res Response * @param next NextFunction */ /* istanbul ignore next */ this.loginHandler = (req, res, next) => __awaiter(this, void 0, void 0, function* () { this.logger.log('Base loginHandler Hit'); const reqSession = req.session; // we are using oidc generator but it's just a helper, rather than installing another library to provide this const state = openid_client_1.generators.state(); /* istanbul ignore next */ const promise = new Promise((resolve) => { var _a, _b; if (req.session && ((_a = this.options) === null || _a === void 0 ? void 0 : _a.sessionKey)) { reqSession[(_b = this.options) === null || _b === void 0 ? void 0 : _b.sessionKey] = { state }; this.logger.log('saving state in session'); req.session.save(() => { this.logger.log('state saved in session'); resolve(true); }); } else { this.logger.warn('sessionKey not available state not saved'); resolve(false); } }); try { /* istanbul ignore next */ yield promise; /* istanbul ignore next */ this.logger.log('calling passport authenticate'); /* istanbul ignore next */ return passport_1.default.authenticate(this.strategyName, { redirect_uri: reqSession === null || reqSession === void 0 ? void 0 : reqSession.callbackURL, state, }, (error, user, info) => { var _a; /* istanbul ignore next */ if (error) { this.logger.error('passport authenticate error ', JSON.stringify(error)); } /* istanbul ignore next */ if (info) { this.logger.info('passport authenticate info', JSON.stringify(info)); } /* istanbul ignore next */ if (!user) { const message = 'No user details returned by the authentication service, redirecting to login'; this.logger.log(message); } else { this.logger.log('User details from passport.authenticate ' + ((_a = user.userInfo) === null || _a === void 0 ? void 0 : _a.email)); } })(req, res, next); /* istanbul ignore next */ } catch (error) { this.logger.error('Exception in passport.authenticate', error, this.strategyName); next(error); return Promise.reject(error); } }); /* istanbul ignore next */ this.setCallbackURL = (req, _res, next) => { const reqSession = req.session; /* istanbul ignore else */ if (req.session && !reqSession.callbackURL) { req.app.set('trust proxy', true); reqSession.callbackURL = URL.format({ protocol: req.protocol, host: req.get('host'), pathname: this.options.callbackURL, }); } /* istanbul ignore next */ next(); }; /* istanbul ignore next */ this.logout = (req, res) => __awaiter(this, void 0, void 0, function* () { const reqSession = req.session; try { this.logger.log('logout start'); const { accessToken, refreshToken } = (reqSession === null || reqSession === void 0 ? void 0 : reqSession.passport.user.tokenset) || null; const auth = this.getAuthorization(this.options.clientID, this.options.clientSecret); yield common_1.http.delete(this.urlFromToken(this.options.logoutURL, accessToken), { headers: { Authorization: auth, }, }); yield common_1.http.delete(this.urlFromToken(this.options.logoutURL, refreshToken), { headers: { Authorization: auth, }, }); //passport provides this method on request object req.logout((err) => { console.error(err); }); yield this.destroySession(req); /* istanbul ignore next */ if (req.query.noredirect) { res.status(200).send({ message: 'You have been logged out!' }); return Promise.resolve(); } const redirect = req.query.redirect ? req.query.redirect : auth_constants_1.AUTH.ROUTE.LOGIN; this.logger.log('redirecting to => ', redirect); // 401 is when no accessToken res.redirect(redirect); /* istanbul ignore next */ } catch (e) { this.logger.error('error => ', e); res.status(401).redirect(auth_constants_1.AUTH.ROUTE.DEFAULT_REDIRECT); } this.logger.log('logout end'); }); /* istanbul ignore next */ this.authRouteHandler = (req, res) => { return res.send(req.isAuthenticated()); }; /* istanbul ignore next */ this.destroySession = (req) => __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { var _a; (_a = req.session) === null || _a === void 0 ? void 0 : _a.destroy((err) => { if (err) { reject(err); } this.logger.log('session destroyed'); resolve(true); }); }); }); /* istanbul ignore next */ this.keepAliveHandler = (_req, _res, next) => { next(); }; /* istanbul ignore next */ this.configure = (options) => { const configuredOptions = Object.assign(Object.assign({}, this.options), options); this.validateOptions(configuredOptions); this.options = configuredOptions; this.serializeUser(); this.deserializeUser(); (() => __awaiter(this, void 0, void 0, function* () { yield this.initialiseStrategy(this.options); }))(); this.initializePassport(); this.initializeSession(); this.initializeKeepAlive(); this.initialiseCSRF(); if (options.useRoutes) { this.router.get(auth_constants_1.AUTH.ROUTE.DEFAULT_AUTH_ROUTE, this.authRouteHandler); this.router.get(auth_constants_1.AUTH.ROUTE.KEEPALIVE_ROUTE, this.authRouteHandler); this.router.get(auth_constants_1.AUTH.ROUTE.LOGIN, this.setCallbackURL, this.loginHandler); this.router.get(auth_constants_1.AUTH.ROUTE.OAUTH_CALLBACK, this.callbackHandler); this.router.get(auth_constants_1.AUTH.ROUTE.LOGOUT, this.logout); } this.addHeaders(); this.emit(`${this.strategyName}.bootstrap.success`); return this.router; }; /* istanbul ignore next */ this.callbackHandler = (req, res, next) => { this.logger.log('inside callbackHandler'); const INVALID_STATE_ERROR = 'Invalid authorization request state.'; const reqSession = req.session; const LOGIN_BOOKMARK_ERROR = 'LoginBookmarkUsed :'; const emitAuthenticationFailure = (logMessages) => { this.logger.log('inside emitAuthenticationFailure'); if (!logMessages.length) return; this.logger.log(`emitAuthenticationFailure logMessages ${logMessages.join('\n')}`); res.locals.message = logMessages.join('\n'); this.emit(auth_constants_1.AUTH.EVENT.AUTHENTICATE_FAILURE, req, res, next); }; const redirectWithFailure = (errorMessages, INVALID_STATE_ERROR, uri) => { errorMessages.push(INVALID_STATE_ERROR); emitAuthenticationFailure(errorMessages); return res.redirect(uri); }; passport_1.default.authenticate(this.strategyName, { redirect_uri: reqSession === null || reqSession === void 0 ? void 0 : reqSession.callbackURL, }, (error, user, info) => { let errorMessages = []; this.logger.log('in passport authenticate callback'); if (error) { switch (error.name) { case 'TimeoutError': const timeoutErrorMessage = `${error.name}: timeout awaiting ${error.url} for ${error.gotOptions.gotTimeout.request}ms`; errorMessages.push(timeoutErrorMessage); this.logger.error(error); break; default: errorMessages.push(error); this.logger.error(error); break; } } if (info) { this.logger.info('Authenticate callback info', info); } if (!user) { const MISMATCH_NONCE = 'nonce mismatch'; const MISMATCH_STATE = 'state mismatch'; if ((info === null || info === void 0 ? void 0 : info.message) === INVALID_STATE_ERROR) { errorMessages.push(LOGIN_BOOKMARK_ERROR); return redirectWithFailure(errorMessages, INVALID_STATE_ERROR, auth_constants_1.AUTH.ROUTE.EXPIRED_LOGIN_LINK); } else if ((info === null || info === void 0 ? void 0 : info.message.includes(MISMATCH_NONCE)) || (info === null || info === void 0 ? void 0 : info.message.includes(MISMATCH_STATE))) { errorMessages.push(LOGIN_BOOKMARK_ERROR); return redirectWithFailure(errorMessages, info.message, auth_constants_1.AUTH.ROUTE.EXPIRED_LOGIN_LINK); } else if (error === null || error === void 0 ? void 0 : error.message.includes('did not find expected authorization request details in session')) { errorMessages = [LOGIN_BOOKMARK_ERROR]; return redirectWithFailure(errorMessages, error.message, auth_constants_1.AUTH.ROUTE.EXPIRED_LOGIN_LINK); } else { const message = 'No user details returned by the authentication service, redirecting to login'; this.logger.log(message); return redirectWithFailure(errorMessages, message, auth_constants_1.AUTH.ROUTE.LOGIN); } } emitAuthenticationFailure(errorMessages); this.verifyLogin(req, user, next, res); })(req, res, next); }; /* istanbul ignore next */ this.isTokenExpired = (token) => { const jwtData = (0, jwt_decode_1.default)(token); return this.jwTokenExpired(jwtData); }; /* istanbul ignore next */ this.authenticate = (req, _res, next) => { var _a, _b, _c; if (req.isUnauthenticated() || !((_c = (_b = (_a = req === null || req === void 0 ? void 0 : req.session) === null || _a === void 0 ? void 0 : _a.passport) === null || _b === void 0 ? void 0 : _b.user) === null || _c === void 0 ? void 0 : _c.userinfo)) { this.logger.log('unauthenticated'); return _res.status(401).send({ message: 'Unauthorized' }); } next(); }; /* istanbul ignore next */ this.makeAuthorization = (passport) => `Bearer ${passport.user.tokenset.accessToken}`; /* istanbul ignore next */ this.setHeaders = (req, _res, next) => __awaiter(this, void 0, void 0, function* () { var _a; const reqSession = req.session; if ((_a = reqSession === null || reqSession === void 0 ? void 0 : reqSession.passport) === null || _a === void 0 ? void 0 : _a.user) { if (this.isRouteCredentialNeeded(req.path, this.options)) { yield this.setCredentialToken(req); } else { req.headers['user-roles'] = reqSession.passport.user.userinfo.roles.join(); req.headers.Authorization = this.makeAuthorization(reqSession.passport); } } else if (this.isRouteCredentialNeeded(req.path, this.options)) { yield this.setCredentialToken(req); } next(); }); /* istanbul ignore next */ this.isRouteCredentialNeeded = (url, options) => { return options.routeCredential && options.routeCredential.routes && options.routeCredential.routes.includes(url); }; /* istanbul ignore next */ this.setCredentialToken = (req) => __awaiter(this, void 0, void 0, function* () { let routeCredentialToken; const cachedToken = req.app.get('routeCredentialToken'); if (cachedToken && cachedToken.access_token && !this.isTokenExpired(cachedToken.access_token)) { routeCredentialToken = cachedToken; } else { routeCredentialToken = yield this.generateToken(); req.app.set('routeCredentialToken', routeCredentialToken); } if (routeCredentialToken && routeCredentialToken.access_token) { req.headers.Authorization = `Bearer ${routeCredentialToken.access_token}`; } }); /* istanbul ignore next */ this.generateToken = () => __awaiter(this, void 0, void 0, function* () { const url = this.getUrlFromOptions(this.options); try { const axiosConfig = { headers: { 'content-type': 'application/x-www-form-urlencoded' }, }; const body = this.getRequestBody(this.options); const response = yield common_1.http.post(url, body, axiosConfig); return response.data; } catch (error) { this.logger.error('error generating authentication token => ', error); } }); /* istanbul ignore next */ this.verifyLogin = (req, user, next, res) => { req.logIn(user, (err) => { const roles = user.userinfo.roles; if (err) { this.logger.error('verifyLogin error', err); return next(err); } if (this.options.allowRolesRegex && !(0, common_1.arrayPatternMatch)(roles, this.options.allowRolesRegex)) { this.logger.error(`User has no application access, as they do not have a role that matches ${this.options.allowRolesRegex}.`); return this.logout(req, res); } if (!this.listenerCount(auth_constants_1.AUTH.EVENT.AUTHENTICATE_SUCCESS)) { this.logger.log(`redirecting, no listener count: ${auth_constants_1.AUTH.EVENT.AUTHENTICATE_SUCCESS}, user: ${user.email}`); res.redirect(auth_constants_1.AUTH.ROUTE.DEFAULT_REDIRECT); } else { req.isRefresh = false; this.emit(auth_constants_1.AUTH.EVENT.AUTHENTICATE_SUCCESS, req, res, next); } }); }; /* istanbul ignore next */ this.initializePassport = () => { this.router.use(passport_1.default.initialize()); }; /* istanbul ignore next */ this.initializeSession = () => { this.router.use(passport_1.default.session()); }; /* istanbul ignore next */ this.initializeKeepAlive = () => { this.router.use(this.keepAliveHandler); }; /** * helper method to store csrf token into session */ /* istanbul ignore next */ this.initialiseCSRF = () => { if (this.options.useCSRF) { this.logger.log('initialising CSRF middleware'); const csrfProtection = (0, csurf_1.default)({ value: this.getCSRFValue, }); // cookie options added via EXUI-986, fortify issues const cookieOptions = { sameSite: 'none', secure: true, }; /* istanbul ignore next */ this.router.use(csrfProtection, (req, res, next) => { res.cookie('XSRF-TOKEN', req.csrfToken(), cookieOptions); next(); }); } }; /** * retrieve the csrf token value, lastly from sent cookies * @param req * @return string */ /* istanbul ignore next */ this.getCSRFValue = (req) => { return ((req.body && req.body._csrf) || (req.query && req.query._csrf) || req.headers['csrf-token'] || req.headers['xsrf-token'] || req.headers['x-csrf-token'] || req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']); }; /* istanbul ignore next */ this.addHeaders = () => { this.router.use(this.setHeaders); }; /* istanbul ignore next */ this.serializeUser = () => { passport_1.default.serializeUser((user, done) => { this.logger.log(`${this.strategyName} serializeUser`); this.emitIfListenersExist(auth_constants_1.AUTH.EVENT.SERIALIZE_USER, user, done); }); }; /* istanbul ignore next */ this.deserializeUser = () => { passport_1.default.deserializeUser((id, done) => { this.logger.log(`${this.strategyName} deserializeUser`); this.emitIfListenersExist(auth_constants_1.AUTH.EVENT.DESERIALIZE_USER, id, done); }); }; /* istanbul ignore next */ this.jwTokenExpired = (jwtData) => { const expires = new Date(jwtData.exp * 1000).getTime(); const now = new Date().getTime(); return expires < now; }; /** * Get session URL * @return {string} */ /* istanbul ignore next */ this.urlFromToken = (url, token) => { return `${url}/session/${token}`; }; /** * Get authorization from ClientID and secret * @return {string} */ /* istanbul ignore next */ this.getAuthorization = (clientID, clientSecret, encoding = 'base64') => { return `Basic ${Buffer.from(`${clientID}:${clientSecret}`).toString(encoding)}`; }; /** * Get all the events that this strategy emits * @return {string[]} - ['auth.authenticate.success'] */ /* istanbul ignore next */ this.getEvents = () => { return Object.values(auth_constants_1.AUTH.EVENT); }; /** * emit Events if any subscriptions available */ /* istanbul ignore next */ this.emitIfListenersExist = (eventName, eventObject, done) => { if (!this.listenerCount(eventName)) { this.logger.error('no listeners for event ' + eventName); done(null, eventObject); } else { this.emit(eventName, eventObject, done); } }; /* istanbul ignore next */ this.getRequestBody = (options) => { var _a; if (options.routeCredential) { const userName = options.routeCredential.userName; const userPassword = encodeURIComponent(options.routeCredential.password); const scope = (_a = options.routeCredential) === null || _a === void 0 ? void 0 : _a.scope; const clientSecret = options.clientSecret; const idamClient = options.clientID; return `grant_type=password&password=${userPassword}&username=${userName}&scope=${scope}&client_id=${idamClient}&client_secret=${clientSecret}`; } const msg = 'options.routeCredential missing values'; throw new Error('options.routeCredential missing values'); }; /* istanbul ignore next */ this.getUrlFromOptions = (options) => { if (options.routeCredential) { return `${options.logoutURL}/o/token`; } const msg = 'missing routeCredential in options'; this.logger.error('msg'); throw new Error(msg); }; this.strategyName = strategyName; this.router = router; this.logger = logger; } validateOptions(options) { const schema = joi_1.default.object({ authorizationURL: joi_1.default.string().required(), tokenURL: joi_1.default.string().required(), clientID: joi_1.default.string().required(), clientSecret: joi_1.default.string().required(), callbackURL: joi_1.default.string().required(), discoveryEndpoint: joi_1.default.string(), issuerURL: joi_1.default.string(), logoutURL: joi_1.default.string().required(), scope: joi_1.default.string().required(), scopeSeparator: joi_1.default.any(), sessionKey: joi_1.default.any(), useRoutes: joi_1.default.bool(), skipUserProfile: joi_1.default.any(), responseTypes: joi_1.default.array(), tokenEndpointAuthMethod: joi_1.default.string(), pkce: joi_1.default.any(), proxy: joi_1.default.any(), store: joi_1.default.any(), state: joi_1.default.any(), customHeaders: joi_1.default.any(), allowRolesRegex: joi_1.default.string(), useCSRF: joi_1.default.bool(), serviceOverride: joi_1.default.bool(), routeCredential: joi_1.default.any(), }); const { error } = schema.validate(options); if (error) { throw error; } return true; } } exports.Strategy = Strategy; //# sourceMappingURL=strategy.class.js.map