openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
248 lines (181 loc) • 8.62 kB
JavaScript
;
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