@sap/xssec
Advanced tools
Comparing version 3.1.2 to 3.2.0
# Change Log | ||
All notable changes to this project will be documented in this file. | ||
## 3.1.2 - 2020-03-01 | ||
- Feature: Support for IAS to XSUAA token exchange ([more details](doc/IAStoXSUAA.md)) | ||
- Feature: Support for ZoneID enabled token flows ([more details](doc/TokenFlows.md)) | ||
## 3.1.1 - 2020-02-11 | ||
@@ -5,0 +10,0 @@ - Bugfix: Tokenexchange with additional attributes may result in a wrong formatted url |
@@ -12,2 +12,4 @@ 'use strict'; | ||
exports.config = require('./xssec.config'); | ||
exports.TokenInfo = require('./tokeninfo'); | ||
exports.TokenInfo = require('./tokeninfo'); | ||
exports.errors = require('./errors'); |
'use strict'; | ||
const constants = require('./constants'); | ||
const LRU = require('lru-cache'); | ||
const nodeRSA = require('node-rsa'); | ||
const validUrl = require('valid-url'); | ||
@@ -12,2 +12,3 @@ | ||
const constants = require('./constants'); | ||
const requests = require('./requests'); | ||
@@ -80,2 +81,87 @@ | ||
function createPublicKeyFromJWKS(json) { | ||
if (json.kty !== "RSA") { | ||
throw new Error("KTY '" + json.kty + "' not supported"); | ||
} | ||
const modulus = Buffer.from(json.n, 'base64'); | ||
const exponent = Buffer.from(json.e, 'base64'); | ||
const pubKey = new nodeRSA().importKey({ n: modulus, e: exponent }, 'components-public'); | ||
return pubKey.exportKey('pkcs8-public-pem'); | ||
} | ||
KeyCache.prototype.getWellKnownFromOIDC = function(serviceUrl, cb) { | ||
var cacheKey = serviceUrl + ".well_known."; | ||
var tmpResult = this.lruCache.get(cacheKey); | ||
if(tmpResult) { | ||
cb(null, tmpResult); | ||
} else { | ||
var self = this; | ||
requests.requestOpenIDConfiguration(serviceUrl, function(err, result) { | ||
if(err) { | ||
return cb(err); | ||
} | ||
self.addKey(cacheKey, result); | ||
cb(null, result); | ||
}); | ||
} | ||
} | ||
KeyCache.prototype.getKeyOIDC = function(serviceUrl, keyId, cb) { | ||
var self = this; | ||
this.getWellKnownFromOIDC(serviceUrl, function(err, result) { | ||
if(err) { | ||
return cb(err); | ||
} | ||
let tokenUrl = result.jwks_uri; | ||
let tmpResult = self.lruCache.get(tokenUrl + keyId); | ||
if(tmpResult) { | ||
//found in Cache | ||
return cb(null, tmpResult); | ||
} | ||
requests.fetchOIDCKey(tokenUrl, function(err, result) { | ||
if(err) { | ||
return cb(err); | ||
} | ||
//add all Keys to cache | ||
for(var i=0;i<result.keys.length;++i) { | ||
var key = result.keys[i]; | ||
try { | ||
self.addKey(tokenUrl + key.kid, createPublicKeyFromJWKS(key)); | ||
} catch(e) { | ||
debugError(e); | ||
} | ||
} | ||
var cacheKey = tokenUrl + keyId; | ||
var tmpResult = self.lruCache.get(cacheKey); | ||
if(tmpResult) { | ||
cb(null, tmpResult); | ||
} else { | ||
cb(new Error("Verfication Key not found")); | ||
} | ||
}); | ||
}); | ||
requests.requestOpenIDConfiguration(serviceUrl, function(err, result) { | ||
if(err) { | ||
return cb(err); | ||
} | ||
var tokenUrl = result.jwks_uri; | ||
}); | ||
} | ||
KeyCache.prototype.getKey = function getKey(tokenKeyUrl, keyId, zid, cb) { | ||
@@ -137,72 +223,3 @@ var self = this; | ||
}); | ||
request | ||
.get( | ||
options, | ||
function(err, response, body) { | ||
if (response) { | ||
debugTrace('Finished after number of requests attempts: ' + response.attempts); | ||
} | ||
if (err) { | ||
if (err.code === 'ETIMEDOUT' && err.connect === true) { | ||
debugError('getKey: HTTP connection timeout.'); | ||
} | ||
debugError('An error occurred when reading the token keys from ' | ||
+ options.url | ||
+ ': ' | ||
+ err.message | ||
+ err.stack); | ||
var error = new Error( | ||
'An error occurred when reading the token keys from ' | ||
+ options.url + ': ' | ||
+ err.message + err.stack); | ||
return process.nextTick(function() { | ||
cb(error, null); | ||
}); | ||
} | ||
if (response.statusCode !== 200) { | ||
debugTrace('Call was not successful. Error Code: ' | ||
+ response.statusCode); | ||
var error = new Error( | ||
'Call was not successful. Error Code: ' | ||
+ response.statusCode); | ||
return process.nextTick(function() { | ||
cb(error, null); | ||
}); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
for (var i = 0; i < json.keys.length; i++) { | ||
// Note: The following code removes line | ||
// breaks before adding the key to the cache | ||
self.addKey(tokenKeyUrl + json.keys[i].kid, | ||
json.keys[i].value.replace( | ||
/(\r\n|\n|\r)/gm, '')); | ||
} | ||
} catch (e) { | ||
debugError('Error parsing response from UAA: ' | ||
+ e) | ||
var error = new Error( | ||
'Error parsing response from UAA: ' + e) | ||
return process.nextTick(function() { | ||
cb(error, null); | ||
}); | ||
} | ||
var tmpResult = self.lruCache.get(cacheKey); | ||
if (tmpResult !== undefined) { | ||
debugTrace('Key "' + cacheKey + '" found in cache. Returning key "' + tmpResult + '".'); | ||
return process.nextTick(function() { | ||
cb(null, tmpResult); | ||
}); | ||
} else { | ||
var error = new Error( | ||
'Obtained token keys from UAA, but key with requested keyID "' + | ||
cacheKey + '" still not found in cache.'); | ||
return process.nextTick(function() { | ||
cb(error, null); | ||
}); | ||
} | ||
}); | ||
} | ||
}; |
@@ -8,4 +8,7 @@ 'use strict'; | ||
const errors = require('./errors'); | ||
// use environment variable DEBUG with value 'xssec:*' for trace/error messages | ||
const debug = require('debug'); | ||
const { exit } = require('process'); | ||
const debugTrace = debug('xssec:requests'); | ||
@@ -19,30 +22,39 @@ const debugError = debug('xssec:requests'); | ||
function _requestToXSUAA(fnc, options, retryOptions, cb) { | ||
function getTokenResponseHandler(cb) { | ||
return function(error, response, body) { | ||
if (error) { | ||
if (error.code === 'ETIMEDOUT' && error.connect === true) { | ||
debugError(fnc + ': HTTP connection timeout.'); | ||
} | ||
error.statusCode = -1; | ||
error.description = error.message; | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error); | ||
} | ||
if (response.statusCode !== 200) { | ||
return cb(new errors.ServerRequestError(response.statusCode + " - " + body, response.statusCode, body)); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
} catch (e) { | ||
e.statusCode = -1; | ||
e.description = "Could not parse JSON"; | ||
e.content = body; | ||
return cb(e, null); | ||
} | ||
return cb(null, json.id_token || json.access_token || json, json); | ||
} | ||
} | ||
function _requestToNetwork(fnc, options, cb) { | ||
debugTrace(fnc + '::HTTP Call with %O', options); | ||
request.post( | ||
request( | ||
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) | ||
} | ||
cb | ||
); | ||
}; | ||
function validateParameters(serviceCredentials, cb) { | ||
@@ -108,2 +120,3 @@ // input validation | ||
var options = { | ||
method: 'POST', | ||
url: url + '/oauth/token', | ||
@@ -125,2 +138,32 @@ headers: DefaultHeaders(zoneId), | ||
module.exports.requestOpenIDConfiguration = function (serviceCredentialsUrl, cb) { | ||
var options = { | ||
method: 'GET', | ||
followRedirect: false, | ||
timeout: 2000, | ||
url: serviceCredentialsUrl + '/.well-known/openid-configuration', | ||
headers: { | ||
'Accept': 'application/json', | ||
'User-Agent': constants.USER_AGENT | ||
} | ||
}; | ||
return _requestToNetwork(".well-known", options, getTokenResponseHandler(cb)); | ||
} | ||
module.exports.fetchOIDCKey = function (serviceCredentialsUrl, cb) { | ||
var options = { | ||
method: 'GET', | ||
url: serviceCredentialsUrl, | ||
followRedirect: false, | ||
timeout: 2000, | ||
headers: { | ||
'Accept': 'application/json', | ||
'User-Agent': constants.USER_AGENT | ||
} | ||
}; | ||
return _requestToNetwork(".oidc-jkws", options, getTokenResponseHandler(cb)); | ||
} | ||
module.exports.requestUserToken = function (appToken, serviceCredentials, additionalAttributes, scopes, subdomain, zoneId, cb) { | ||
@@ -155,3 +198,3 @@ //make it backward-compatible (where zoneId is not provided at all) | ||
return _requestToXSUAA("requestUserToken", options, false, cb); | ||
return _requestToNetwork("requestUserToken", options, getTokenResponseHandler(cb)); | ||
} | ||
@@ -184,3 +227,3 @@ | ||
return _requestToXSUAA("requestClientCredentialsToken", options, false, cb); | ||
return _requestToNetwork("requestClientCredentialsToken", options, getTokenResponseHandler(cb)); | ||
}; | ||
@@ -222,2 +265,18 @@ | ||
}); | ||
} | ||
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; | ||
} | ||
} | ||
} | ||
} |
@@ -32,5 +32,6 @@ 'use strict'; | ||
function JWTStrategy(options) { | ||
function JWTStrategy(options, forceType) { | ||
this.options = options; | ||
this.name = 'JWT'; | ||
this._forceType = forceType; | ||
} | ||
@@ -61,4 +62,4 @@ | ||
try { | ||
var self = this; | ||
xssec.createSecurityContext(token, this.options, function (err, ctx, tokenInfo) { | ||
function callback(err, ctx, tokenInfo) { | ||
req.tokenInfo = tokenInfo; | ||
@@ -94,3 +95,9 @@ | ||
self.success(user, ctx); | ||
}); | ||
}; | ||
var self = this; | ||
var paramA = this._forceType ? this._forceType : callback; | ||
var paramB = this._forceType ? callback : undefined; | ||
xssec.createSecurityContext(token, this.options, paramA, paramB); | ||
} | ||
@@ -97,0 +104,0 @@ catch (err) { |
@@ -89,2 +89,10 @@ 'use strict'; | ||
this.getUserId = function() { | ||
if(this.isTokenIssuedByXSUAA()) { | ||
return payload.user_uuid || payload.sub; | ||
} else { | ||
return payload.user_uuid; | ||
} | ||
} | ||
this.getZoneId = function() { | ||
@@ -91,0 +99,0 @@ if(this.isTokenIssuedByXSUAA()) { |
@@ -163,3 +163,49 @@ 'use strict'; | ||
function JwtTokenValidator(verificationKey, configArray, serviceCredentials) { | ||
function JwtTokenValidatorIAS(verificationKey, configArray, serviceCredentials) { | ||
this.isForeignMode = function () { | ||
return false; | ||
} | ||
this.validateToken = function (accessToken, cb) { | ||
function returnError(code, errorString) { | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = code; | ||
return cb(error); | ||
} | ||
var token = new TokenInfo(accessToken); | ||
return token.verify(verificationKey.getCallback(token), function(err, token) { | ||
if (err) { | ||
debugError('\n' + err.message); | ||
err.statuscode = 401; | ||
return cb(err); | ||
} | ||
//Issuer validation | ||
var iss = token.getPayload().iss; | ||
if(iss !== serviceCredentials.url) { | ||
returnError(401, "Issuer validation failed (iss=" + iss + ") url=" + serviceCredentials.url); | ||
} | ||
//Audience validation | ||
let audienceValidator = new JwtAudienceValidator(serviceCredentials.clientid); | ||
for (var i = 1; i < configArray.length; ++i) { | ||
if (configArray[i] && configArray[i].clientid) { | ||
audienceValidator.configureTrustedClientId(configArray[i].clientid); | ||
} | ||
} | ||
let valid_result = audienceValidator.validateToken(token.getAudiencesArray()); | ||
if (!valid_result.isValid()) { | ||
return returnError(401, valid_result.getErrorDescription()); | ||
} | ||
cb(null, token); | ||
}); | ||
} | ||
} | ||
function JwtTokenValidatorXSUAA(verificationKey, configArray, serviceCredentials) { | ||
var foreignMode = false; | ||
@@ -183,11 +229,11 @@ | ||
return tokeninfo.prepareToken(accessToken, | ||
function (errorString, token) { | ||
if (errorString) { | ||
return returnError(401, errorString); | ||
function (err, token) { | ||
if (err) { | ||
debugError('\n' + err.message); | ||
err.statuscode = 401; | ||
return cb(err); | ||
} | ||
return token.verify(verificationKey.getCallback(token), | ||
function (err, token) { | ||
var decodedToken = token.getPayload(); | ||
function (err, token) { | ||
if (err) { | ||
@@ -200,2 +246,4 @@ debugError(err.statuscode); | ||
var decodedToken = token.getPayload(); | ||
if (!token.getClientId()) { | ||
@@ -241,5 +289,13 @@ return returnError(400, 'Client Id not contained in access token. Giving up!'); | ||
function createJWTTokenValidator(verificationKey, configArray, serviceCredentials) { | ||
if(verificationKey.getType() === "XSUAA") { | ||
return new JwtTokenValidatorXSUAA(verificationKey, configArray, serviceCredentials); | ||
} else { | ||
return new JwtTokenValidatorIAS(verificationKey, configArray, serviceCredentials); | ||
} | ||
} | ||
module.exports = { | ||
JwtAudienceValidator: JwtAudienceValidator, | ||
JwtTokenValidator: JwtTokenValidator | ||
createJwtTokenValidator: createJWTTokenValidator | ||
}; |
@@ -13,9 +13,17 @@ 'use strict'; | ||
function VerificationKey(config) { | ||
function VerificationKey(config, type) { | ||
var tokenInfo = null; | ||
this.getCallback = function(token) { | ||
tokenInfo = token; | ||
return this.loadKey.bind(this); | ||
if(type === 'XSUAA') { | ||
return this.loadKeyXSUAA.bind(this); | ||
} else { | ||
return this.loadKeyOIDC.bind(this); | ||
} | ||
} | ||
this.getType = function() { | ||
return type; | ||
} | ||
function cleanUp(pem) { | ||
@@ -47,3 +55,29 @@ if(!pem) { | ||
this.loadKey = function(accessToken, cb) { | ||
function getKeyCache() { | ||
if(!keyCache) { | ||
keyCache = new keycache.KeyCache(config.keyCache.cacheSize, config.keyCache.expirationTime); | ||
} | ||
return keyCache; | ||
} | ||
this.loadKeyOIDC = function(accessToken, cb) { | ||
if (!accessToken.kid) { | ||
return cb(new Error("No key identifier found in token")); | ||
} | ||
let keyCache = getKeyCache(); | ||
//try to get a key from KeyCache | ||
keyCache.getKeyOIDC(config.url, accessToken.kid, function(err, key) { | ||
if (err) { | ||
debugTrace('\n' + err); | ||
return cb(err); | ||
} else { | ||
return cb(null, key); | ||
} | ||
}); | ||
} | ||
this.loadKeyXSUAA = function(accessToken, cb) { | ||
var zid = tokenInfo.getPayload().zid; | ||
@@ -61,10 +95,8 @@ if (!accessToken.kid || accessToken.kid == 'legacy-token-key' || !accessToken.jku) { | ||
if(!keyCache) { | ||
keyCache = new keycache.KeyCache(config.keyCache.cacheSize, config.keyCache.expirationTime); | ||
} | ||
let keyCache = getKeyCache(); | ||
//try to get a key from KeyCache | ||
//try to get a key from KeyCache | ||
keyCache.getKey(accessToken.jku, accessToken.kid, zid, function(err, key) { | ||
if (err) { | ||
debugTrace('\n', err); | ||
debugTrace('\n' + err); | ||
return cb(null, cleanUp(config.verificationkey)); | ||
@@ -71,0 +103,0 @@ } else { |
509
lib/xssec.js
'use strict'; | ||
const constants = require('./constants'); | ||
const url = require('url'); | ||
const requests = require('./requests'); | ||
const xsuaa = require('./ctx/xsuaa'); | ||
const ias = require('./ctx/ias'); | ||
// use environment variable DEBUG with value 'xssec:*' for trace/error messages | ||
var debug = require('debug'); | ||
var debugTrace = debug('xssec:securitycontext'); | ||
var debugError = debug('xssec:securitycontext'); | ||
const debug = require('debug'); | ||
const debugTrace = debug('xssec:securitycontext'); | ||
const debugError = debug('xssec:securitycontext'); | ||
const JwtTokenValidator = require('./validator').JwtTokenValidator; | ||
const VerificationKey = require('./verificationkey'); | ||
const { isArray } = require('util'); | ||
const { config } = require('process'); | ||
debugError.log = console.error.bind(console); | ||
@@ -27,419 +22,147 @@ debugTrace.log = console.log.bind(console); | ||
//For Backward compatibilty | ||
exports.createSecurityContext = function (token, config, cb) { | ||
try { | ||
var securityContext = new SecurityContext(config); | ||
securityContext.verifyToken(token, cb); | ||
} catch (e) { | ||
cb(e); | ||
function validateConfig(config) { | ||
// validate config input | ||
debugTrace('\nConfiguration (note: clientsecret might be contained but is not traced): ' + | ||
JSON.stringify(config, function (key, value) { | ||
return key === 'clientsecret' ? undefined : value; | ||
}, 2)); | ||
// validate config input | ||
if (!config) { | ||
return throw500('Invalid config (missing).'); | ||
} | ||
} | ||
function SecurityContext(configParam) { | ||
//make sure the parameter is an array | ||
var configArr = Array.isArray(configParam) ? configParam : [configParam]; | ||
//our main config is always the config at position 0 | ||
var config = configArr[0]; | ||
if (!config.clientid) { | ||
return throw500('Invalid config: Missing clientid.'); | ||
} | ||
if (!config.clientsecret) { | ||
return throw500('Invalid config: Missing clientsecret.'); | ||
} | ||
if (!config.url) { | ||
return throw500('Invalid config: Missing url.'); | ||
} | ||
var userInfo = { | ||
logonName: '', | ||
givenName: '', | ||
familyName: '', | ||
email: '' | ||
}; | ||
var token; | ||
var xsappname; | ||
var scopes; | ||
var samlToken; | ||
var clientId; | ||
var subaccountid; | ||
var zid; | ||
var subdomain = null; | ||
var origin = null; | ||
var userAttributes; | ||
var additionalAuthAttributes; | ||
var serviceinstanceid = null; | ||
var grantType; | ||
var expirationDate; | ||
var tokenInfo = null; | ||
var isForeignMode = false; | ||
function validateXSAPPNAME() { | ||
if (!config.xsappname) { | ||
if (!process.env.XSAPPNAME) { | ||
var errorString = 'Invalid config: Missing xsappname.\n' | ||
+ 'The application name needs to be defined in xs-security.json.'; | ||
return throw500(errorString); | ||
} else { | ||
xsappname = process.env.XSAPPNAME; | ||
debugTrace('\nXSAPPNAME defined in manifest.yml (legacy).\n' | ||
+ 'You should switch to defining xsappname in xs-security.json.'); | ||
if (!config.keyCache) { | ||
config.keyCache = { | ||
expirationTime: constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES, | ||
cacheSize: constants.KEYCACHE_DEFAULT_CACHE_SIZE | ||
}; | ||
debugTrace("Using KeyCache with default values %o", config.keyCache); | ||
} else { | ||
if (config.keyCache.expirationTime) { | ||
//if it's provided it has to be a number bigger than zero | ||
if (typeof config.keyCache.expirationTime !== 'number' || config.keyCache.expirationTime < constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES) { | ||
debugError("keyCache.expirationTime has to be a Number with the value of at least " + constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES + ". taking default value."); | ||
config.keyCache.expirationTime = constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES; | ||
} | ||
} else { | ||
if (!process.env.XSAPPNAME) { | ||
xsappname = config.xsappname; | ||
} else { | ||
if (process.env.XSAPPNAME == config.xsappname) { | ||
xsappname = process.env.XSAPPNAME; | ||
debugTrace('\nThe application name is defined both in the manifest.yml (legacy) \n' | ||
+ 'as well as in xs-security.json. Remove it in manifest.yml.'); | ||
} else { | ||
var errorString = 'Invalid config: Ambiguous xsappname.\n' | ||
+ 'The application name is defined with different values in the manifest.yml (legacy)\n' | ||
+ 'as well as in xs-security.json. Remove it in manifest.yml.'; | ||
return throw500(errorString); | ||
} | ||
} | ||
config.keyCache.expirationTime = constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES; | ||
} | ||
} | ||
function ctor() { | ||
// validate config input | ||
debugTrace('\nConfiguration (note: clientsecret might be contained but is not traced): ' + | ||
JSON.stringify(config, function(key, value) { | ||
return key === 'clientsecret' ? undefined : value; | ||
}, 4)); | ||
// validate config input | ||
if (!config) { | ||
return throw500('Invalid config (missing).'); | ||
} | ||
validateXSAPPNAME(); | ||
if (!config.clientid) { | ||
return throw500('Invalid config: Missing clientid.'); | ||
} | ||
if (!config.clientsecret) { | ||
return throw500('Invalid config: Missing clientsecret.'); | ||
} | ||
if (!config.url) { | ||
return throw500('Invalid config: Missing url.'); | ||
} | ||
if(!config.keyCache) { | ||
config.keyCache = { | ||
expirationTime: constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES, | ||
cacheSize: constants.KEYCACHE_DEFAULT_CACHE_SIZE | ||
}; | ||
debugTrace("Using KeyCache with default values %o", config.keyCache); | ||
} else { | ||
if(config.keyCache.expirationTime) { | ||
//if it's provided it has to be a number bigger than zero | ||
if(typeof config.keyCache.expirationTime !== 'number' || config.keyCache.expirationTime < constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES) { | ||
debugError("keyCache.expirationTime has to be a Number with the value of at least " + constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES + ". taking default value."); | ||
config.keyCache.expirationTime = constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES; | ||
} | ||
} else { | ||
config.keyCache.expirationTime = constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES; | ||
} | ||
if(config.keyCache.cacheSize) { | ||
//if it's provided it has to be a number bigger than zero | ||
if(typeof config.keyCache.cacheSize !== 'number' || config.keyCache.cacheSize < constants.KEYCACHE_DEFAULT_CACHE_SIZE) { | ||
debugError("keyCache.cacheSize has to be a Number of at least " + constants.KEYCACHE_DEFAULT_CACHE_SIZE + ". taking default value"); | ||
config.keyCache.cacheSize = constants.KEYCACHE_DEFAULT_CACHE_SIZE; | ||
} | ||
} else { | ||
if (config.keyCache.cacheSize) { | ||
//if it's provided it has to be a number bigger than zero | ||
if (typeof config.keyCache.cacheSize !== 'number' || config.keyCache.cacheSize < constants.KEYCACHE_DEFAULT_CACHE_SIZE) { | ||
debugError("keyCache.cacheSize has to be a Number of at least " + constants.KEYCACHE_DEFAULT_CACHE_SIZE + ". taking default value"); | ||
config.keyCache.cacheSize = constants.KEYCACHE_DEFAULT_CACHE_SIZE; | ||
} | ||
debugTrace("Using KeyCache with custom values %o", config.keyCache); | ||
} else { | ||
config.keyCache.cacheSize = constants.KEYCACHE_DEFAULT_CACHE_SIZE; | ||
} | ||
} | ||
function ifNotClientCredentialsToken(functionName, value) { | ||
if (grantType === constants.GRANTTYPE_CLIENTCREDENTIAL) { | ||
var errorString = '\nCall to ' + functionName + ' not allowed with a token of grant type ' + constants.GRANTTYPE_CLIENTCREDENTIAL + '.'; | ||
debugTrace(errorString); | ||
return null; | ||
} | ||
return value; | ||
debugTrace("Using KeyCache with custom values %o", config.keyCache); | ||
} | ||
} | ||
this.getSubaccountId = function () { | ||
return subaccountid; | ||
}; | ||
function validateXSUAAConfig(config) { | ||
if (!config.xsappname) { | ||
if (!process.env.XSAPPNAME) { | ||
var errorString = 'Invalid config: Missing xsappname.\n' | ||
+ 'The application name needs to be defined in xs-security.json.'; | ||
return throw500(errorString); | ||
} | ||
this.getZoneId = function () { | ||
return zid; | ||
}; | ||
this.getSubdomain = function () { | ||
return subdomain; | ||
}; | ||
this.getClientId = function () { | ||
return clientId; | ||
}; | ||
this.getExpirationDate = function () { | ||
return expirationDate; | ||
}; | ||
this.getOrigin = function () { | ||
return origin; | ||
}; | ||
this.getLogonName = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getLogonName', userInfo.logonName); | ||
}; | ||
this.getGivenName = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getGivenName', userInfo.givenName); | ||
}; | ||
this.getFamilyName = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getFamilyName', userInfo.familyName); | ||
}; | ||
this.getEmail = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getEmail', userInfo.email); | ||
}; | ||
this.getUserName = function () { | ||
if (grantType === constants.GRANTTYPE_CLIENTCREDENTIAL) { | ||
return `client/${clientId}`; | ||
} else { | ||
return this.getUniquePrincipalName(origin, userInfo.logonName); | ||
debugTrace('\nXSAPPNAME defined in manifest.yml (legacy).\n' | ||
+ 'You should switch to defining xsappname in xs-security.json.'); | ||
return process.env.XSAPPNAME; | ||
} else { | ||
if (!process.env.XSAPPNAME) { | ||
return config.xsappname; | ||
} | ||
}; | ||
this.getUniquePrincipalName = function (origin, logonName) { | ||
if(!ifNotClientCredentialsToken('SecurityContext.getUniquePrincipalName', true)) { | ||
return null; | ||
if (process.env.XSAPPNAME == config.xsappname) { | ||
debugTrace('\nThe application name is defined both in the manifest.yml (legacy) \n' | ||
+ 'as well as in xs-security.json. Remove it in manifest.yml.'); | ||
return process.env.XSAPPNAME; | ||
} | ||
if (!origin) { | ||
debugTrace('Origin claim not set in JWT. Cannot create unique user name. Returning null.'); | ||
return null; | ||
} | ||
if (!logonName) { | ||
debugTrace('User login name claim not set in JWT. Cannot create unique user name. Returning null.'); | ||
return null; | ||
} | ||
if (origin.includes('/')) { | ||
debugTrace('Illegal \'/\' character detected in origin claim of JWT. Cannot create unique user name. Retuning null.'); | ||
return null; | ||
} | ||
return `user/${origin}/${logonName}`; | ||
}; | ||
this.getHdbToken = function () { | ||
if (userAttributes && isForeignMode) { | ||
debugTrace('\nThe SecurityContext has been initialized with an access token of a\n' | ||
+ 'foreign OAuth Client Id and/or Identity Zone. Furthermore, the \n' | ||
+ 'access token contains attributes. Due to the fact that we want to\n' | ||
+ 'restrict attribute access to the application that provided the \n' | ||
+ 'attributes, the getHdbToken function does not return a valid token.\n'); | ||
return null; | ||
} | ||
return samlToken ? samlToken : this.getAppToken(); | ||
}; | ||
this.getAppToken = function () { | ||
return token; | ||
}; | ||
this.getTokenInfo = function() { | ||
return tokenInfo; | ||
var errorString = 'Invalid config: Ambiguous xsappname.\n' | ||
+ 'The application name is defined with different values in the manifest.yml (legacy)\n' | ||
+ 'as well as in xs-security.json. Remove it in manifest.yml.'; | ||
return throw500(errorString); | ||
} | ||
} | ||
this.getAttribute = function (name) { | ||
if(!ifNotClientCredentialsToken('SecurityContext.getAttribute', true)) { | ||
return null; | ||
} | ||
if (!userAttributes) { | ||
debugTrace('\nThe access token contains no user attributes.\n'); | ||
return null; | ||
} | ||
if (isForeignMode) { | ||
debugTrace('\nThe SecurityContext has been initialized with an access token of a\n' | ||
+ 'foreign OAuth Client Id and/or Identity Zone. Furthermore, the \n' | ||
+ 'access token contains attributes. Due to the fact that we want to\n' | ||
+ 'restrict attribute access to the application that provided the \n' | ||
+ 'attributes, the getAttribute function does not return any attributes.\n'); | ||
return null; | ||
} | ||
if (!name) { | ||
debugTrace('\nInvalid attribute name (may not be null, empty, or undefined).'); | ||
return null; | ||
} | ||
if (!userAttributes[name]) { | ||
debugTrace('\nNo attribute "' + name + '" found for user "' + this.getLogonName() + '".'); | ||
return null; | ||
} | ||
return userAttributes[name]; | ||
}; | ||
function validateIASConfig(config) { | ||
//TODO: clarify do we need to check something here? | ||
} | ||
this.getAdditionalAuthAttribute = function (name) { | ||
if (!additionalAuthAttributes) { | ||
debugTrace('\nThe access token contains no additional authentication attributes.\n'); | ||
return null; | ||
} | ||
if (!name) { | ||
debugTrace('\nInvalid attribute name (may not be null, empty, or undefined).'); | ||
return null; | ||
} | ||
if (!additionalAuthAttributes[name]) { | ||
debugTrace('\nNo attribute "' + name + '" found as additional authentication attribute.'); | ||
return null; | ||
} | ||
return additionalAuthAttributes[name]; | ||
}; | ||
this.getCloneServiceInstanceId = function () { | ||
return serviceinstanceid; | ||
}; | ||
this.isInForeignMode = function () { | ||
return isForeignMode; | ||
}; | ||
this.hasAttributes = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.hasAttributes', userAttributes ? true : false); | ||
}; | ||
this.checkLocalScope = function (scope) { | ||
if (!scope || !scopes) { | ||
function takeXSUAAConfiguration(config, forceType) { | ||
if(forceType === exports.XSUAA) { | ||
debugTrace("Creating a XSUAA securityContext because of forceType"); | ||
return true; | ||
} if(forceType === exports.IAS) { | ||
debugTrace("Creating a IAS securityContext because of forceType"); | ||
return false; | ||
} else if(!forceType) { | ||
if(config.xsappname ? true : (process.env.XSAPPNAME ? true : false)) { | ||
debugTrace("Creating a XSUAA securityContext because of xsappname"); | ||
return true; | ||
} else { | ||
debugTrace("Creating a IAS securityContext because of missing xsappname"); | ||
return false; | ||
} | ||
var scopeName = xsappname + '.' + scope; | ||
return scopes.indexOf(scopeName) !== -1; | ||
}; | ||
} | ||
this.getGrantType = function () { | ||
return grantType; | ||
}; | ||
throw500("Unknown SecurityContextType (" + forceType + ")"); | ||
} | ||
this.checkScope = function (scope) { | ||
if (!scope || !scopes) { | ||
return false; | ||
} | ||
exports.XSUAA = "XSUAA"; | ||
exports.IAS = "IAS"; | ||
if (scope.substring(0, constants.XSAPPNAMEPREFIX.length) === constants.XSAPPNAMEPREFIX) { | ||
scope = scope.replace(constants.XSAPPNAMEPREFIX, xsappname + '.'); | ||
} | ||
return scopes.indexOf(scope) !== -1; | ||
}; | ||
//For Backward compatibilty | ||
exports.createSecurityContext = function (token, configParam, forceType, cb) { | ||
if(typeof forceType === 'function') { | ||
cb = forceType; | ||
forceType = null; | ||
} | ||
this.requestToken = function (serviceCredentials, type, additionalAttributes, cb) { | ||
if (type === constants.TYPE_USER_TOKEN) { | ||
return requests.requestUserToken(this.getAppToken(), serviceCredentials, additionalAttributes, null, this.getSubdomain(), cb); | ||
} else if (type === constants.TYPE_CLIENT_CREDENTIALS_TOKEN) { | ||
return requests.requestClientCredentialsToken(this.getSubdomain(), serviceCredentials, additionalAttributes, cb); | ||
} else { | ||
return cb(new Error('Invalid grant type.')); | ||
} | ||
}; | ||
function cleanUpUserAttributes(attr) { | ||
if(!attr) { | ||
return null; | ||
} | ||
var len = 0; | ||
for(var n in attr) { | ||
++len; | ||
} | ||
return len == 0 ? null : attr; | ||
if(!cb || typeof cb !== 'function') { | ||
throw500("The callback parameter should be a function"); | ||
} | ||
function fillContext(encodedToken, info) { | ||
tokenInfo = info; | ||
var decodedToken = tokenInfo.getPayload(); | ||
debugTrace('\nApplication received a token of grant type "' + decodedToken.grant_type + '".'); | ||
try { | ||
//make sure the parameter is an array | ||
var configArr = Array.isArray(configParam) ? configParam : [configParam]; | ||
token = encodedToken; | ||
scopes = decodedToken.scope || []; | ||
zid = decodedToken.zid; | ||
subaccountid = decodedToken["ext_attr"] ? decodedToken["ext_attr"].subaccountid : zid; | ||
if(!subaccountid) { | ||
subaccountid = zid; | ||
} | ||
//our main config is always the config at position 0 | ||
var config = configArr[0]; | ||
validateConfig(config); | ||
clientId = tokenInfo.getClientId(); | ||
expirationDate = new Date(decodedToken.exp * 1000); | ||
grantType = decodedToken.grant_type; | ||
let securityContext; | ||
if(takeXSUAAConfiguration(config, forceType)) { | ||
//will return the xsappname to use (env or config) | ||
config.xsappname = validateXSUAAConfig(config); | ||
origin = decodedToken.origin || null; | ||
if (grantType !== constants.GRANTTYPE_CLIENTCREDENTIAL) { | ||
var givenName, familyName; | ||
if(decodedToken.ext_attr) { | ||
givenName = decodedToken.ext_attr.given_name || null; | ||
familyName = decodedToken.ext_attr.family_name || null; | ||
} | ||
userInfo.givenName = givenName || decodedToken.given_name || ''; | ||
userInfo.familyName = familyName || decodedToken.family_name || ''; | ||
userInfo.email = decodedToken.email || ''; | ||
userInfo.logonName = decodedToken.user_name || ''; | ||
debugTrace('\nObtained logon name: ' + this.getLogonName()); | ||
debugTrace('Obtained given name: ' + this.getGivenName()); | ||
debugTrace('Obtained family name: ' + this.getFamilyName()); | ||
debugTrace('Obtained email: ' + this.getEmail()); | ||
if (decodedToken.ext_cxt) { | ||
userAttributes = decodedToken.ext_cxt['xs.user.attributes'] || null; | ||
samlToken = decodedToken.ext_cxt['hdb.nameduser.saml'] || null; | ||
} else { | ||
userAttributes = decodedToken['xs.user.attributes']; | ||
samlToken = decodedToken['hdb.nameduser.saml'] || null; | ||
} | ||
userAttributes = cleanUpUserAttributes(userAttributes); | ||
if(userAttributes) { | ||
debugTrace('\nObtained attributes: ' + JSON.stringify(userAttributes, null, 4)); | ||
} else { | ||
debugTrace('\nObtained attributes: no XS user attributes in JWT token available.'); | ||
} | ||
} | ||
additionalAuthAttributes = decodedToken.az_attr || null; | ||
if(additionalAuthAttributes) { | ||
debugTrace('\nObtained additional authentication attributes: ' + JSON.stringify(additionalAuthAttributes, null, 4)); | ||
//create a XSUAA security Context | ||
securityContext = new xsuaa.SecurityContext(config, configArr); | ||
} else { | ||
debugTrace('\nObtained attributes: no additional authentication attributes in JWT token available.'); | ||
validateIASConfig(config); | ||
//create a IAS security Context | ||
securityContext = new ias.SecurityContext(config, configArr); | ||
} | ||
if(decodedToken.ext_attr) { | ||
serviceinstanceid = decodedToken.ext_attr.serviceinstanceid || null; | ||
subdomain = decodedToken.ext_attr.zdn || null; | ||
} | ||
debugTrace('\nObtained subdomain: ' + this.getSubdomain()); | ||
debugTrace('Obtained serviceinstanceid: ' + this.getCloneServiceInstanceId()); | ||
debugTrace('Obtained origin: ' + this.getOrigin()); | ||
debugTrace('Obtained scopes: ' + JSON.stringify(scopes, null, 4)); | ||
securityContext.verifyToken(token, cb); | ||
} catch (e) { | ||
cb(e); | ||
} | ||
this.verifyToken = function (encodedToken, cb) { | ||
var verificationKey = new VerificationKey(config); | ||
var jwtValidator = new JwtTokenValidator(verificationKey, configArr, config); | ||
//Now validate the tokens | ||
jwtValidator.validateToken(encodedToken, function(err, tokenInfo) { | ||
if(err) { | ||
return cb(err, null, tokenInfo); | ||
} | ||
isForeignMode = jwtValidator.isForeignMode(); | ||
//Token is now validated. So just fill local variables | ||
fillContext.call(this, encodedToken, tokenInfo); | ||
cb(null, this, tokenInfo); | ||
}.bind(this)); | ||
}; | ||
//call constructor | ||
ctor(); | ||
}; | ||
} |
{ | ||
"name": "@sap/xssec", | ||
"version": "3.1.2", | ||
"version": "3.2.0", | ||
"description": "XS Advanced Container Security API for node.js", | ||
@@ -13,2 +13,5 @@ "main": "./lib", | ||
"lib", | ||
"lib/ctx", | ||
"lib/strategies", | ||
"doc", | ||
"package.json", | ||
@@ -34,3 +37,4 @@ "README.md" | ||
"jsonwebtoken": "^8.5.1", | ||
"lru-cache": "5.1.1", | ||
"lru-cache": "6.0.0", | ||
"node-rsa": "^1.1.1", | ||
"request": "^2.88.2", | ||
@@ -37,0 +41,0 @@ "requestretry": "4.0.0", |
114196
24
1599
7
7
+ Addednode-rsa@^1.1.1
+ Addedlru-cache@6.0.0(transitive)
+ Addednode-rsa@1.1.1(transitive)
+ Addedyallist@4.0.0(transitive)
- Removedlru-cache@5.1.1(transitive)
- Removedyallist@3.1.1(transitive)
Updatedlru-cache@6.0.0