@sap/xssec
Advanced tools
Comparing version 2.1.16 to 3.0.3
# Change Log | ||
All notable changes to this project will be documented in this file. | ||
## 3.0.3 - 2020-05-25 | ||
- Fix jwt-bearer flow to take the right token as uri parameter. | ||
## 3.0.2 - 2020-05-20 | ||
- Fix get verification key from keycache. | ||
## 3.0.1 - 2020-05-19 | ||
- HotFix missing debugTrace in verification key | ||
- Fix RetryStrategy | ||
## 3.0.0 - 2020-05-15 | ||
- Replace grant type user_token in method requestToken (TYPE_USER_TOKEN) in favor of urn:ietf:params:oauth:grant-type:jwt-bearer | ||
- Remove obsolete method getToken (use getHdbToken or getAppToken)) | ||
- Remove obsolete method requestTokenForClient (use requestToken) | ||
- Remove obsolete method getIdentityZone (getSubaccountId) | ||
- Support for audience validation in token | ||
- remove of SAP_JWT_TRUST_ACL environment variable support (functionality now comes with audience validation) | ||
- remove depencency to node-jwt (ALPINE support) | ||
- restructure internal code for better maintainability | ||
## 2.2.5 - 2020-02-28 | ||
- Update to node-jwt version 1.6.6 | ||
## 2.2.4 - 2019-08-14 | ||
- Support for API methods getUserName and getUniquePrincipalName | ||
## 2.2.3 - 2019-08-07 | ||
- Add retry for recieving keys | ||
## 2.2.2 - 2019-06-24 | ||
- Use verification key from binding as backup if online key retrieval fails | ||
## 2.2.1 - 2019-06-17 | ||
- Fix uaaDomain comparison in key cache | ||
## 2.2.0 - 2019-06-17 | ||
- Align key cache implementation with other container security libraries | ||
## 2.1.17 - 2019-05-17 | ||
- Introduce http timeout of two seconds | ||
- Update version of module debug, lru-cache and @sap/xsenv | ||
- Fix token verification for broker master instance subscriptions | ||
## 2.1.16 - 2019-01-28 | ||
@@ -5,0 +60,0 @@ |
@@ -6,5 +6,4 @@ 'use strict'; | ||
exports.createSecurityContextCC = xssec.createSecurityContextCC; | ||
exports.createSecurityContext = xssec.createSecurityContext; | ||
exports.constants = require('./constants'); | ||
exports.JWTStrategy = passportStrategy.JWTStrategy; |
'use strict'; | ||
var constants = require('./constants'); | ||
var request = require('request'); | ||
var request = require('requestretry'); | ||
var LRU = require('lru-cache'); | ||
@@ -82,8 +82,8 @@ var validUrl = require('valid-url'); | ||
KeyCache.prototype.getKey = function getKey(keyId, uaaUrl, cb) { | ||
KeyCache.prototype.getKey = function getKey(tokenKeyUrl, keyId, cb) { | ||
var self = this; | ||
if ((keyId === null) || (keyId === undefined)) { | ||
if ((tokenKeyUrl === null) || (tokenKeyUrl === undefined)) { | ||
var error = new Error( | ||
'Parameter keyId null or undefined. To read a key from the KeyCache with function KeyCache.getKey, you need to specify parameter keyId.'); | ||
'Parameter tokenKeyUrl null or undefined. To enable the KeyCache reading keys from the UAA which are yet unavailable in the cache, you need to specify parameter tokenKeyUrl as a valid https URL.'); | ||
return process.nextTick(function() { | ||
@@ -93,5 +93,5 @@ cb(error, null); | ||
} | ||
if ((uaaUrl === null) || (uaaUrl === undefined)) { | ||
if (validUrl.isHttpsUri(tokenKeyUrl) === undefined) { | ||
var error = new Error( | ||
'Parameter uaaUrl null or undefined. To enable the KeyCache reading keys from the UAA which are yet unavailable in the cache, you need to specify parameter uaaUrl as a valid https URL.'); | ||
'Parameter tokenKeyUrl is not a valid https URL. To enable the KeyCache reading keys from the UAA which are yet unavailable in the cache, you need to specify parameter tokenKeyUrl as a valid https URL.'); | ||
return process.nextTick(function() { | ||
@@ -101,5 +101,5 @@ cb(error, null); | ||
} | ||
if (validUrl.isHttpsUri(uaaUrl) === undefined) { | ||
if ((keyId === null) || (keyId === undefined)) { | ||
var error = new Error( | ||
'Parameter uaaUrl is not a valid https URL. To enable the KeyCache reading keys from the UAA which are yet unavailable in the cache, you need to specify parameter uaaUrl as a valid https URL.'); | ||
'Parameter keyId null or undefined. To read a key from the KeyCache with function KeyCache.getKey, you need to specify parameter keyId.'); | ||
return process.nextTick(function() { | ||
@@ -110,8 +110,8 @@ cb(error, null); | ||
debugTrace('Looking for key with keyID: "' + keyId + '" in cache.'); | ||
var cacheKey = tokenKeyUrl + keyId; | ||
debugTrace('Looking for key "' + cacheKey + '" in cache.'); | ||
// Check whether keyid is in cache | ||
var tmpResult = this.lruCache.get(keyId); | ||
var tmpResult = this.lruCache.get(cacheKey); | ||
if (tmpResult !== undefined) { | ||
debugTrace('Key with keyID: "' + keyId | ||
+ '" found in cache. Returning key "' + tmpResult + '".'); | ||
debugTrace('Key with keyID: "' + keyId + '" found in cache. Returning key "' + tmpResult + '".'); | ||
return process.nextTick(function() { | ||
@@ -123,4 +123,3 @@ cb(null, tmpResult); | ||
// UAA | ||
var error = new Error('Key with keyID: "' + keyId | ||
+ '" not found in cache. Configuration says not to query UAA.'); | ||
var error = new Error('Key "' + cacheKey + '" not found in cache. Configuration says not to query UAA.'); | ||
return process.nextTick(function() { | ||
@@ -132,7 +131,9 @@ cb(error, null); | ||
var options = { | ||
url : uaaUrl + this.tokenKeyPath | ||
url : tokenKeyUrl, | ||
timeout: 2000, | ||
maxAttempts: 3, | ||
retryDelay: 500, | ||
retryStrategy: request.RetryStrategies.HTTPOrNetworkError | ||
}; | ||
debugTrace('Key with keyID: "' + keyId | ||
+ '" not found in cache. Querying keys from UAA via URL "' | ||
+ options.url + '".'); | ||
debugTrace('Key "' + cacheKey + '" not found in cache. Querying keys from UAA via URL "' + options.url + '".'); | ||
request | ||
@@ -142,3 +143,9 @@ .get( | ||
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 ' | ||
@@ -173,3 +180,3 @@ + options.url | ||
// breaks before adding the key to the cache | ||
self.addKey(json.keys[i].kid, | ||
self.addKey(tokenKeyUrl + json.keys[i].kid, | ||
json.keys[i].value.replace( | ||
@@ -187,7 +194,5 @@ /(\r\n|\n|\r)/gm, '')); | ||
} | ||
var tmpResult = self.lruCache.get(keyId); | ||
var tmpResult = self.lruCache.get(cacheKey); | ||
if (tmpResult !== undefined) { | ||
debugTrace('Key with keyID: "' + keyId | ||
+ '" found in cache. Returning key "' | ||
+ tmpResult + '".'); | ||
debugTrace('Key "' + cacheKey + '" found in cache. Returning key "' + tmpResult + '".'); | ||
return process.nextTick(function() { | ||
@@ -198,5 +203,4 @@ cb(null, tmpResult); | ||
var error = new Error( | ||
'Obtained token keys from UAA, but key with requested keyID "' | ||
+ keyId | ||
+ '" still not found in cache.'); | ||
'Obtained token keys from UAA, but key with requested keyID "' + | ||
cacheKey + '" still not found in cache.'); | ||
return process.nextTick(function() { | ||
@@ -203,0 +207,0 @@ cb(error, null); |
1082
lib/xssec.js
'use strict'; | ||
const constants = require('./constants'); | ||
const request = require('request'); | ||
const url = require('url'); | ||
var constants = require('./constants'); | ||
var request = require('request'); | ||
var url = require('url'); | ||
const requests = require('./requests'); | ||
@@ -12,480 +13,202 @@ // use environment variable DEBUG with value 'xssec:*' for trace/error messages | ||
var keycache = require('./keycache'); | ||
const JwtTokenValidator = require('./validator').JwtTokenValidator; | ||
const VerificationKey = require('./verificationkey'); | ||
// Note: the keycache is initialized currently with the default size defined in constants | ||
// Consider making this configurable for the application, e.g. via xssecurity.json | ||
// or (probably worse) via environment variables. | ||
// Similarly, the keycache uses the default expiration time for cache entries as | ||
// defined in constants. Also here, consider making this configurable. | ||
var keyCache = new keycache.KeyCache(constants.KEYCACHE_DEFAULT_CACHE_SIZE, constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES); | ||
debugError.log = console.error.bind(console); | ||
debugTrace.log = console.log.bind(console); | ||
exports.createSecurityContext = createSecurityContext; | ||
function throw500(errorString) { | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 500; //500 (Invalid config) | ||
throw error; | ||
} | ||
function createSecurityContext(token, config, cb) { | ||
var securityContext; | ||
//For Backward compatibilty | ||
exports.createSecurityContext = function (token, config, cb) { | ||
try { | ||
securityContext = new SecurityContext(token, config); | ||
securityContext.init(done); | ||
var securityContext = new SecurityContext(config); | ||
securityContext.verifyToken(token, cb); | ||
} catch (e) { | ||
cb(e); | ||
} | ||
function done(err) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
cb(null, securityContext); | ||
} | ||
} | ||
function SecurityContext(token, config) { | ||
this.token = token; | ||
this.config = config; | ||
this.xsappname = ''; | ||
this.isForeignMode = false; | ||
this.tokenContainsAttributes = false; | ||
this.tokenContainsAdditionalAuthAttributes = false; | ||
this.userInfo = { | ||
logonName : '', | ||
givenName : '', | ||
familyName : '', | ||
email : '' | ||
function SecurityContext(config) { | ||
var userInfo = { | ||
logonName: '', | ||
givenName: '', | ||
familyName: '', | ||
email: '' | ||
}; | ||
this.scopes = []; | ||
this.samlToken = ''; | ||
this.clientId = ''; | ||
this.identityZone = ''; | ||
this.subdomain = null; | ||
this.origin = null; | ||
this.userAttributes = ''; | ||
this.additionalAuthAttributes = ''; | ||
this.serviceinstanceid = null; | ||
this.grantType = ''; | ||
// sapssoext | ||
var ssojwt; | ||
if (process.sapnodejwtlib) { | ||
this.ssojwt = process.sapnodejwtlib; | ||
} else { | ||
try { | ||
var jwt = require('@sap/node-jwt'); | ||
this.ssojwt = process.sapnodejwtlib = new jwt(""); | ||
} catch (e) { | ||
var error = new Error('No jwt.node available. Error: ' + e.message ); | ||
error.statuscode = 500; //No jwt.node | ||
throw error; | ||
} | ||
} | ||
var token; | ||
var xsappname; | ||
var scopes; | ||
var samlToken; | ||
var clientId; | ||
var identityZone; | ||
var subdomain = null; | ||
var origin = null; | ||
var userAttributes; | ||
var additionalAuthAttributes; | ||
var serviceinstanceid = null; | ||
var grantType; | ||
var expirationDate; | ||
// validate config input | ||
debugTrace('\nConfiguration (note: clientsecret might be contained but is not traced): ' + JSON.stringify(config, credentialsReplacer, 4)); | ||
if (!this.ssojwt || this.ssojwt.getLibraryVersion() === -1) { | ||
debugTrace('\nSSO library path: ' + process.env['SSOEXT_LIB']); | ||
var error = new Error('JWT validation library could not be loaded. Used ' | ||
+ process.env['SSOEXT_LIB']); | ||
error.statuscode = 500; //lib not found | ||
throw error; | ||
} | ||
var isForeignMode = false; | ||
// validate config input | ||
if (!token || token.length === 0) { | ||
var error = new Error('Invalid token (empty).'); | ||
error.statuscode = 401; //(Token Empty/No Token) | ||
throw error; | ||
} | ||
if (!config) { | ||
var error = new Error('Invalid config (missing).'); | ||
error.statuscode = 500; //500 (Invalid config) | ||
throw error; | ||
} | ||
if (!this.config.clientid) { | ||
var error = new Error('Invalid config: Missing clientid.'); | ||
error.statuscode = 500; //500 (Invalid config) | ||
throw error; | ||
} | ||
if (!this.config.clientsecret) { | ||
var error = new Error('Invalid config: Missing clientsecret.'); | ||
error.statuscode = 500; //500 (Invalid config) | ||
throw error; | ||
} | ||
if (!this.config.url) { | ||
var error = new Error('Invalid config: Missing url.'); | ||
error.statuscode = 500; //500 (Invalid config) | ||
throw error; | ||
} | ||
if (!this.config.xsappname) { | ||
if (!process.env.XSAPPNAME) { | ||
var errorString = 'Invalid config: Missing xsappname.\n' | ||
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.'; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 500; //500 (Invalid config) | ||
throw error; | ||
} else { | ||
this.xsappname = process.env.XSAPPNAME; | ||
debugTrace('\nXSAPPNAME defined in manifest.yml (legacy).\n' | ||
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.'); | ||
} | ||
} else { | ||
if (!process.env.XSAPPNAME) { | ||
this.xsappname = this.config.xsappname; | ||
} | ||
} else { | ||
if (process.env.XSAPPNAME == this.config.xsappname) { | ||
this.xsappname = process.env.XSAPPNAME; | ||
debugTrace('\nThe application name is defined both in the manifest.yml (legacy) \n' | ||
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' | ||
} 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.'; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 500; //500 (Invalid config) | ||
throw error; | ||
return throw500(errorString); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
SecurityContext.prototype.init = function init(cb) { | ||
var self = this; | ||
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)); | ||
offlineValidation( | ||
self.token, | ||
self.config, | ||
self.ssojwt, | ||
function(error, result) { | ||
if (error) { | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error, null); | ||
} | ||
// validate config input | ||
if (!config) { | ||
return throw500('Invalid config (missing).'); | ||
} | ||
if (!result.cid) { | ||
var errorString = 'Client Id not contained in access token. Giving up!'; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 400; //400 (no clientID) | ||
return cb(error, null); | ||
} | ||
if (!result.zid) { | ||
var errorString = 'Identity Zone not contained in access token. Giving up!'; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 400; //400 (wrong idz) | ||
return cb(error, null); | ||
} | ||
validateXSAPPNAME(); | ||
var applicationPlan = false; | ||
if (result.cid.indexOf('!t') !== -1) { | ||
applicationPlan = true; | ||
} | ||
if ((result.cid === self.config.clientid) | ||
&& (result.zid === self.config.identityzoneid || applicationPlan === true)) { | ||
// clientid and identity zone of the token must match with the values of the application | ||
// (if the token was issued for the xsuaa application plan it is sufficient that the clientid matches) | ||
if (applicationPlan === false) { | ||
debugTrace('\nClient Id and Identity Zone of the access token match\n' | ||
+ 'with the current application\'s Client Id and Zone.'); | ||
} else { | ||
debugTrace('\nClient Id of the access token (XSUAA application plan) matches\n' | ||
+ 'with the current application\'s Client Id.'); | ||
} | ||
self.isForeignMode = false; | ||
} else if (self.config.trustedclientidsuffix && self.config.trustedclientidsuffix.length > 0 && | ||
result.cid.substring(result.cid.length - self.config.trustedclientidsuffix.length, result.cid.length) === self.config.trustedclientidsuffix) { | ||
debugTrace('\nClient Id "' + result.cid + '" of the access token allows consumption by the Client Id "' + | ||
self.config.clientid + '" of the current application\n'); | ||
self.isForeignMode = false; | ||
} else if (process.env.SAP_JWT_TRUST_ACL) { | ||
debugTrace('\nClient Id "' | ||
+ result.cid | ||
+ '" and/or Identity Zone "' | ||
+ result.zid | ||
+ '" of the access token\n' | ||
+ 'does/do not match with the Client Id "' | ||
+ self.config.clientid | ||
+ '" and Identity Zone "' | ||
+ self.config.identityzoneid | ||
+ '"\nof the current application.\n' | ||
+ 'Validating token against JWT trust ACL (SAP_JWT_TRUST_ACL).'); | ||
var parsedACL; | ||
try { | ||
parsedACL = JSON.parse(process.env.SAP_JWT_TRUST_ACL); | ||
} catch (er) { | ||
var errorString = 'JWT trust ACL (ACL SAP_JWT_TRUST_ACL):\n' | ||
+ process.env.SAP_JWT_TRUST_ACL | ||
+ '\ncould not be parsed successfully.\n' | ||
+ 'Error: ' + er.message; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 500; //500 (Internal Server Error) | ||
return cb(error, null); | ||
} | ||
var foundMatch = false; | ||
for ( var aclEntry in parsedACL) { | ||
if (((result.cid === parsedACL[aclEntry].clientid) || ('*' === parsedACL[aclEntry].clientid)) | ||
&& ((result.zid === parsedACL[aclEntry].identityzone) || ('*' === parsedACL[aclEntry].identityzone))) { | ||
foundMatch = true; | ||
break; | ||
} | ||
} | ||
if (foundMatch) { | ||
debugTrace('\nForeign token received, but matching entry\n' | ||
+ 'in JWT trust ACL (SAP_JWT_TRUST_ACL) found.'); | ||
self.isForeignMode = true; | ||
} else { | ||
var errorString = 'Client Id "' | ||
+ result.cid | ||
+ '" and/or Identity Zone "' | ||
+ result.zid | ||
+ '" of the access token\n' | ||
+ 'does/do not match with the Client Id "' | ||
+ self.config.clientid | ||
+ '" and Identity Zone "' | ||
+ self.config.identityzoneid | ||
+ '" of the current application.\n' | ||
+ 'No match found in JWT trust ACL (SAP_JWT_TRUST_ACL):\n' | ||
+ JSON.stringify(parsedACL, null, 4); | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 403; //403 Forbidden (as no JWT trust entry) | ||
return cb(error, null); | ||
} | ||
} else { | ||
if (result.cid !== self.config.clientid) { | ||
var errorString = 'Client Id of the access token "' | ||
+ result.cid | ||
+ '" does not match with\nthe OAuth Client Id "' | ||
+ self.config.clientid | ||
+ '" of the application.\n' | ||
+ 'No JWT trust ACL (SAP_JWT_TRUST_ACL) specified in environment.'; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 403; //403 Forbidden (as client IDs do not match and no trust entry exists) | ||
return cb(error, null); | ||
} else { | ||
var errorString = 'Identity Zone of the access token "' | ||
+ result.zid | ||
+ '" does not match\nwith the Identity Zone "' | ||
+ self.config.identityzoneid | ||
+ '" of the application.\n' | ||
+ 'No JWT trust ACL (SAP_JWT_TRUST_ACL) specified in environment.'; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 403; //403 Forbidden (as identity zones do not match and no trust entry exists) | ||
return cb(error, null); | ||
} | ||
} | ||
debugTrace('\nApplication received a token of grant type "' + result.grant_type + '".'); | ||
self.identityZone = result.zid; | ||
self.clientId = result.cid; | ||
if (result.origin !== null && result.origin !== undefined) { | ||
self.origin = result.origin; | ||
} | ||
self.expirationDate = new Date(result.exp * 1000); | ||
self.grantType = result.grant_type; | ||
if (self.grantType !== constants.GRANTTYPE_CLIENTCREDENTIAL) { | ||
self.userInfo.logonName = result.user_name; // jshint ignore:line | ||
if (result.hasOwnProperty('ext_attr') && result.ext_attr.given_name !== undefined) { | ||
self.userInfo.givenName = result.ext_attr.given_name; | ||
} else { | ||
self.userInfo.givenName = result.given_name; // jshint ignore:line | ||
} | ||
if (result.hasOwnProperty('ext_attr') && result.ext_attr.family_name !== undefined) { | ||
self.userInfo.familyName = result.ext_attr.family_name; | ||
} else { | ||
self.userInfo.familyName = result.family_name; // jshint ignore:line | ||
} | ||
self.userInfo.email = result.email; | ||
debugTrace('\nObtained logon name: ' + self.userInfo.logonName); | ||
debugTrace('Obtained given name: ' + self.userInfo.givenName); | ||
debugTrace('Obtained family name: ' + self.userInfo.familyName); | ||
debugTrace('Obtained email: ' + self.userInfo.email); | ||
if (result.hasOwnProperty('ext_cxt')) { | ||
if (result.ext_cxt['hdb.nameduser.saml'] !== undefined) { | ||
self.samlToken = result.ext_cxt['hdb.nameduser.saml']; | ||
} | ||
if (result.ext_cxt['xs.user.attributes'] !== undefined) { | ||
self.userAttributes = result.ext_cxt['xs.user.attributes']; | ||
self.tokenContainsAttributes = true; | ||
debugTrace('\nObtained attributes: ' | ||
+ JSON.stringify(self.userAttributes, null, 4)); | ||
} else { | ||
self.tokenContainsAttributes = false; | ||
debugTrace('\nObtained attributes: no XS user attributes in JWT token available.'); | ||
} | ||
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.'); | ||
} | ||
} | ||
} else { | ||
self.samlToken = result['hdb.nameduser.saml']; | ||
if (result.hasOwnProperty('xs.user.attributes')) { | ||
self.userAttributes = result['xs.user.attributes']; | ||
self.tokenContainsAttributes = true; | ||
debugTrace('\nObtained attributes: ' | ||
+ JSON.stringify(self.userAttributes, null, 4)); | ||
} else { | ||
self.tokenContainsAttributes = false; | ||
debugTrace('\nObtained attributes: no XS user attributes in JWT token available.'); | ||
} | ||
} | ||
} | ||
if (result.hasOwnProperty('az_attr')) { | ||
self.additionalAuthAttributes = result['az_attr']; | ||
self.tokenContainsAdditionalAuthAttributes = true; | ||
debugTrace('\nObtained additional authentication attributes: ' | ||
+ JSON.stringify(self.additionalAuthAttributes, null, 4)); | ||
} else { | ||
self.tokenContainsAdditionalAuthAttributes = false; | ||
debugTrace('\nObtained attributes: no additional authentication attributes in JWT token available.'); | ||
} | ||
if (result.hasOwnProperty('ext_attr') && result.ext_attr.serviceinstanceid !== undefined) { | ||
self.serviceinstanceid = result.ext_attr.serviceinstanceid; | ||
} | ||
if (result.hasOwnProperty('ext_attr') && result.ext_attr.zdn !== undefined) { | ||
self.subdomain = result.ext_attr.zdn; | ||
} | ||
debugTrace('\nObtained subdomain: ' + self.subdomain); | ||
debugTrace('Obtained serviceinstanceid: ' + self.serviceinstanceid); | ||
debugTrace('Obtained origin: ' + self.origin); | ||
self.scopes = result.scope; | ||
debugTrace('Obtained scopes: ' | ||
+ JSON.stringify(self.scopes, null, 4)); | ||
cb(); | ||
}); | ||
}; | ||
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; | ||
} | ||
SecurityContext.prototype.getIdentityZone = function() { | ||
return this.identityZone; | ||
}; | ||
this.getSubaccountId = function () { | ||
return identityZone; | ||
}; | ||
SecurityContext.prototype.getSubaccountId = function() { | ||
return this.identityZone; | ||
}; | ||
this.getSubdomain = function () { | ||
return subdomain; | ||
}; | ||
SecurityContext.prototype.getSubdomain = function() { | ||
return this.subdomain; | ||
}; | ||
this.getClientId = function () { | ||
return clientId; | ||
}; | ||
SecurityContext.prototype.getClientId = function() { | ||
return this.clientId; | ||
}; | ||
this.getExpirationDate = function () { | ||
return expirationDate; | ||
}; | ||
SecurityContext.prototype.getExpirationDate = function() { | ||
return this.expirationDate; | ||
}; | ||
this.getOrigin = function () { | ||
return origin; | ||
}; | ||
SecurityContext.prototype.getOrigin = function() { | ||
return this.origin; | ||
}; | ||
this.getLogonName = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getLogonName', userInfo.logonName); | ||
}; | ||
SecurityContext.prototype.getLogonName = function() { | ||
try { | ||
forbidClientCredentialsToken('SecurityContext.getLogonName', this.grantType); | ||
} catch (e) { | ||
return null; | ||
} | ||
return this.userInfo.logonName; | ||
}; | ||
this.getGivenName = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getGivenName', userInfo.givenName); | ||
}; | ||
SecurityContext.prototype.getGivenName = function() { | ||
try { | ||
forbidClientCredentialsToken('SecurityContext.getGivenName', this.grantType); | ||
} catch (e) { | ||
return null; | ||
} | ||
return this.userInfo.givenName; | ||
}; | ||
this.getFamilyName = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getFamilyName', userInfo.familyName); | ||
}; | ||
SecurityContext.prototype.getFamilyName = function() { | ||
try { | ||
forbidClientCredentialsToken('SecurityContext.getFamilyName', this.grantType); | ||
} catch (e) { | ||
return null; | ||
} | ||
return this.userInfo.familyName; | ||
}; | ||
this.getEmail = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.getEmail', userInfo.email); | ||
}; | ||
SecurityContext.prototype.getEmail = function() { | ||
try { | ||
forbidClientCredentialsToken('SecurityContext.getEmail', this.grantType); | ||
} catch (e) { | ||
return null; | ||
} | ||
return this.userInfo.email; | ||
}; | ||
this.getUserName = function () { | ||
if (grantType === constants.GRANTTYPE_CLIENTCREDENTIAL) { | ||
return `client/${clientId}`; | ||
} else { | ||
return this.getUniquePrincipalName(origin, userInfo.logonName); | ||
} | ||
}; | ||
SecurityContext.prototype.getToken = function(namespace, name) { | ||
if (this.tokenContainsAttributes && this.isForeignMode) { | ||
debugTrace('\nThe SecurityContext has been initialized with an access token of a\n' | ||
this.getUniquePrincipalName = function (origin, logonName) { | ||
if(!ifNotClientCredentialsToken('SecurityContext.getUniquePrincipalName', true)) { | ||
return null; | ||
} | ||
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 getToken function does not return a valid token.\n'); | ||
return null; | ||
} | ||
if (namespace === undefined || namespace === null) { | ||
debugTrace('\nInvalid token namespace (may not be null or undefined).'); | ||
return null; | ||
} else if (namespace !== constants.SYSTEM) { | ||
debugTrace('\nNamespace "' + namespace + '" not supported.'); | ||
return null; | ||
} | ||
if (name === undefined || name === null) { | ||
debugTrace('\nInvalid token name (may not be null or undefined).'); | ||
return null; | ||
} | ||
switch (name) { | ||
case constants.JOBSCHEDULER: | ||
return this.token; | ||
case constants.HDB: | ||
if (this.samlToken === undefined || this.samlToken === null) { | ||
return this.token; | ||
} else { | ||
return this.samlToken; | ||
+ 'attributes, the getHdbToken function does not return a valid token.\n'); | ||
return null; | ||
} | ||
default: | ||
debugTrace('\nToken name "' + name + '" not supported.'); | ||
return null; | ||
} | ||
}; | ||
SecurityContext.prototype.getHdbToken = function() { | ||
return this.getToken(constants.SYSTEM, constants.HDB); | ||
} | ||
return samlToken ? samlToken : token; | ||
}; | ||
SecurityContext.prototype.getAppToken = function() { | ||
return this.token; | ||
} | ||
this.getAppToken = function () { | ||
return token; | ||
}; | ||
SecurityContext.prototype.requestTokenForClient = function(serviceCredentials, scopes, cb) { | ||
return requestUserToken(this, serviceCredentials, null, scopes, false, cb); | ||
} | ||
SecurityContext.prototype.requestToken = function(serviceCredentials, type, additionalAttributes, cb) { | ||
if (type === constants.TYPE_USER_TOKEN) { | ||
return requestUserToken(this, serviceCredentials, additionalAttributes, null, true, cb); | ||
} else if (type === constants.TYPE_CLIENT_CREDENTIALS_TOKEN) { | ||
return requestClientCredentialsToken(this, serviceCredentials, additionalAttributes, cb); | ||
} else { | ||
var error = new Error('Invalid grant type.'); | ||
return cb(error, null); | ||
} | ||
} | ||
SecurityContext.prototype.getAttribute = function(name) { | ||
try { | ||
forbidClientCredentialsToken('SecurityContext.getAttribute', this.grantType); | ||
} catch (e) { | ||
return null; | ||
} | ||
if (!this.tokenContainsAttributes) { | ||
debugTrace('\nThe access token contains no user attributes.\n'); | ||
return null; | ||
} | ||
if (this.isForeignMode) { | ||
debugTrace('\nThe SecurityContext has been initialized with an access token of a\n' | ||
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' | ||
@@ -495,386 +218,159 @@ + 'access token contains attributes. Due to the fact that we want to\n' | ||
+ 'attributes, the getAttribute function does not return any attributes.\n'); | ||
return null; | ||
} | ||
if (name === undefined || name === null || name === '') { | ||
debugTrace('\nInvalid attribute name (may not be null, empty, or undefined).'); | ||
return null; | ||
} | ||
if (!this.userAttributes.hasOwnProperty(name)) { | ||
debugTrace('\nNo attribute "' + name + '" found for user "' | ||
+ this.userInfo.logonName + '".'); | ||
return null; | ||
} | ||
return this.userAttributes[name]; | ||
}; | ||
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]; | ||
}; | ||
SecurityContext.prototype.getAdditionalAuthAttribute = function(name) { | ||
if (!this.tokenContainsAdditionalAuthAttributes) { | ||
debugTrace('\nThe access token contains no additional authentication attributes.\n'); | ||
return null; | ||
} | ||
if (name === undefined || name === null || name == '') { | ||
debugTrace('\nInvalid attribute name (may not be null, empty, or undefined).'); | ||
return null; | ||
} | ||
if (!this.additionalAuthAttributes.hasOwnProperty(name)) { | ||
debugTrace('\nNo attribute "' + name + '" found as additional authentication attribute.'); | ||
return null; | ||
} | ||
return this.additionalAuthAttributes[name]; | ||
}; | ||
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]; | ||
}; | ||
SecurityContext.prototype.getCloneServiceInstanceId = function() { | ||
return this.serviceinstanceid; | ||
}; | ||
this.getCloneServiceInstanceId = function () { | ||
return serviceinstanceid; | ||
}; | ||
SecurityContext.prototype.isInForeignMode = function() { | ||
return this.isForeignMode; | ||
}; | ||
this.isInForeignMode = function () { | ||
return isForeignMode; | ||
}; | ||
SecurityContext.prototype.hasAttributes = function() { | ||
try { | ||
forbidClientCredentialsToken('SecurityContext.hasAttributes', this.grantType); | ||
} catch (e) { | ||
return null; | ||
} | ||
return this.tokenContainsAttributes; | ||
}; | ||
this.hasAttributes = function () { | ||
return ifNotClientCredentialsToken('SecurityContext.hasAttributes', userAttributes ? true : false); | ||
}; | ||
SecurityContext.prototype.checkLocalScope = function(scope) { | ||
var scopeName = this.xsappname + '.' + scope; | ||
if ((scope === null) || (scope === undefined)) { | ||
return false; | ||
} | ||
return this.scopes.indexOf(scopeName) !== -1; | ||
}; | ||
this.checkLocalScope = function (scope) { | ||
if (!scope) { | ||
return false; | ||
} | ||
var scopeName = xsappname + '.' + scope; | ||
return scopes.indexOf(scopeName) !== -1; | ||
}; | ||
SecurityContext.prototype.getGrantType = function() { | ||
return this.grantType; | ||
}; | ||
this.getGrantType = function () { | ||
return grantType; | ||
}; | ||
SecurityContext.prototype.checkScope = function(scope) { | ||
var scopeName = scope; | ||
this.checkScope = function (scope) { | ||
if (!scope) { | ||
return false; | ||
} | ||
if ((scope === null) || (scope === undefined)) { | ||
return false; | ||
} | ||
if (scopeName.substring(0, constants.XSAPPNAMEPREFIX.length) === constants.XSAPPNAMEPREFIX) { | ||
scopeName = scopeName.replace(constants.XSAPPNAMEPREFIX, this.xsappname + '.'); | ||
} | ||
return this.scopes.indexOf(scopeName) !== -1; | ||
}; | ||
function loadVerificationKey(accessToken, config, cb) { | ||
if (config.verificationkey === undefined) { | ||
var error = new Error('Error in offline validation of access token, because of missing verificationkey', null); | ||
error.statuscode = 500; //500 (missing verificationkey) | ||
return cb(error); | ||
} | ||
var uaaURLString = config.url; | ||
var invalidatedTokenHeaderJSON = null; | ||
try { | ||
var invalidatedTokenParts = accessToken.split('.'); | ||
if (invalidatedTokenParts.length !== 3) { | ||
var error = new Error('Unexpected JWT structure.', null); | ||
error.statuscode = 400; | ||
return cb(error); | ||
if (scope.substring(0, constants.XSAPPNAMEPREFIX.length) === constants.XSAPPNAMEPREFIX) { | ||
scope = scope.replace(constants.XSAPPNAMEPREFIX, xsappname + '.'); | ||
} | ||
var invalidatedTokenHeaderBuffer = new Buffer(invalidatedTokenParts[0], 'base64'); | ||
var invalidatedTokenHeaderString = invalidatedTokenHeaderBuffer.toString('utf8'); | ||
invalidatedTokenHeaderJSON = JSON.parse(invalidatedTokenHeaderString); | ||
if (!invalidatedTokenHeaderJSON.kid || invalidatedTokenHeaderJSON.kid == 'legacy-token-key') { | ||
return cb(null, config.verificationkey); | ||
} | ||
var invalidatedTokenContentBuffer = new Buffer(invalidatedTokenParts[1], 'base64'); | ||
var invalidatedTokenContentString = invalidatedTokenContentBuffer.toString('utf8'); | ||
var invalidatedTokenContentJSON = JSON.parse(invalidatedTokenContentString); | ||
if (!invalidatedTokenContentJSON.iss) { | ||
var error = new Error('JWT token contains no iss field. Giving up.', null); | ||
error.statuscode = 400; | ||
return cb(error); | ||
} | ||
var tokenIssuer = invalidatedTokenContentJSON.iss; | ||
var tokenIssuerURL = url.parse(tokenIssuer); | ||
var tokenIssuerURLHostname = tokenIssuerURL.hostname; | ||
var tokenIssuerURLIDZIndex = tokenIssuerURLHostname.indexOf('.'); | ||
if (tokenIssuerURLIDZIndex < 0) { | ||
debugTrace('\nUnexpected Issuer Format in JWT. Use legacy-token-key.'); | ||
return cb(null, config.verificationkey); | ||
} | ||
var tokenIssuerIDZ = tokenIssuerURLHostname.substring(0, tokenIssuerURLIDZIndex); | ||
debugTrace('\nIdentity zone of token issuer: '+tokenIssuerIDZ+'\n'); | ||
var uaaURL = url.parse(uaaURLString); | ||
var uaaURLHostname = uaaURL.hostname; | ||
var uaaURLIDZIndex = uaaURLHostname.indexOf('.'); | ||
if (uaaURLIDZIndex < 0) { | ||
var error = new Error('Unexpected format of UAA URL in configuration. Giving up.', null); | ||
error.statuscode = 500; | ||
return cb(error); | ||
} | ||
var uaaURLHostnameWithoutIDZ = uaaURLHostname.substring(uaaURLIDZIndex, uaaURLHostname.length); | ||
var newHostname = tokenIssuerIDZ + uaaURLHostnameWithoutIDZ; | ||
uaaURL.hostname = newHostname; | ||
uaaURL.host = null; | ||
uaaURLString = url.format(uaaURL); | ||
return keyCache.getKey(invalidatedTokenHeaderJSON.kid, uaaURLString, cb); | ||
} catch (e) { | ||
e.statuscode = 403; | ||
return cb(e); | ||
} | ||
} | ||
return scopes.indexOf(scope) !== -1; | ||
}; | ||
function checkTokenLocal(accessToken, verificationkey, ssojwt, cb) { | ||
var ssorc = ssojwt.loadPEM(verificationkey); | ||
if ((ssorc !== 0) && (ssorc === 9)) { | ||
debugTrace('\nSSO library path: ' + process.env['SSOEXT_LIB']); | ||
debugTrace('\nCCL library path: ' + process.env['SSF_LIB']); | ||
debugTrace('\nSSO library version: ' + ssojwt.getLibraryVersion()); | ||
debugTrace('\nSSO library code: ' + ssojwt.getErrorRC()); | ||
var error = new Error('Error in sapssoext, version : ' | ||
+ ssojwt.getLibraryVersion() + ' . Cannot load CCL from path: ' | ||
+ process.env['SSF_LIB'], null); | ||
error.statuscode = 500; //500 (lib not found) | ||
return cb(error); | ||
} | ||
ssojwt.checkToken(accessToken); | ||
if (ssojwt.getErrorDescription() !== "") { | ||
ssorc = ssojwt.getErrorRC(); | ||
debugTrace('\nSSO library path: ' + process.env['SSOEXT_LIB']); | ||
debugTrace('\nCCL library path: ' + process.env['SSF_LIB']); | ||
debugTrace('\nSSO library version: ' + ssojwt.getLibraryVersion()); | ||
debugTrace('\nSSO library code: ' + ssojwt.getErrorRC()); | ||
if ((ssorc !== 0) && (ssorc === 5)) { | ||
// verification key and JWT are not valid, no library error | ||
debugTrace('\nInvalid JWT: ' + accessToken); | ||
var error = new Error( | ||
'Invalid access token. Validation error: ' | ||
+ ssojwt.getErrorDescription(), null); | ||
error.statuscode = 403; //403 (validation error) | ||
return cb(error); | ||
this.requestToken = function (serviceCredentials, type, additionalAttributes, cb) { | ||
if (type === constants.TYPE_USER_TOKEN) { | ||
return requests.requestUserToken(this, serviceCredentials, additionalAttributes, null, true, cb); | ||
} else if (type === constants.TYPE_CLIENT_CREDENTIALS_TOKEN) { | ||
return requests.requestClientCredentialsToken(this, serviceCredentials, additionalAttributes, cb); | ||
} else { | ||
var error = new Error( | ||
'Error in offline validation of access token: ' | ||
+ ssojwt.getErrorDescription(), null); | ||
error.statuscode = 403; //403 (validation error) | ||
return cb(error); | ||
return cb(new Error('Invalid grant type.')); | ||
} | ||
} | ||
var parsedPayload = null; | ||
try { | ||
parsedPayload = JSON.parse(ssojwt.getJWPayload()); | ||
} catch (er) { | ||
var errorString = 'Access token payload could not be parsed successfully.\n' | ||
+ 'Error: ' + er.message; | ||
debugError('\n' + errorString); | ||
var error = new Error(errorString); | ||
error.statuscode = 400; //400 (parsing error) | ||
return cb(error, null); | ||
} | ||
cb(null, parsedPayload); | ||
} | ||
}; | ||
function offlineValidation(accessToken, config, ssojwt, cb) { | ||
loadVerificationKey(accessToken, config, function(err, verificationkey) { | ||
if (err) { | ||
return cb(err); | ||
} | ||
checkTokenLocal(accessToken, verificationkey, ssojwt, cb); | ||
}); | ||
} | ||
function fillContext(encodedToken, decodedToken) { | ||
debugTrace('\nApplication received a token of grant type "' + decodedToken.grant_type + '".'); | ||
function credentialsReplacer(key, value) { | ||
if (key === 'clientsecret') { | ||
return undefined; | ||
} else { | ||
return value; | ||
} | ||
} | ||
token = encodedToken; | ||
scopes = decodedToken.scope; | ||
identityZone = decodedToken.zid; | ||
clientId = decodedToken.cid; | ||
expirationDate = new Date(decodedToken.exp * 1000); | ||
grantType = decodedToken.grant_type; | ||
function requestUserToken(securityContext, serviceCredentials, additionalAttributes, scopes, adaptSubdomain, cb) { | ||
// input validation | ||
if (!serviceCredentials) { | ||
var error = new Error('Parameter serviceCredentials is missing but mandatory.'); | ||
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); | ||
} | ||
if (securityContext.checkScope('uaa.user') === false) { | ||
var error = new Error('JWT token does not include scope "uaa.user".'); | ||
return cb(error, null); | ||
} | ||
// adapt subdomain in service url, if necessary | ||
var urlWithCorrectSubdomain = serviceCredentials.url; | ||
if (adaptSubdomain === true) { | ||
var tokenSubdomain = securityContext.getSubdomain(); | ||
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); | ||
} | ||
} | ||
// user token flow | ||
var options = { | ||
url : urlWithCorrectSubdomain + '/oauth/token?grant_type=user_token&response_type=token&client_id=' + serviceCredentials.clientid, | ||
headers: { Accept: 'application/json' }, | ||
auth: { | ||
bearer: securityContext.token | ||
} | ||
}; | ||
if (scopes !== null) { | ||
options.url = options.url + "&scope=" + scopes; | ||
} | ||
if (additionalAttributes !== null) { | ||
var authorities = { "az_attr" : additionalAttributes }; | ||
options.url = options.url + "&authorities=" + encodeURIComponent(JSON.stringify(authorities)); | ||
} | ||
request.post( | ||
options, | ||
function(error, response, body) { | ||
if (error) { | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error, null); | ||
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; | ||
} | ||
if (response.statusCode === 401) { | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: user_token). Bearer token invalid, requesting client does not have grant_type=user_token or no scopes were granted.'); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: user_token). Bearer token invalid, requesting client does not have grant_type=user_token or no scopes were granted.'), 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; | ||
} | ||
if (response.statusCode !== 200) { | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: user_token). HTTP status code: ' + response.statusCode); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: user_token). HTTP status code: ' + response.statusCode), null); | ||
if(userAttributes) { | ||
debugTrace('\nObtained attributes: ' + JSON.stringify(userAttributes, null, 4)); | ||
} else { | ||
debugTrace('\nObtained attributes: no XS user attributes in JWT token available.'); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
} catch (e) { | ||
return cb(e, null); | ||
} | ||
// refresh token flow | ||
options = { | ||
url : urlWithCorrectSubdomain + '/oauth/token?grant_type=refresh_token&refresh_token=' + json.refresh_token, | ||
headers: { Accept: 'application/json' }, | ||
auth: { | ||
user: serviceCredentials.clientid, | ||
pass: serviceCredentials.clientsecret | ||
} | ||
}; | ||
request.post( | ||
options, | ||
function(error, response, body) { | ||
if (error) { | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error, null); | ||
} | ||
if (response.statusCode === 401) { | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: refresh_token). Client credentials invalid.'); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: refresh_token). Client credentials invalid.'), null); | ||
} | ||
if (response.statusCode !== 200) { | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: refresh_token). HTTP status code: ' + response.statusCode); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: refresh_token). HTTP status code ' + response.statusCode), null); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
} catch (e) { | ||
return cb(e, null); | ||
} | ||
return cb(null, json.access_token) | ||
} | ||
); | ||
} | ||
); | ||
} | ||
function requestClientCredentialsToken(securityContext, serviceCredentials, additionalAttributes, cb) { | ||
// input validation | ||
if (!serviceCredentials) { | ||
var error = new Error('Parameter serviceCredentials is missing but mandatory.'); | ||
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 = securityContext.getSubdomain(); | ||
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?grant_type=client_credentials&response_type=token', | ||
headers: { 'Accept' : 'application/json', 'Content-Type' : 'application/x-www-form-urlencoded' }, | ||
auth: { | ||
user: serviceCredentials.clientid, | ||
pass: serviceCredentials.clientsecret | ||
additionalAuthAttributes = decodedToken.az_attr || null; | ||
if(additionalAuthAttributes) { | ||
debugTrace('\nObtained additional authentication attributes: ' + JSON.stringify(additionalAuthAttributes, null, 4)); | ||
} else { | ||
debugTrace('\nObtained attributes: no additional authentication attributes in JWT token available.'); | ||
} | ||
}; | ||
if (additionalAttributes !== null) { | ||
var authorities = { "az_attr" : additionalAttributes }; | ||
options.url = options.url + "&authorities=" + encodeURIComponent(JSON.stringify(authorities)); | ||
} | ||
request.post( | ||
options, | ||
function(error, response, body) { | ||
if (error) { | ||
debugError(error.message); | ||
debugError(error.stack); | ||
return cb(error, null); | ||
} | ||
if (response.statusCode === 401) { | ||
debugTrace(body); | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: client_credentials). Client credentials invalid.'); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: client_credentials). Client credentials invalid.'), null); | ||
} | ||
if (response.statusCode !== 200) { | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: client_credentials). HTTP status code: ' + response.statusCode); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: client_credentials). HTTP status code ' + response.statusCode), null); | ||
} | ||
var json = null; | ||
try { | ||
json = JSON.parse(body); | ||
} catch (e) { | ||
return cb(e, null); | ||
} | ||
return cb(null, json.access_token); | ||
if(decodedToken.ext_attr) { | ||
serviceinstanceid = decodedToken.ext_attr.serviceinstanceid || null; | ||
subdomain = decodedToken.ext_attr.zdn || null; | ||
} | ||
); | ||
}; | ||
function forbidClientCredentialsToken(functionName, grantType) { | ||
if (grantType === constants.GRANTTYPE_CLIENTCREDENTIAL) { | ||
var errorString = '\nCall to ' + functionName + ' not allowed with a token of grant type ' + constants.GRANTTYPE_CLIENTCREDENTIAL + '.'; | ||
debugTrace(errorString); | ||
throw new Error(errorString); | ||
debugTrace('\nObtained subdomain: ' + this.getSubdomain()); | ||
debugTrace('Obtained serviceinstanceid: ' + this.getCloneServiceInstanceId()); | ||
debugTrace('Obtained origin: ' + this.getOrigin()); | ||
debugTrace('Obtained scopes: ' + JSON.stringify(scopes, null, 4)); | ||
} | ||
}; | ||
this.verifyToken = function (encodedToken, cb) { | ||
var verificationKey = new VerificationKey(config); | ||
var jwtValidator = new JwtTokenValidator(verificationKey, config); | ||
//Now validate the tokens | ||
jwtValidator.validateToken(encodedToken, function(err, decodedToken) { | ||
if(err) { | ||
return cb(err); | ||
} | ||
isForeignMode = jwtValidator.isForeignMode(); | ||
//Token is now validated. So just fill local variables | ||
fillContext.call(this, encodedToken, decodedToken); | ||
cb(null, this); | ||
}.bind(this)); | ||
}; | ||
//call constructor | ||
ctor(); | ||
}; |
@@ -1,1 +0,1 @@ | ||
{"bundleDependencies":false,"dependencies":{"@sap/node-jwt":"^1.4.13","@sap/xsenv":"^1.2.9","debug":"3.1.0","lru-cache":"4.1.1","request":"2.88.0","valid-url":"1.0.9"},"deprecated":false,"description":"XS Advanced Container Security API for node.js","devDependencies":{"filter-node-package":"2.0.0","istanbul":"^0.4.5","jwt-decode":"^2.2.0","mocha":"^5.1.0","should":"^13.2.1"},"keywords":["xs"],"main":"./lib","name":"@sap/xssec","repository":{"type":"git"},"scripts":{"prepareRelease":"clean-packages && npm prune --production","test":"make test"},"version":"2.1.16","license":"SEE LICENSE IN developer-license-3.1.txt"} | ||
{"bundleDependencies":false,"dependencies":{"debug":"4.1.1","jsonwebtoken":"^8.5.1","lru-cache":"5.1.1","request":"2.88.0","requestretry":"4.0.0","valid-url":"1.0.9"},"deprecated":false,"description":"XS Advanced Container Security API for node.js","devDependencies":{"@sap/xsenv":"^2.2.0","istanbul":"^0.4.5","jwt-decode":"^2.2.0","mocha":"^5.1.0","should":"^13.2.1"},"keywords":["xs"],"main":"./lib","name":"@sap/xssec","repository":{"type":"git","url":"ssh://git@github.wdf.sap.corp/xs2/node-xs2sec.git"},"scripts":{"prepareRelease":"npm prune --production","test":"make test"},"version":"3.0.3","license":"SEE LICENSE IN developer-license-3.1.txt"} |
@@ -14,9 +14,4 @@ @sap/xssec: XS Advanced Container Security API for node.js | ||
sap-xssec offers an offline validation of the access token, which requires no additional call to the UAA. The trust for this offline validation is created by binding the XS UAA service instance to your application. Inside the credentials section in the environment variable VCAP_SERVICES, the key for validation of tokens is included. By default, the offline validation check will only accept tokens intended for the same OAuth2 client in the same UAA identity zone. This makes sense and will cover the vast majority of use cases. However, if an application absolutely wants to consume token that were issued for either different OAuth2 clients or different identity zones, an Access Control List (ACL) entry for this can be specified in an environment variable named SAP_JWT_TRUST_ACL. The name of the OAuth client is sb-<xsappname from xs-security.json> | ||
The content is a JSON String, containing an array of identity zones and OAuth2 clients. To trust any OAuth2 client and/or identity zones, an * can be used. For OP, identity zones are not used and value for the identity zone is uaa. | ||
sap-xssec offers an offline validation of the access token, which requires no additional call to the UAA. The trust for this offline validation is created by binding the XS UAA service instance to your application. Inside the credentials section in the environment variable VCAP_SERVICES, the key for validation of tokens is included. By default, the offline validation check will only accept tokens intended for the same OAuth2 client in the same UAA identity zone. This makes sense and will cover the vast majority of use cases. | ||
```JSON | ||
SAP_JWT_TRUST_ACL: [ {"clientid":"<client-id of the OAuth2 client>","identityzone":"<identity zone>"},...] | ||
``` | ||
If you want to enable another (foreign) application to use some of your application's scopes, you can add a ```granted-apps``` marker to your scope in the ```xs-security.json``` file (as in the following example). The value of the marker is a list of applications that is allowed to request a token with the denoted scope. | ||
@@ -63,3 +58,3 @@ | ||
```js | ||
xssec.createSecurityContext(access_token, xsenv.getServices({ uaa: 'uaa' }).uaa, function(error, securityContext) { | ||
xssec.createSecurityContext(access_token, xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa, function(error, securityContext) { | ||
if (error) { | ||
@@ -96,3 +91,3 @@ console.log('Security Context creation failed'); | ||
passport.use(new JWTStrategy(xsenv.getServices({uaa:{tag:'xsuaa'}}).uaa)); | ||
passport.use(new JWTStrategy(xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa)); | ||
@@ -184,2 +179,6 @@ app.use(passport.initialize()); | ||
### Usage in Docker | ||
If you intend to use XS Advanced Container Security inside a Docker image make sure to use a base image other than **alpine**. The alpine base system is lacking needed symbols for system calls. | ||
## API Description | ||
@@ -217,2 +216,10 @@ | ||
### getUserName | ||
returns unique principal name of a user `user/<origin>/<logon name>` or client id that the access token has been issued for `client/<client id>` | ||
### getUniquePrincipalName | ||
not available for tokens of grant_type `client_credentials`, returns unique principal name of a user. `user/<origin>/<logon name>` | ||
### getOrigin | ||
@@ -240,10 +247,2 @@ | ||
### getToken (obsolete, use getHdbToken or getAppToken) | ||
Parameters: | ||
* `namespace` ... Tokens can eventually be used in different contexts, e.g. to access the HANA database, to access another XS2-based service such as the Job Scheduler, or even to access other applications/containers. To differentiate between these use cases, the `namespace` is used. In `lib/constants.js` we define supported namespaces (e.g. `SYSTEM`). | ||
* `name` ... The name is used to differentiate between tokens in a given namespace, e.g. `HDB` for HANA database or `JOBSCHEDULER` for the job scheduler. These names are also defined in the file `lib/constants.js`. | ||
* returns a token that can be used e.g. for contacting the HANA database. If the token, that the security context has been instantiated with, is a foreign token (meaning that the OAuth client contained in the token and the OAuth client of the current application do not match), `null` is returned instead of a token. | ||
### getAppToken | ||
@@ -266,12 +265,2 @@ | ||
### requestTokenForClient (obsolete, use requestToken instead) | ||
Requests a token with `grant_type=user_token` from another client. Prerequisite is that the requesting client has `grant_type=user_token` and that the current user token includes the scope `uaa.user`. | ||
Parameters: | ||
* `serviceCredentials` ... the credentials of the service as JSON object. The attributes `clientid`, `clientsecret` and `url` (UAA) are mandatory. | ||
* `scopes` ... comma-separated list of requested scopes for the token, e.g. `app.scope1,app.scope2`. If null, all scopes are granted. Note that $XSAPPNAME is not supported as part of the scope names. | ||
* `cb(error, token)` ... callback function | ||
### hasAttributes | ||
@@ -311,6 +300,2 @@ | ||
### getIdentityZone (obsolete, use getSubaccountId instead) | ||
* returns the identity zone that the access token has been issued for. | ||
### getSubaccountId | ||
@@ -331,1 +316,8 @@ | ||
* returns the grant type of the JWT token, e.g. `authorization_code`, `password`, `client_credentials` or `urn:ietf:params:oauth:grant-type:saml2-bearer`. | ||
## Latest published Version | ||
Use this command to check for the latest version that is published to the NPM repository: | ||
``` | ||
npm view --registry https://npm.sap.com @sap/xssec versions | ||
``` |
Sorry, the diff of this file is not supported yet
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
284163
13
5
1041
315
1
+ Addedjsonwebtoken@^8.5.1
+ Addedrequestretry@4.0.0
+ Addedbuffer-equal-constant-time@1.0.1(transitive)
+ Addeddebug@4.1.1(transitive)
+ Addedecdsa-sig-formatter@1.0.11(transitive)
+ Addedjsonwebtoken@8.5.1(transitive)
+ Addedjwa@1.4.1(transitive)
+ Addedjws@3.2.2(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedlodash.includes@4.3.0(transitive)
+ Addedlodash.isboolean@3.0.3(transitive)
+ Addedlodash.isinteger@4.0.4(transitive)
+ Addedlodash.isnumber@3.0.3(transitive)
+ Addedlodash.isplainobject@4.0.6(transitive)
+ Addedlodash.isstring@4.0.1(transitive)
+ Addedlodash.once@4.1.1(transitive)
+ Addedlru-cache@5.1.1(transitive)
+ Addedms@2.1.3(transitive)
+ Addedrequestretry@4.0.0(transitive)
+ Addedsemver@5.7.2(transitive)
+ Addedwhen@3.7.8(transitive)
+ Addedyallist@3.1.1(transitive)
- Removed@sap/node-jwt@^1.4.13
- Removed@sap/xsenv@^1.2.9
- Removed@sap/node-jwt@1.6.26(transitive)
- Removed@sap/xsenv@1.3.0(transitive)
- Removeddebug@3.1.0(transitive)
- Removedlru-cache@4.1.1(transitive)
- Removedms@2.0.0(transitive)
- Removedpseudomap@1.0.2(transitive)
- Removedyallist@2.1.2(transitive)
Updateddebug@4.1.1
Updatedlru-cache@5.1.1