UNPKG

@hmcts/rpx-xui-node-lib

Version:

Common nodejs library components for XUI

248 lines 12.6 kB
"use strict"; 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.oidc = exports.OpenID = void 0; const express_1 = require("express"); const openid_client_1 = require("openid-client"); const passport_1 = __importDefault(require("passport")); const oidc_constants_1 = require("../oidc.constants"); const auth_constants_1 = require("../../auth.constants"); const models_1 = require("../../models"); const messaging_constants_1 = require("../../messaging.constants"); const common_1 = require("../../../common"); class OpenID extends models_1.Strategy { constructor(router = (0, express_1.Router)({ mergeParams: true }), logger = (0, common_1.getLogger)('auth:oidc'), options = {}) { super(oidc_constants_1.OIDC.STRATEGY_NAME, router, logger); /** * Helper function to customise GOT defaults and hooks to provide debug information * @param options */ /* istanbul ignore next */ this.setHttpOptionsDefaults = (options) => { const defaults = { retry: 3, timeout: 15000, hooks: { beforeRequest: [ (options) => { this.logger.log('--> %s %s', options.method.toUpperCase(), options.href); }, ], afterResponse: [ (response) => { this.logger.log('<-- %i FROM %s %s', response.statusCode, response.request.gotOptions.method.toUpperCase(), response.request.gotOptions.href); return response; }, ], }, }; const httpOptions = Object.assign(Object.assign({}, defaults), options); openid_client_1.custom.setHttpOptionsDefaults(httpOptions); }; this.getOpenIDOptions = (authOptions) => { return { client_id: authOptions.clientID, client_secret: authOptions.clientSecret, discovery_endpoint: authOptions.discoveryEndpoint, issuer_url: authOptions.issuerURL, logout_url: authOptions.logoutURL, response_types: authOptions.responseTypes, scope: authOptions.scope, sessionKey: authOptions.sessionKey, token_endpoint_auth_method: authOptions.tokenEndpointAuthMethod, useRoutes: authOptions.useRoutes, }; }; // TODO: this.client should be passed in // This function is hard to mock, come back to once we've mocked out easier prod code. /* istanbul ignore next */ this.keepAliveHandler = (req, res, next) => __awaiter(this, void 0, void 0, function* () { var _a, _b; const reqsession = req.session; if (!((_a = reqsession === null || reqsession === void 0 ? void 0 : reqsession.passport) === null || _a === void 0 ? void 0 : _a.user)) { return next(); } if (req.isAuthenticated() && this.getClient()) { const userDetails = reqsession.passport.user; const currentAccessToken = userDetails.tokenset.accessToken; if (currentAccessToken) { try { // TODO: ideally we need to introspect the tokens but currently unsupported in IDAM if (this.isTokenExpired(currentAccessToken)) { this.logger.log('token expired'); const tokenSet = yield ((_b = this.getClient()) === null || _b === void 0 ? void 0 : _b.refresh(reqsession.passport.user.tokenset.refreshToken)); reqsession.passport.user.tokenset = this.convertTokenSet(tokenSet); if (!this.listenerCount(auth_constants_1.AUTH.EVENT.AUTHENTICATE_SUCCESS)) { this.logger.log(`refresh: no listener count: ${auth_constants_1.AUTH.EVENT.AUTHENTICATE_SUCCESS}`); return next(); } else { req.isRefresh = true; this.emit(auth_constants_1.AUTH.EVENT.AUTHENTICATE_SUCCESS, req, res, next); return; } } } catch (e) { this.logger.error('refresh error => ', e); next(e); } } } next(); }); this.discover = () => __awaiter(this, void 0, void 0, function* () { var _c; this.logger.info(`discovering endpoint: ${this.options.discoveryEndpoint}`); const issuer = yield this.discoverIssuer(); const metadata = issuer.metadata; this.logger.info(`start serviceOverride check`); if (!this.options.serviceOverride) { this.logger.info(`issuerURL: ${(_c = this.options) === null || _c === void 0 ? void 0 : _c.issuerURL}`); metadata.issuer = this.options.issuerURL; } this.logger.info(`end serviceOverride check`); this.logger.log('discover metadata', metadata); return this.newIssuer(metadata); }); this.initialiseStrategy = (authOptions) => __awaiter(this, void 0, void 0, function* () { this.logger.log('initialiseStrategy start'); const options = this.getOpenIDOptions(authOptions); const strategy = yield this.createNewStrategy(options); this.useStrategy(this.strategyName, strategy); this.logger.log('initialiseStrategy end'); }); this.convertTokenSet = (tokenset) => { return { accessToken: tokenset === null || tokenset === void 0 ? void 0 : tokenset.access_token, refreshToken: tokenset === null || tokenset === void 0 ? void 0 : tokenset.refresh_token, idToken: tokenset === null || tokenset === void 0 ? void 0 : tokenset.id_token, }; }; this.verify = (tokenset, userinfo, done) => { if (!(userinfo === null || userinfo === void 0 ? void 0 : userinfo.roles)) { this.logger.warn(messaging_constants_1.VERIFY_ERROR_MESSAGE_NO_ACCESS_ROLES); return done(null, false, { message: messaging_constants_1.VERIFY_ERROR_MESSAGE_NO_ACCESS_ROLES }); } this.logger.info('verify okay, user:', userinfo); return done(null, { tokenset: this.convertTokenSet(tokenset), userinfo }); }; this.discoverIssuer = () => __awaiter(this, void 0, void 0, function* () { return yield openid_client_1.Issuer.discover(`${this.options.discoveryEndpoint}`); }); this.newIssuer = (metadata) => { this.logger.log('newIssuer'); return new openid_client_1.Issuer(metadata); }; this.useStrategy = (strategyName, strategy) => { passport_1.default.use(strategyName, strategy); }; // TODO: Don't throw errors from inside functions as it's side effecting, // get the function to return and throw the error in the caller function. // Why? - this makes the function more pure, and allows it to be easily testable. /* istanbul ignore next */ this.createNewStrategy = (options) => __awaiter(this, void 0, void 0, function* () { this.issuer = yield this.discover(); if (!this.issuer) { throw new Error('auto discovery failed'); } this.client = this.getClientFromIssuer(this.issuer, options); if (!this.client) { throw new Error('client not initialised'); } return this.getNewStrategy(options, this.client); }); /* istanbul ignore next */ this.getNewStrategy = (options, client) => { return new openid_client_1.Strategy({ client, params: { prompt: oidc_constants_1.OIDC.PROMPT, scope: options.scope, }, sessionKey: options.sessionKey, }, this.verify); }; this.getClientFromIssuer = (issuer, options) => { return new issuer.Client(options); }; this.getClient = () => { return this.client; }; /** * The login route handler, will attempt to setup security state and nonce 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('OIDC loginHandler Hit'); const nonce = openid_client_1.generators.nonce(); const state = openid_client_1.generators.state(); const reqsession = req.session; 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('no session in request, state not saved'); resolve(false); } }); try { this.logger.log('waiting for session state to be saved'); yield promise; this.logger.log('calling passport authenticate'); return passport_1.default.authenticate(this.strategyName, { redirect_uri: reqsession === null || reqsession === void 0 ? void 0 : reqsession.callbackURL, nonce, state, }, (error, user, info) => { this.logger.log('passport authenticate'); if (error) { this.logger.error('loginHandler error: ', JSON.stringify(error)); } /* istanbul ignore next */ if (info) { this.logger.info('loginHandler info: ', JSON.stringify(info)); } /* istanbul ignore next */ if (user) { const message = 'loginHandler User details returned by passport authenticate'; this.logger.log(message); } if (!user) { const message = 'loginHandler no User details returned by passport authenticate'; this.logger.log(message); } })(req, res, next); } catch (error) { this.logger.error('this should not throw an error'); throw new Error(`this should not throw an ${error}`); } }); this.setHttpOptionsDefaults(options); } } exports.OpenID = OpenID; exports.oidc = new OpenID(); //# sourceMappingURL=openid.class.js.map