775ac7b58c
you must login with an BTP account in order to see the app
455 lines
14 KiB
JavaScript
455 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
const constants = require('./constants');
|
|
const axios = require('axios');
|
|
const url = require('url');
|
|
const https = require('https');
|
|
|
|
// use environment variable DEBUG with value 'xssec:*' for trace/error messages
|
|
const debug = require('debug');
|
|
const debugTrace = debug('xssec:requests');
|
|
const debugError = debug('xssec:requests');
|
|
|
|
debugError.log = console.error.bind(console);
|
|
debugTrace.log = console.log.bind(console);
|
|
|
|
const IAS = "ias";
|
|
const XSUAA = "xsuaa";
|
|
const X_ZONE_ID_HEADER_NAME = "x-zid";
|
|
const CORRELATIONID_HEADER = "x-vcap-request-id";
|
|
|
|
const DEFAULT_TIMEOUT = 2000;
|
|
const DEFAULT_USER_TOKEN_TIMEOUT = 10 * 1000;
|
|
|
|
|
|
/**
|
|
* Converts form parameters object to an array of key value pairs.
|
|
* Parameters with array values are converted to multiple key-value pairs with the same key.
|
|
* Parameters with object values are converted to a key-value pair with the object value stringified to JSON.
|
|
* @param {*} params key-value object with type of values being string|Array|object
|
|
* @returns array of key-value pairs
|
|
*/
|
|
function toFormArray(params) {
|
|
return Object.entries(params)
|
|
.filter(([value]) => value != null)
|
|
.flatMap(([key, value]) => {
|
|
if(Array.isArray(value)) {
|
|
return value.map(arrayValue => [key, arrayValue]);
|
|
}
|
|
|
|
if(typeof value === 'object') {
|
|
return [[key, JSON.stringify(value)]];
|
|
}
|
|
|
|
return [[key, value]];
|
|
});
|
|
}
|
|
|
|
async function _requestToNetworkAXIOS(fnc, options, cb) {
|
|
if (debugTrace.enabled) {
|
|
let opt = JSON.parse(JSON.stringify(options));
|
|
if (opt.https && opt.https.key) {
|
|
opt.https.key = "<Private Key for MTLS>";
|
|
}
|
|
|
|
if (opt.form && opt.form.client_secret) {
|
|
opt.form.client_secret = "<ClientSecret>"
|
|
}
|
|
|
|
if (opt.form && opt.form.cert) {
|
|
opt.form.cert = "<Certificates>"
|
|
}
|
|
|
|
if (opt.form && opt.form.assertion) {
|
|
opt.form.assertion = "<Assertion>"
|
|
}
|
|
|
|
debugTrace(fnc + '::HTTP Call with %O', opt);
|
|
}
|
|
|
|
const axios_options = {
|
|
maxRedirects: 0, //no followRedirect
|
|
headers: options.headers,
|
|
url: options.url,
|
|
method: options.method,
|
|
timeout: options.timeout || DEFAULT_TIMEOUT
|
|
};
|
|
|
|
if(options.params) {
|
|
axios_options.params = options.params;
|
|
}
|
|
|
|
if (options.form) {
|
|
const formData = options.configType?.toLowerCase() === IAS ? toFormArray(options.form) : options.form;
|
|
axios_options.data = new url.URLSearchParams(formData).toString();
|
|
}
|
|
|
|
if (options.https) {
|
|
axios_options.httpsAgent = new https.Agent({
|
|
cert: options.https.certificate,
|
|
key: options.https.key
|
|
});
|
|
}
|
|
|
|
try {
|
|
const result = await axios(axios_options);
|
|
const json = result.data;
|
|
if (options._responseToken) {
|
|
cb(null, json[options._responseToken], json);
|
|
} else {
|
|
cb(null, json.id_token || json.access_token || json, json);
|
|
}
|
|
} catch (e) {
|
|
return cb(e);
|
|
}
|
|
}
|
|
|
|
let _requestToNetwork = _requestToNetworkAXIOS;
|
|
|
|
function validateParameters(serviceCredentials, cb) {
|
|
// input validation
|
|
if (!serviceCredentials) {
|
|
return new Error('Parameter serviceCredentials is missing but mandatory.');
|
|
}
|
|
if (!serviceCredentials.clientid) {
|
|
//client_secret will be checked later (only if really needed)
|
|
return new Error('Invalid service credentials: Missing clientid.');
|
|
}
|
|
if (!serviceCredentials.url) {
|
|
return new Error('Invalid service credentials: Missing url.');
|
|
}
|
|
if (!cb || typeof cb !== 'function') {
|
|
return new Error('No callback function provided.');
|
|
}
|
|
}
|
|
|
|
function buildSubdomain(serviceCredentials, subdomain) {
|
|
var urlWithCorrectSubdomain = serviceCredentials.url;
|
|
if (subdomain) {
|
|
var tokenSubdomain = subdomain;
|
|
var tokenRequestSubdomain = null;
|
|
var uaaUrl = url.parse(serviceCredentials.url);
|
|
if (uaaUrl.hostname.indexOf('.') === -1) {
|
|
tokenRequestSubdomain = null;
|
|
} else {
|
|
tokenRequestSubdomain = uaaUrl.hostname.substring(0, uaaUrl.hostname.indexOf('.'));
|
|
}
|
|
|
|
if (tokenSubdomain !== null && tokenRequestSubdomain != null && tokenSubdomain !== tokenRequestSubdomain) {
|
|
urlWithCorrectSubdomain = uaaUrl.protocol + "//" + tokenSubdomain + uaaUrl.host.substring(uaaUrl.host.indexOf('.'), uaaUrl.host.size);
|
|
}
|
|
|
|
if (serviceCredentials.certificate) {
|
|
urlWithCorrectSubdomain = urlWithCorrectSubdomain.replace(".authentication.", ".authentication.cert.")
|
|
}
|
|
} else if (serviceCredentials.certificate && serviceCredentials.certurl) {
|
|
urlWithCorrectSubdomain = serviceCredentials.certurl;
|
|
}
|
|
|
|
return urlWithCorrectSubdomain;
|
|
}
|
|
|
|
function appendAdditonalAttributes(options, additionalAttributes) {
|
|
if (additionalAttributes !== null) {
|
|
options.form.authorities = { "az_attr": additionalAttributes };
|
|
}
|
|
}
|
|
|
|
function DefaultHeaders(zoneId, type) {
|
|
var ret = {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'User-Agent': constants.USER_AGENT
|
|
};
|
|
|
|
if (zoneId && type !== IAS) {
|
|
ret[X_ZONE_ID_HEADER_NAME] = zoneId;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
function buildOptions(serviceCredentials, additionalAttributes, url, grantType, appTid, attributes) {
|
|
// jwt bearer flow
|
|
const path = attributes.configType === 'ias' ? '/oauth2/token' : '/oauth/token';
|
|
|
|
const options = {
|
|
method: 'POST',
|
|
url: url + path,
|
|
headers: DefaultHeaders(appTid, attributes.configType),
|
|
form: {
|
|
grant_type: grantType,
|
|
response_type: 'token',
|
|
client_id: serviceCredentials.clientid,
|
|
app_tid: appTid || serviceCredentials.app_tid
|
|
},
|
|
timeout: attributes.timeout,
|
|
configType: attributes.configType
|
|
};
|
|
|
|
if (attributes.resource) {
|
|
options.form.resource = attributes.resource;
|
|
}
|
|
|
|
if (serviceCredentials.certificate) {
|
|
options.https = {
|
|
key: serviceCredentials.key,
|
|
certificate: serviceCredentials.certificate
|
|
}
|
|
} else {
|
|
//make sure we have a client_secret set here
|
|
if (!serviceCredentials.clientsecret) {
|
|
throw new Error('Invalid config: Missing clientsecret.');
|
|
}
|
|
|
|
options.form.client_secret = serviceCredentials.clientsecret;
|
|
}
|
|
|
|
appendAdditonalAttributes(options, additionalAttributes);
|
|
|
|
if (attributes.correlationId) {
|
|
options.headers[CORRELATIONID_HEADER] = attributes.correlationId;
|
|
}
|
|
|
|
if (attributes.scopes) {
|
|
options.form.scope = attributes.scopes;
|
|
}
|
|
|
|
if (attributes.username) {
|
|
options.form.username = attributes.username;
|
|
options.form.password = attributes.password;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
module.exports.requestOpenIDConfiguration = function (serviceCredentialsUrl, attributes, cb) {
|
|
var options = {
|
|
method: 'GET',
|
|
followRedirect: false,
|
|
timeout: DEFAULT_TIMEOUT,
|
|
url: serviceCredentialsUrl + '/.well-known/openid-configuration',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'User-Agent': constants.USER_AGENT
|
|
}
|
|
};
|
|
|
|
if (attributes) {
|
|
if (attributes.correlationId) {
|
|
options.headers[CORRELATIONID_HEADER] = attributes.correlationId;
|
|
}
|
|
}
|
|
|
|
return _requestToNetwork(".well-known", options, cb);
|
|
}
|
|
|
|
module.exports.fetchOIDCKey = function (serviceCredentialsUrl, params = {}, cb) {
|
|
var options = {
|
|
method: 'GET',
|
|
url: serviceCredentialsUrl,
|
|
followRedirect: false,
|
|
timeout: DEFAULT_TIMEOUT,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'User-Agent': constants.USER_AGENT
|
|
}
|
|
};
|
|
|
|
params.client_id && (options.headers["x-client_id"] = params.client_id);
|
|
params.app_tid && (options.headers["x-app_tid"] = params.app_tid);
|
|
params.azp && (options.headers["x-azp"] = params.azp);
|
|
params.correlationId && (options.headers[CORRELATIONID_HEADER] = params.correlationId);
|
|
|
|
return _requestToNetwork(".oidc-jkws", options, cb);
|
|
}
|
|
|
|
function getServiceCredentials(config) {
|
|
if (config.credentials) {
|
|
return config.credentials;
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
function getAttributes(config, defaultTimeout, maxTimeout) {
|
|
if (config.credentials) {
|
|
let timeout = config.timeout || defaultTimeout;
|
|
if (timeout > maxTimeout) {
|
|
timeout = maxTimeout;
|
|
} else if (timeout < DEFAULT_TIMEOUT) {
|
|
timeout = DEFAULT_TIMEOUT;
|
|
}
|
|
|
|
if (config.type === "IAS") {
|
|
config.type = IAS;
|
|
}
|
|
|
|
return {
|
|
configType: config.type || XSUAA,
|
|
scopes: config.scopes,
|
|
correlationId: config.correlationId,
|
|
timeout: timeout,
|
|
username: config.username,
|
|
password: config.password,
|
|
resource: config.resource
|
|
};
|
|
}
|
|
|
|
return {
|
|
timeout: defaultTimeout,
|
|
configType: XSUAA
|
|
};
|
|
}
|
|
|
|
module.exports.requestUserToken = function (appToken, config, additionalAttributes, scopes, subdomain, zoneId, cb) {
|
|
//make it backward-compatible (where zoneId is not provided at all)
|
|
if (typeof zoneId === 'function') {
|
|
cb = zoneId;
|
|
zoneId = null;
|
|
}
|
|
|
|
const serviceCredentials = getServiceCredentials(config);
|
|
const attributes = getAttributes(config, DEFAULT_USER_TOKEN_TIMEOUT, 10 * 1000);
|
|
|
|
var error = validateParameters(serviceCredentials, cb);
|
|
if (error) {
|
|
error.statuscode = 500;
|
|
return cb(error, null);
|
|
}
|
|
|
|
const urlWithCorrectSubdomain = buildSubdomain(serviceCredentials, subdomain);
|
|
|
|
try {
|
|
// jwt bearer flow
|
|
var options = buildOptions(serviceCredentials,
|
|
additionalAttributes,
|
|
urlWithCorrectSubdomain,
|
|
'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
zoneId,
|
|
attributes);
|
|
|
|
//add Assertion
|
|
options.form.assertion = appToken;
|
|
|
|
if (scopes !== null) {
|
|
options.form.scope = scopes;
|
|
}
|
|
|
|
return _requestToNetwork("requestUserToken", options, cb);
|
|
} catch (e) {
|
|
//the verification of the serviceCredentials fails
|
|
e.statuscode = 500;
|
|
return cb(e);
|
|
}
|
|
}
|
|
|
|
module.exports.requestPasswordUserToken = function (subdomain, config, additionalAttributes, cb) {
|
|
const serviceCredentials = getServiceCredentials(config);
|
|
const attributes = getAttributes(config, DEFAULT_USER_TOKEN_TIMEOUT, 10 * 1000);
|
|
|
|
// input validation
|
|
const error = validateParameters(serviceCredentials, cb);
|
|
if (error) {
|
|
error.statuscode = 500;
|
|
return cb(error, null);
|
|
}
|
|
|
|
// adapt subdomain in service url, if necessary
|
|
const urlWithCorrectSubdomain = buildSubdomain(serviceCredentials, subdomain);
|
|
|
|
try {
|
|
const options = buildOptions(serviceCredentials,
|
|
additionalAttributes,
|
|
urlWithCorrectSubdomain,
|
|
'password',
|
|
null,
|
|
attributes);
|
|
|
|
appendAdditonalAttributes(options, additionalAttributes);
|
|
|
|
options._responseToken = 'access_token';
|
|
return _requestToNetwork("requestPasswordUserToken", options, cb);
|
|
} catch (e) {
|
|
//the verification of the serviceCredentials fails
|
|
e.statuscode = 500;
|
|
return cb(e);
|
|
}
|
|
};
|
|
|
|
module.exports.requestClientCredentialsToken = function (subdomain, config, additionalAttributes, zoneId, cb) {
|
|
//make it backward-compatible (where zoneId is not provided at all)
|
|
if (typeof zoneId === 'function') {
|
|
cb = zoneId;
|
|
zoneId = null;
|
|
}
|
|
|
|
const serviceCredentials = getServiceCredentials(config);
|
|
const attributes = getAttributes(config, DEFAULT_TIMEOUT, 5 * 1000);
|
|
|
|
// input validation
|
|
const error = validateParameters(serviceCredentials, cb);
|
|
if (error) {
|
|
error.statuscode = 500;
|
|
return cb(error, null);
|
|
}
|
|
|
|
// adapt subdomain in service url, if necessary
|
|
const urlWithCorrectSubdomain = buildSubdomain(serviceCredentials, subdomain);
|
|
|
|
try {
|
|
const options = buildOptions(serviceCredentials,
|
|
additionalAttributes,
|
|
urlWithCorrectSubdomain,
|
|
'client_credentials',
|
|
zoneId,
|
|
attributes);
|
|
|
|
appendAdditonalAttributes(options, additionalAttributes);
|
|
return _requestToNetwork("requestClientCredentialsToken", options, cb);
|
|
} catch (e) {
|
|
//the verification of the serviceCredentials fails
|
|
e.statuscode = 500;
|
|
return cb(e);
|
|
}
|
|
};
|
|
|
|
module.exports.fetchKeyFromXSUAA = async function (tokenKeyUrl, zid, attributes, cb) {
|
|
const options = {
|
|
headers: {
|
|
"User-Agent": constants.USER_AGENT
|
|
},
|
|
method: "GET",
|
|
url: tokenKeyUrl,
|
|
followRedirect: false,
|
|
timeout: DEFAULT_TIMEOUT
|
|
};
|
|
|
|
if (zid) {
|
|
options.params = { zid };
|
|
}
|
|
|
|
if (attributes) {
|
|
if (attributes.correlationId) {
|
|
options.headers[CORRELATIONID_HEADER] = attributes.correlationId;
|
|
}
|
|
}
|
|
|
|
_requestToNetwork("fetchKeyFromXSUAA", options, cb);
|
|
}
|
|
|
|
module.exports.__patchNetwork = new function () {
|
|
var oldrequestToXSUAA = _requestToNetwork;
|
|
|
|
this.patch = function (fnc) {
|
|
if (typeof fnc === 'function') {
|
|
debugTrace("patch XSUAA communication to another function");
|
|
_requestToNetwork = fnc;
|
|
} else {
|
|
if (_requestToNetwork !== oldrequestToXSUAA) {
|
|
debugTrace("patch XSUAA communication to original function");
|
|
_requestToNetwork = oldrequestToXSUAA;
|
|
}
|
|
}
|
|
}
|
|
}
|