UNPKG

openhim-core

Version:

The OpenHIM core application that provides logging and routing of http requests

248 lines (181 loc) 8.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.authenticate = authenticate; exports._getEnabledAuthenticationTypesFromConfig = void 0; var _basicAuth = _interopRequireDefault(require("basic-auth")); var _crypto = _interopRequireDefault(require("crypto")); var _winston = _interopRequireDefault(require("winston")); var _atnaAudit = _interopRequireDefault(require("atna-audit")); var _os = _interopRequireDefault(require("os")); var _users = require("../model/users"); var _config = require("../config"); var auditing = _interopRequireWildcard(require("../auditing")); var _utils = require("../utils"); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } _config.config.api = _config.config.get('api'); _config.config.auditing = _config.config.get('auditing'); const himSourceID = _config.config.auditing.auditEvents.auditSourceID; // will NOT audit any successful logins on the following paths (specified as regex patterns) // only 'noisy' endpoints should be included, such as heartbeats or endpoints that get polled // // /transactions is treated as a special case - see below const auditingExemptPaths = [/\/tasks/, /\/events.*/, /\/metrics.*/, /\/mediators\/.*\/heartbeat/, /\/audits/, /\/logs/]; const isUndefOrEmpty = string => string == null || string === ''; async function authenticateBasic(ctx) { const credentials = (0, _basicAuth.default)(ctx); if (credentials == null) { // No basic auth details found return null; } const { name: email, pass: password } = credentials; const user = await _users.UserModelAPI.findOne({ email: (0, _utils.caseInsensitiveRegex)(email) }); if (user == null) { // not authenticated - user not found ctx.throw(401, `No user exists for ${email}, denying access to API, request originated from ${ctx.request.host}`, { email }); } const hash = _crypto.default.createHash(user.passwordAlgorithm); hash.update(user.passwordSalt); hash.update(password); if (user.passwordHash !== hash.digest('hex')) { // not authenticated - password mismatch ctx.throw(401, `Password did not match expected value, denying access to API, the request was made by ${email} from ${ctx.request.host}`, { email }); } return user; } async function authenticateToken(ctx) { const { header } = ctx.request; const email = header['auth-username']; const authTS = header['auth-ts']; const authSalt = header['auth-salt']; const authToken = header['auth-token']; // if any of the required headers aren't present if (isUndefOrEmpty(email) || isUndefOrEmpty(authTS) || isUndefOrEmpty(authSalt) || isUndefOrEmpty(authToken)) { ctx.throw(401, `API request made by ${email} from ${ctx.request.host} is missing required API authentication headers, denying access`, { email }); } // check if request is recent const requestDate = new Date(Date.parse(authTS)); const authWindowSeconds = _config.config.api.authWindowSeconds != null ? _config.config.api.authWindowSeconds : 10; const to = new Date(); to.setSeconds(to.getSeconds() + authWindowSeconds); const from = new Date(); from.setSeconds(from.getSeconds() - authWindowSeconds); if (requestDate < from || requestDate > to) { // request expired ctx.throw(401, `API request made by ${email} from ${ctx.request.host} has expired, denying access`, { email }); } const user = await _users.UserModelAPI.findOne({ email: (0, _utils.caseInsensitiveRegex)(email) }); if (user == null) { // not authenticated - user not found ctx.throw(401, `No user exists for ${email}, denying access to API, request originated from ${ctx.request.host}`, { email }); } const hash = _crypto.default.createHash('sha512'); hash.update(user.passwordHash); hash.update(authSalt); hash.update(authTS); if (authToken !== hash.digest('hex')) { // not authenticated - token mismatch ctx.throw(401, `API token did not match expected value, denying access to API, the request was made by ${email} from ${ctx.request.host}`, { email }); } return user; } function getEnabledAuthenticationTypesFromConfig(config) { if (Array.isArray(config.api.authenticationTypes)) { return config.api.authenticationTypes; } try { // Attempt to parse the authentication types as JSON // e.g. if configured through an environment variable const enabledTypes = JSON.parse(config.api.authenticationTypes); if (Array.isArray(enabledTypes)) { return enabledTypes; } } catch (err) {// Squash parsing errors } _winston.default.warn(`Invalid value for API authenticationTypes config: ${config.api.authenticationTypes}`); return []; } function isAuthenticationTypeEnabled(type) { return getEnabledAuthenticationTypesFromConfig(_config.config).includes(type); } async function authenticateRequest(ctx) { let user; // First attempt basic authentication if enabled if (user == null && isAuthenticationTypeEnabled('basic')) { user = await authenticateBasic(ctx); } // Otherwise try token based authentication if enabled if (user == null && isAuthenticationTypeEnabled('token')) { user = await authenticateToken(ctx); } // User could not be authenticated if (user == null) { const enabledTypes = getEnabledAuthenticationTypesFromConfig(_config.config).join(', '); ctx.throw(401, `API request could not be authenticated with configured authentication types: "${enabledTypes}"`); } return user; } function handleAuditResponse(err) { if (err) { _winston.default.error('Sending audit event failed', err); return; } _winston.default.debug('Processed internal audit'); } async function authenticate(ctx, next) { let user; try { user = await authenticateRequest(ctx); } catch (err) { // Handle authentication errors if (err.status === 401) { _winston.default.info(err.message); // Set the status but NOT THE BODY // We do not want to expose any sensitive information in the body ctx.status = err.status; // Send an auth failure audit event let audit = _atnaAudit.default.construct.userLoginAudit(_atnaAudit.default.constants.OUTCOME_SERIOUS_FAILURE, himSourceID, _os.default.hostname(), err.email); audit = _atnaAudit.default.construct.wrapInSyslog(audit); auditing.sendAuditEvent(audit, handleAuditResponse); return; } // Rethrow other errors throw err; } // Set the user on the context for consumption by other middleware ctx.authenticated = user; // Deal with paths exempt from audit if (ctx.path === '/transactions') { if (!ctx.query.filterRepresentation || ctx.query.filterRepresentation !== 'full') { // exempt from auditing success return next(); } } else { for (const pathTest of auditingExemptPaths) { if (pathTest.test(ctx.path)) { // exempt from auditing success return next(); } } } // Send an auth success audit event let audit = _atnaAudit.default.construct.userLoginAudit(_atnaAudit.default.constants.OUTCOME_SUCCESS, himSourceID, _os.default.hostname(), user.email, user.groups.join(','), user.groups.join(',')); audit = _atnaAudit.default.construct.wrapInSyslog(audit); auditing.sendAuditEvent(audit, handleAuditResponse); return next(); } // Exports for testing only const _getEnabledAuthenticationTypesFromConfig = getEnabledAuthenticationTypesFromConfig; exports._getEnabledAuthenticationTypesFromConfig = _getEnabledAuthenticationTypesFromConfig; //# sourceMappingURL=authentication.js.map