@sap/xssec
Advanced tools
Comparing version 3.1.1 to 3.1.2
@@ -9,2 +9,5 @@ 'use strict'; | ||
exports.JWTStrategy = passportStrategy.JWTStrategy; | ||
exports.requests = require('./requests'); | ||
exports.requests = require('./requests'); | ||
exports.config = require('./xssec.config'); | ||
exports.TokenInfo = require('./tokeninfo'); |
@@ -16,17 +16,50 @@ 'use strict'; | ||
module.exports.requestUserToken = function (appToken, serviceCredentials, additionalAttributes, scopes, subdomain, cb) { | ||
const X_ZONE_ID_HEADER_NAME = "x-zid"; | ||
function _requestToXSUAA(fnc, options, retryOptions, cb) { | ||
debugTrace(fnc + '::HTTP Call with %O', options); | ||
request.post( | ||
options, | ||
function (error, response, body) { | ||
if (error) { | ||
if (error.code === 'ETIMEDOUT' && error.connect === true) { | ||
debugError( fnc + ': HTTP connection timeout.'); | ||
} | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error, null); | ||
} | ||
if (response.statusCode !== 200) { | ||
return cb(new Error(response.statusCode + " - " + body)); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
} catch (e) { | ||
return cb(e, null); | ||
} | ||
return cb(null, json.access_token, json) | ||
} | ||
); | ||
}; | ||
function validateParameters(serviceCredentials, cb) { | ||
// input validation | ||
if (!serviceCredentials) { | ||
var error = new Error('Parameter serviceCredentials is missing but mandatory.'); | ||
return cb(error, null); | ||
return new Error('Parameter serviceCredentials is missing but mandatory.'); | ||
} | ||
if (!serviceCredentials.clientid || !serviceCredentials.clientsecret) { | ||
var error = new Error('Invalid service credentials: Missing clientid/clientsecret.'); | ||
return cb(error, null); | ||
return new Error('Invalid service credentials: Missing clientid/clientsecret.'); | ||
} | ||
if (!serviceCredentials.url) { | ||
var error = new Error('Invalid service credentials: Missing url.'); | ||
return cb(error, null); | ||
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; | ||
@@ -47,125 +80,103 @@ if (subdomain) { | ||
return urlWithCorrectSubdomain; | ||
} | ||
function appendAdditonalAttribites(options, additionalAttributes) { | ||
if (additionalAttributes !== null) { | ||
var authorities = { "az_attr" : additionalAttributes }; | ||
options.form.authorities = authorities; | ||
} | ||
} | ||
function DefaultHeaders(zoneId) { | ||
var ret = { | ||
'Accept': 'application/json', | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
'User-Agent': constants.USER_AGENT | ||
}; | ||
if(zoneId) { | ||
ret[X_ZONE_ID_HEADER_NAME] = zoneId; | ||
} | ||
return ret; | ||
} | ||
function buildOptions(serviceCredentials, additionalAttributes, url, grantType, zoneId, timeout) { | ||
// jwt bearer flow | ||
var options = { | ||
url: urlWithCorrectSubdomain + '/oauth/token', | ||
headers: { | ||
'Accept': 'application/json', | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
'User-Agent': constants.USER_AGENT | ||
}, | ||
url: url + '/oauth/token', | ||
headers: DefaultHeaders(zoneId), | ||
form: { | ||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
grant_type: grantType, | ||
response_type: 'token', | ||
client_id: serviceCredentials.clientid, | ||
client_secret: serviceCredentials.clientsecret, | ||
assertion: appToken | ||
client_secret: serviceCredentials.clientsecret | ||
}, | ||
timeout: 10*1000 | ||
timeout: timeout | ||
}; | ||
appendAdditonalAttribites(options, additionalAttributes); | ||
return options; | ||
} | ||
module.exports.requestUserToken = function (appToken, serviceCredentials, additionalAttributes, scopes, subdomain, zoneId, cb) { | ||
//make it backward-compatible (where zoneId is not provided at all) | ||
if (typeof zoneId === 'function') { | ||
cb = zoneId; | ||
zoneId = null; | ||
} | ||
var error = validateParameters(serviceCredentials, cb); | ||
if(error) { | ||
return cb(error, null); | ||
} | ||
var urlWithCorrectSubdomain = buildSubdomain(serviceCredentials, subdomain); | ||
// jwt bearer flow | ||
var options = buildOptions(serviceCredentials, | ||
additionalAttributes, | ||
urlWithCorrectSubdomain, | ||
'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
zoneId, | ||
10*1000); | ||
//add Assertion | ||
options.form.assertion = appToken; | ||
if (scopes !== null) { | ||
options.form.scope = scopes; | ||
} | ||
if (additionalAttributes !== null) { | ||
var authorities = { "az_attr" : additionalAttributes }; | ||
options.form.authorities = authorities; | ||
} | ||
debugTrace('requestUserToken::HTTP Call with %O', options); | ||
request.post( | ||
options, | ||
function (error, response, body) { | ||
if (error) { | ||
if (error.code === 'ETIMEDOUT' && error.connect === true) { | ||
debugError('requestToken: HTTP connection timeout.'); | ||
} | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error, null); | ||
} | ||
if (response.statusCode !== 200) { | ||
return cb(new Error(response.statusCode + " - " + body)); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
} catch (e) { | ||
return cb(e, null); | ||
} | ||
return cb(null, json.access_token, json) | ||
} | ||
); | ||
return _requestToXSUAA("requestUserToken", options, false, cb); | ||
} | ||
module.exports.requestClientCredentialsToken = function (subdomain, serviceCredentials, additionalAttributes, cb) { | ||
module.exports.requestClientCredentialsToken = function (subdomain, serviceCredentials, additionalAttributes, zoneId, cb) { | ||
//make it backward-compatible (where zoneId is not provided at all) | ||
if (typeof zoneId === 'function') { | ||
cb = zoneId; | ||
zoneId = null; | ||
} | ||
// input validation | ||
if (!serviceCredentials) { | ||
var error = new Error('Parameter serviceCredentials is missing but mandatory.'); | ||
var error = validateParameters(serviceCredentials, cb); | ||
if(error) { | ||
return cb(error, null); | ||
} | ||
if (!serviceCredentials.clientid || !serviceCredentials.clientsecret) { | ||
var error = new Error('Invalid service credentials: Missing clientid/clientsecret.'); | ||
return cb(error, null); | ||
} | ||
if (!serviceCredentials.url) { | ||
var error = new Error('Invalid service credentials: Missing url.'); | ||
return cb(error, null); | ||
} | ||
// adapt subdomain in service url, if necessary | ||
var urlWithCorrectSubdomain = serviceCredentials.url; | ||
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); | ||
} | ||
// client credentials flow | ||
var options = { | ||
url: urlWithCorrectSubdomain + '/oauth/token', | ||
headers: { | ||
'Accept': 'application/json', | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
'User-Agent': constants.USER_AGENT | ||
}, | ||
form: { | ||
grant_type: 'client_credentials', | ||
response_type: 'token', | ||
client_id: serviceCredentials.clientid, | ||
client_secret: serviceCredentials.clientsecret | ||
}, | ||
timeout: 2 * 1000 | ||
}; | ||
if (additionalAttributes !== null) { | ||
var authorities = { "az_attr": additionalAttributes }; | ||
options.url = options.url + "?authorities=" + encodeURIComponent(JSON.stringify(authorities)); | ||
} | ||
debugTrace('requestClientCredentialsToken::HTTP Call with %O', options); | ||
requestRetry.post( | ||
options, | ||
function (error, response, body) { | ||
if (error) { | ||
if (error.code === 'ETIMEDOUT' && error.connect === true) { | ||
debugError('requestToken: HTTP connection timeout.'); | ||
} | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error); | ||
} | ||
if (response.statusCode !== 200) { | ||
return cb(new Error(response.statusCode + " - " + body)); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
} catch (e) { | ||
return cb(e); | ||
} | ||
return cb(null, json.access_token, json); | ||
} | ||
); | ||
var urlWithCorrectSubdomain = buildSubdomain(serviceCredentials, subdomain); | ||
var options = buildOptions(serviceCredentials, | ||
additionalAttributes, | ||
urlWithCorrectSubdomain, | ||
'client_credentials', | ||
zoneId, | ||
2*1000); | ||
appendAdditonalAttribites(options, additionalAttributes); | ||
return _requestToXSUAA("requestClientCredentialsToken", options, false, cb); | ||
}; | ||
@@ -172,0 +183,0 @@ |
@@ -26,2 +26,5 @@ 'use strict'; | ||
} | ||
} else { | ||
errobj = new jwt.JsonWebTokenError("jwt undefined"); | ||
errobj.statuscode = 400; | ||
} | ||
@@ -87,2 +90,10 @@ } | ||
this.getZoneId = function() { | ||
if(this.isTokenIssuedByXSUAA()) { | ||
return payload.zid; | ||
} else { | ||
return payload.zone_uuid; | ||
} | ||
} | ||
this.getClientId = function() { | ||
@@ -89,0 +100,0 @@ var azp = payload.azp; |
'use strict'; | ||
const util = require('util'); | ||
@@ -10,4 +9,8 @@ | ||
const jwt = require('jsonwebtoken'); | ||
const tokenInfo = require('./tokeninfo') | ||
const TokenInfo = require('./tokeninfo'); | ||
const TokenExchanger = require('./tokenexchanger'); | ||
const requests = require('./requests'); | ||
const DOT = "."; | ||
@@ -65,3 +68,3 @@ | ||
this.isForeignMode = function() { | ||
this.isForeignMode = function () { | ||
return foreignMode; | ||
@@ -71,3 +74,3 @@ } | ||
function validateSameClientId(cidFromToken) { | ||
if(!cidFromToken || !clientId) { | ||
if (!cidFromToken || !clientId) { | ||
return false; | ||
@@ -110,3 +113,3 @@ } | ||
if(ret === null) { | ||
if (ret === null) { | ||
foreignMode = true; | ||
@@ -117,3 +120,3 @@ } | ||
this.getListOfAudiencesFromToken = function(aud, scopes, cid) { | ||
this.getListOfAudiencesFromToken = function (aud, scopes, cid) { | ||
return extractAudiencesFromToken(aud || [], scopes || [], cid); | ||
@@ -141,7 +144,7 @@ } | ||
if (audiences.length == 0) { | ||
for(var i=0;i < scopes.length;++i) { | ||
for (var i = 0; i < scopes.length; ++i) { | ||
var scope = scopes[i]; | ||
if (scope.indexOf(DOT) >-1) { | ||
if (scope.indexOf(DOT) > -1) { | ||
var aud = scope.substring(0, scope.indexOf(DOT)).trim(); | ||
if(aud && !audiences.includes(aud)) { | ||
if (aud && !audiences.includes(aud)) { | ||
audiences.push(aud); | ||
@@ -152,7 +155,7 @@ } | ||
} | ||
if(cid && audiences.indexOf(cid) === -1) { | ||
if (cid && audiences.indexOf(cid) === -1) { | ||
audiences.push(cid); | ||
} | ||
return audiences; | ||
@@ -167,6 +170,6 @@ } | ||
function JwtTokenValidator(verificationKey, configArray) { | ||
function JwtTokenValidator(verificationKey, configArray, serviceCredentials) { | ||
var foreignMode = false; | ||
this.isForeignMode = function() { | ||
this.isForeignMode = function () { | ||
return foreignMode; | ||
@@ -184,51 +187,58 @@ } | ||
var token = new tokenInfo(accessToken); | ||
let tokeninfo = new TokenExchanger(serviceCredentials); | ||
token.verify(verificationKey.getCallback(token), | ||
function (err, token) { | ||
var decodedToken = token.getPayload(); | ||
if (err) { | ||
debugError(err.statuscode); | ||
debugError(err.message); | ||
debugError(err.stack); | ||
return cb(err, token); | ||
return tokeninfo.prepareToken(accessToken, | ||
function (errorString, token) { | ||
if (errorString) { | ||
return returnError(401, errorString); | ||
} | ||
if(!token.getClientId()) { | ||
return returnError(400, 'Client Id not contained in access token. Giving up!'); | ||
} | ||
return token.verify(verificationKey.getCallback(token), | ||
function (err, token) { | ||
var decodedToken = token.getPayload(); | ||
if (!decodedToken.zid) { | ||
return returnError(400, 'Identity Zone not contained in access token. Giving up!'); | ||
} | ||
if (err) { | ||
debugError(err.statuscode); | ||
debugError(err.message); | ||
debugError(err.stack); | ||
return cb(err, token); | ||
} | ||
var audienceValidator = new JwtAudienceValidator(configArray[0].clientid); | ||
if (configArray[0].xsappname) { | ||
audienceValidator.configureTrustedClientId(configArray[0].xsappname); | ||
} | ||
if (!token.getClientId()) { | ||
return returnError(400, 'Client Id not contained in access token. Giving up!'); | ||
} | ||
for(var i=1;i<configArray.length;++i) { | ||
if(configArray[i]) { | ||
if (configArray[i].clientid) { | ||
audienceValidator.configureTrustedClientId(configArray[i].clientid); | ||
if (!decodedToken.zid) { | ||
return returnError(400, 'Identity Zone not contained in access token. Giving up!'); | ||
} | ||
if (configArray[i].xsappname) { | ||
audienceValidator.configureTrustedClientId(configArray[i].xsappname); | ||
var audienceValidator = new JwtAudienceValidator(configArray[0].clientid); | ||
if (configArray[0].xsappname) { | ||
audienceValidator.configureTrustedClientId(configArray[0].xsappname); | ||
} | ||
} | ||
} | ||
var valid_result = audienceValidator.validateToken(token.getAudiencesArray(), decodedToken.scope, decodedToken.cid); | ||
if (!valid_result.isValid()) { | ||
return returnError(401, valid_result.getErrorDescription()); | ||
} | ||
for (var i = 1; i < configArray.length; ++i) { | ||
if (configArray[i]) { | ||
if (configArray[i].clientid) { | ||
audienceValidator.configureTrustedClientId(configArray[i].clientid); | ||
} | ||
if (configArray[i].xsappname) { | ||
audienceValidator.configureTrustedClientId(configArray[i].xsappname); | ||
} | ||
} | ||
} | ||
if(configArray[0].clientid !== decodedToken.cid) { | ||
foreignMode = audienceValidator.isForeignMode(); | ||
} | ||
var valid_result = audienceValidator.validateToken(token.getAudiencesArray(), decodedToken.scope, decodedToken.cid); | ||
if (!valid_result.isValid()) { | ||
return returnError(401, valid_result.getErrorDescription()); | ||
} | ||
cb(null, token); | ||
} | ||
); | ||
if (configArray[0].clientid !== decodedToken.cid) { | ||
foreignMode = audienceValidator.isForeignMode(); | ||
} | ||
cb(null, token); | ||
} | ||
); | ||
}) | ||
}; | ||
@@ -235,0 +245,0 @@ }; |
@@ -425,3 +425,3 @@ 'use strict'; | ||
var verificationKey = new VerificationKey(config); | ||
var jwtValidator = new JwtTokenValidator(verificationKey, configArr); | ||
var jwtValidator = new JwtTokenValidator(verificationKey, configArr, config); | ||
@@ -428,0 +428,0 @@ //Now validate the tokens |
{ | ||
"name": "@sap/xssec", | ||
"version": "3.1.1", | ||
"version": "3.1.2", | ||
"description": "XS Advanced Container Security API for node.js", | ||
@@ -5,0 +5,0 @@ "main": "./lib", |
@@ -172,3 +172,7 @@ @sap/xssec: XS Advanced Container Security API for node.js | ||
``` | ||
### Support for automatic IAS to XSUAA token conversion | ||
Since verison 3.1.2 it is supported to automatically exchange an incoming IAS token with an XSUAA token, so the token contains scopes like XSUAA applications expect. | ||
For details have a look [here](doc/IAStoXSUAA.md). | ||
### Test Usage without having an Access Token | ||
@@ -326,2 +330,3 @@ | ||
also have a look on how to initiate the [token flows](doc/TokenFlows.md) directly | ||
### hasAttributes | ||
@@ -328,0 +333,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
94925
15
1322
390