SAP-BTP-Spielwiese/app1/node_modules/@sap/xssec/lib/requests.js

456 lines
14 KiB
JavaScript
Raw Normal View History

'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;
}
}
}
}