@hmcts/rpx-xui-node-lib
Version:
Common nodejs library components for XUI
248 lines • 12.6 kB
JavaScript
"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