atlassian-connect-express
Version:
Library for building Atlassian Add-ons on top of Express
272 lines (231 loc) • 7.27 kB
JavaScript
const fs = require("fs");
const JSON5 = require("json5");
const _ = require("lodash");
const jwt = require("atlassian-jwt");
const packageVersion = require("../../package.json").version;
const URI = require("urijs");
const querystring = require("querystring");
const JWT_PARAM = "jwt";
const TOKEN_KEY_PARAM = "acpt";
const TOKEN_KEY_HEADER = `X-${TOKEN_KEY_PARAM}`;
const AUTH_HEADER = "authorization"; // the header name appears as lower-case
const utils = {};
utils.unescapelf = function unescapelf(str) {
return str ? str.replace(/\\n/g, "\n") : str;
};
utils.replaceAll = function replaceAll(settings, values) {
Object.keys(settings).forEach(k => {
const setting = settings[k];
if (_.isString(setting)) {
settings[k] = utils.replaceStr(setting, values);
} else if (_.isObject(setting)) {
utils.replaceAll(setting, values);
}
});
return settings;
};
utils.replaceStr = function replaceStr(setting, values) {
return setting.replace(/\$([a-zA-Z]\w*)/g, ($0, $1) => {
return values[$1] || $0;
});
};
utils.loadFile = function loadFile(path) {
return fs.existsSync(path) ? fs.readFileSync(path).toString() : null;
};
utils.loadJSON = function loadConfig(path) {
let data = {};
try {
data = utils.loadFile(path);
} catch (e) {
// do nothing
}
return data ? JSON5.parse(data) : {};
};
utils.replaceTokensInJson = function (obj, from, to) {
for (const i in obj) {
if (typeof obj[i] === "object") {
obj[i] = utils.replaceTokensInJson(obj[i], from, to);
} else {
const re = new RegExp(from);
if (re.test(obj[i])) {
obj[i] = obj[i].replace(from, to);
}
}
}
return obj;
};
utils.merge = function (obj, override) {
return _.merge(obj, override);
};
utils.packageVersion = function () {
return packageVersion;
};
utils.checkNotNull = function (thing, name) {
if (_.isNull(thing)) {
throw new Error(`${name} must be defined`);
}
};
utils.checkNotUndefined = function (thing, name) {
if (_.isNil(thing)) {
throw new Error(`${name} must be defined`);
}
};
utils.extractJwtFromRequest = function (addon, req) {
const tokenInQuery = req.query[JWT_PARAM];
// JWT is missing in query and we don't have a valid body.
if (!tokenInQuery && !req.body) {
addon.logger.warn(
`Cannot find JWT token in query parameters. Please include body-parser middleware and parse the urlencoded body (See https://github.com/expressjs/body-parser) if the add-on is rendering in POST mode. Otherwise please ensure the ${JWT_PARAM} parameter is presented in query.`
);
return;
}
// JWT appears in both parameter and body will result query hash being invalid.
const tokenInBody = req.body[JWT_PARAM];
if (tokenInQuery && tokenInBody) {
addon.logger.warn(
"JWT token can only appear in either query parameter or request body."
);
return;
}
let token = tokenInQuery || tokenInBody;
// if there was no token in the query-string then fall back to checking the Authorization header
const authHeader = req.headers[AUTH_HEADER];
if (authHeader && authHeader.indexOf("JWT ") === 0) {
if (token) {
const foundIn = tokenInQuery ? "query" : "request body";
addon.logger.warn(
`JWT token found in ${foundIn} and in header: using ${foundIn} value.`
);
} else {
token = authHeader.substring(4);
}
}
// TODO: Remove when we discontinue the old token middleware
if (!token) {
token = req.query[TOKEN_KEY_PARAM] || req.header(TOKEN_KEY_HEADER);
}
return token;
};
utils.validateQshFromRequest = function (
verifiedClaims,
request,
addon,
skipQshVerification = false
) {
const jwtRequest = jwt.fromExpressRequest(request);
if (!skipQshVerification && verifiedClaims.qsh) {
let expectedHash = jwt.createQueryStringHash(
jwtRequest,
false,
addon.config.baseUrl.href
);
let signatureHashVerified = verifiedClaims.qsh === expectedHash;
if (!signatureHashVerified) {
let canonicalRequest = jwt.createCanonicalRequest(
jwtRequest,
false,
addon.config.baseUrl.href
);
// If that didn't verify, it might be a post/put - check the request body too
expectedHash = jwt.createQueryStringHash(
jwtRequest,
true,
addon.config.baseUrl.href
);
signatureHashVerified = verifiedClaims.qsh === expectedHash;
if (!signatureHashVerified) {
canonicalRequest = jwt.createCanonicalRequest(
jwtRequest,
true,
addon.config.baseUrl.href
);
// Send the error message for the first verification - it's 90% more likely to be the one we want.
addon.logger.error(
`Auth failure: Query hash mismatch: Received: "${verifiedClaims.qsh}" but calculated "${expectedHash}". Canonical query was: "${canonicalRequest}`
);
return false;
}
}
}
return true;
};
utils.isJiraDevBaseUrl = function (baseUrl) {
const host = new URI(baseUrl).hostname();
return host.endsWith(".jira-dev.com");
};
utils.isFedRAMPStagingHost = function (baseUrl) {
const host = new URI(baseUrl).hostname();
return host.endsWith(".atlassian-stg-fedm.net");
};
utils.isFedRAMPProductionHost = function (baseUrl) {
const host = new URI(baseUrl).hostname();
return host.endsWith(".atlassian-us-gov-mod.net");
};
utils.isProductionHost = function (baseUrl) {
const host = new URI(baseUrl).hostname();
return host.endsWith(".atlassian.net") || host.endsWith(".jira.com");
};
utils.isSandboxBaseUrl = function (baseUrl) {
const host = new URI(baseUrl).hostname();
return host.endsWith("atlassian-fex.net");
};
utils.modifyArgs = function (
addon,
options,
augmentHeaders,
callback,
hostBaseUrl
) {
const args = [];
if (_.isString(options)) {
options = { uri: options };
}
if (options.url) {
options.uri = options.url;
delete options.url;
}
if (options.form) {
options.multipartFormData = options.form;
delete options.form;
addon.logger.warn(
"options.form is deprecated: please use options.multipartFormData"
);
}
if (options.urlEncodedFormData) {
options.form = options.urlEncodedFormData;
delete options.urlEncodedFormData;
}
let originalUri = options.uri;
const targetUri = new URI(originalUri);
const hostBaseUri = new URI(hostBaseUrl);
if (options.qs) {
targetUri.query(querystring.encode(options.qs));
originalUri = targetUri.toString();
delete options.qs;
}
if (!targetUri.origin()) {
targetUri.origin(hostBaseUri.origin());
const newPath = URI.joinPaths(hostBaseUri.path(), targetUri.path());
targetUri.path(newPath.path());
}
options.uri = targetUri.toString();
args.push(options);
if (targetUri.origin() === hostBaseUri.origin()) {
if (!options.headers) {
options.headers = {};
}
if (augmentHeaders) {
augmentHeaders(options.headers, originalUri);
}
options.jar = false;
}
if (callback) {
args.push(callback);
}
return args;
};
utils.isUrlAbsolute = function (url) {
const isAbsolute = new RegExp("^([a-z]+://|//)", "i");
return isAbsolute.test(url);
};
module.exports = utils;