@sap/xssec
Advanced tools
Comparing version 3.0.3 to 3.0.5
@@ -89,3 +89,3 @@ // Scope Prefix: | ||
Object.defineProperty(exports, "KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES", { | ||
value: 15, | ||
value: 10, | ||
enumerable: true, | ||
@@ -97,3 +97,3 @@ writable: false, | ||
Object.defineProperty(exports, "KEYCACHE_DEFAULT_CACHE_SIZE", { | ||
value: 100, | ||
value: 1000, | ||
enumerable: true, | ||
@@ -103,1 +103,8 @@ writable: false, | ||
}); | ||
Object.defineProperty(exports, "USER_AGENT", { | ||
value: "nodejs-xssec-3", | ||
enumerable: true, | ||
writable: false, | ||
configurable: false | ||
}); |
@@ -18,4 +18,3 @@ 'use strict'; | ||
function KeyCache(cacheSize, cacheEntryExpirationTimeInMinutes, | ||
callUaaToReadTokenKeys, tokenKeyPath) { | ||
function KeyCache(cacheSize, cacheEntryExpirationTimeInMinutes, callUaaToReadTokenKeys, tokenKeyPath) { | ||
debugTrace('Initializing KeyCache with parameters cacheSize (' + cacheSize | ||
@@ -83,3 +82,3 @@ + '), cacheEntryExpirationTimeInMinutes (' | ||
KeyCache.prototype.getKey = function getKey(tokenKeyUrl, keyId, cb) { | ||
KeyCache.prototype.getKey = function getKey(tokenKeyUrl, keyId, zid, cb) { | ||
var self = this; | ||
@@ -130,7 +129,12 @@ | ||
timeout: 2000, | ||
headers: { | ||
"User-Agent": constants.USER_AGENT, | ||
}, | ||
maxAttempts: 3, | ||
retryDelay: 500, | ||
followRedirect: false, | ||
retryStrategy: request.RetryStrategies.HTTPOrNetworkError | ||
}; | ||
debugTrace('Key "' + cacheKey + '" 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 + '" and zid: "' + zid + '".'); | ||
request | ||
@@ -137,0 +141,0 @@ .get( |
@@ -48,3 +48,6 @@ 'use strict'; | ||
'&response_type=token&client_id=' + serviceCredentials.clientid + '&assertion=' + securityContext.getAppToken(), | ||
headers: { Accept: 'application/json' }, | ||
headers: { | ||
'Accept': 'application/json', | ||
'User-Agent': constants.USER_AGENT | ||
}, | ||
auth: { | ||
@@ -56,2 +59,3 @@ user: serviceCredentials.clientid, | ||
}; | ||
if (scopes !== null) { | ||
@@ -64,2 +68,4 @@ options.url = options.url + "&scope=" + scopes; | ||
} | ||
debugTrace('requestUserToken::HTTP Call with %O', options); | ||
request.post( | ||
@@ -78,2 +84,3 @@ options, | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer). Bearer token invalid, requesting client does not have the grant_type or no scopes were granted.'); | ||
debugTrace(body); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer). Bearer token invalid, requesting client does not have the grant_type or no scopes were granted.'), null); | ||
@@ -83,2 +90,3 @@ } | ||
debugTrace('requestToken: Call to /oauth/token was not successful (grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer). HTTP status code: ' + response.statusCode); | ||
debugTrace(body); | ||
return cb(new Error('Call to /oauth/token was not successful (grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer). HTTP status code: ' + response.statusCode), null); | ||
@@ -128,3 +136,7 @@ } | ||
url : urlWithCorrectSubdomain + '/oauth/token?grant_type=client_credentials&response_type=token', | ||
headers: { 'Accept' : 'application/json', 'Content-Type' : 'application/x-www-form-urlencoded' }, | ||
headers: { | ||
'Accept' : 'application/json', | ||
'Content-Type' : 'application/x-www-form-urlencoded', | ||
'User-Agent': constants.USER_AGENT | ||
}, | ||
auth: { | ||
@@ -140,2 +152,3 @@ user: serviceCredentials.clientid, | ||
} | ||
debugTrace('requestClientCredentialsToken::HTTP Call with %O', options); | ||
request.post( | ||
@@ -142,0 +155,0 @@ options, |
@@ -60,4 +60,6 @@ 'use strict'; | ||
var self = this; | ||
xssec.createSecurityContext(token, this.options, function (err, ctx) { | ||
if (err) { | ||
xssec.createSecurityContext(token, this.options, function (err, ctx, tokenInfo) { | ||
req.tokenInfo = tokenInfo; | ||
if (err) { | ||
return err.statuscode ? self.fail(err.statuscode) : self.error(err); | ||
@@ -64,0 +66,0 @@ } |
@@ -10,2 +10,3 @@ 'use strict'; | ||
const jwt = require('jsonwebtoken'); | ||
const tokenInfo = require('./tokeninfo') | ||
@@ -54,6 +55,6 @@ const DOT = "."; | ||
this.validateToken = function (audiencesFromToken) { | ||
this.validateToken = function (audiencesFromToken, scopesFromToken, cid) { | ||
foreignMode = false; | ||
var allowedAudiences = getAllowedAudiencesFromToken(audiencesFromToken); | ||
if (validateAudienceOfXsuaaBrokerClone(allowedAudiences) === true || validateDefault(allowedAudiences) === true) { | ||
var allowedAudiences = getAllowedAudiencesFromToken(audiencesFromToken, scopesFromToken || []); | ||
if (validateSameClientId(cid) === true || validateAudienceOfXsuaaBrokerClone(allowedAudiences) === true || validateDefault(allowedAudiences) === true) { | ||
return ValidationResults.createValid(); | ||
@@ -69,2 +70,9 @@ } | ||
function validateSameClientId(cid) { | ||
if(!cid || !clientId) { | ||
return false; | ||
} | ||
return clientId.trim() === cid.trim(); | ||
} | ||
//iterate over all configured clientIds and return true of the cb returns true | ||
@@ -107,3 +115,7 @@ function forEachClientId(cb) { | ||
function getAllowedAudiencesFromToken(aud) { | ||
this.getListOfAudiencesFromToken = function(aud, scopes) { | ||
return getAllowedAudiencesFromToken(aud || [], scopes || []); | ||
} | ||
function getAllowedAudiencesFromToken(aud, scopes) { | ||
var audiences = []; | ||
@@ -118,3 +130,3 @@ var tokenAudiences = aud || []; | ||
var aud = audience.substring(0, audience.indexOf(DOT)).trim(); | ||
if (aud && aud.length > 0) { | ||
if (aud && !audiences.includes(aud)) { | ||
audiences.push(aud); | ||
@@ -126,2 +138,14 @@ } | ||
} | ||
if (audiences.length == 0) { | ||
for(var i=0;i < scopes.length;++i) { | ||
var scope = scopes[i]; | ||
if (scope.indexOf(DOT) >-1) { | ||
var aud = scope.substring(0, scope.indexOf(DOT)).trim(); | ||
if(aud && !audiences.includes(aud)) { | ||
audiences.push(aud); | ||
} | ||
} | ||
} | ||
} | ||
return audiences; | ||
@@ -152,13 +176,13 @@ } | ||
jwt.verify(accessToken, | ||
verificationKey.getCallback(), | ||
{ | ||
algorithms: ['RS256'] //we currently only allow RS256 | ||
}, | ||
function (err, decodedToken) { | ||
var token = new tokenInfo(accessToken); | ||
token.verify(verificationKey.getCallback(token), | ||
function (err, token) { | ||
var decodedToken = token.getPayload(); | ||
if (err) { | ||
debugError(err.statuscode); | ||
debugError(err.message); | ||
debugError(err.stack); | ||
err.statuscode = accessToken ? 403 : 401; | ||
return cb(err); | ||
debugError(err.stack); | ||
return cb(err, token); | ||
} | ||
@@ -179,5 +203,5 @@ | ||
var valid_result = audienceValidator.validateToken(decodedToken.aud); | ||
var valid_result = audienceValidator.validateToken(decodedToken.aud, decodedToken.scope, decodedToken.cid); | ||
if (!valid_result.isValid()) { | ||
return returnError(403, valid_result.getErrorDescription()); | ||
return returnError(401, valid_result.getErrorDescription()); | ||
} | ||
@@ -189,4 +213,5 @@ | ||
cb(null, decodedToken); | ||
}); | ||
cb(null, token); | ||
} | ||
); | ||
}; | ||
@@ -193,0 +218,0 @@ }; |
@@ -11,11 +11,9 @@ 'use strict'; | ||
// 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. | ||
const keyCache = new keycache.KeyCache(constants.KEYCACHE_DEFAULT_CACHE_SIZE, constants.KEYCACHE_DEFAULT_CACHE_ENTRY_EXPIRATION_TIME_IN_MINUTES); | ||
//keyCache is now configureable using the config object | ||
var keyCache = null; | ||
function VerificationKey(config) { | ||
this.getCallback = function() { | ||
var tokenInfo = null; | ||
this.getCallback = function(token) { | ||
tokenInfo = token; | ||
return this.loadKey.bind(this); | ||
@@ -48,2 +46,3 @@ } | ||
this.loadKey = function(accessToken, cb) { | ||
var zid = tokenInfo.getPayload().zid; | ||
if (!accessToken.kid || accessToken.kid == 'legacy-token-key' || !accessToken.jku) { | ||
@@ -60,4 +59,8 @@ return cb(null, cleanUp(config.verificationkey)); | ||
if(!keyCache) { | ||
keyCache = new keycache.KeyCache(config.keyCache.cacheSize, config.keyCache.expirationTime); | ||
} | ||
//try to get a key from KeyCache | ||
keyCache.getKey(accessToken.jku, accessToken.kid, function(err, key) { | ||
keyCache.getKey(accessToken.jku, accessToken.kid, zid, function(err, key) { | ||
if (err) { | ||
@@ -64,0 +67,0 @@ debugTrace('\n', err); |
@@ -49,3 +49,4 @@ 'use strict'; | ||
var clientId; | ||
var identityZone; | ||
var subaccountid; | ||
var zid; | ||
var subdomain = null; | ||
@@ -59,2 +60,4 @@ var origin = null; | ||
var tokenInfo = null; | ||
var isForeignMode = false; | ||
@@ -114,2 +117,32 @@ | ||
} | ||
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 { | ||
config.keyCache.cacheSize = constants.KEYCACHE_DEFAULT_CACHE_SIZE; | ||
} | ||
debugTrace("Using KeyCache with custom values %o", config.keyCache); | ||
} | ||
} | ||
@@ -127,5 +160,9 @@ | ||
this.getSubaccountId = function () { | ||
return identityZone; | ||
return subaccountid; | ||
}; | ||
this.getZoneId = function () { | ||
return zid; | ||
}; | ||
this.getSubdomain = function () { | ||
@@ -200,3 +237,3 @@ return subdomain; | ||
return samlToken ? samlToken : token; | ||
return samlToken ? samlToken : this.getAppToken(); | ||
}; | ||
@@ -208,2 +245,6 @@ | ||
this.getTokenInfo = function() { | ||
return tokenInfo; | ||
} | ||
this.getAttribute = function (name) { | ||
@@ -265,3 +306,3 @@ if(!ifNotClientCredentialsToken('SecurityContext.getAttribute', true)) { | ||
this.checkLocalScope = function (scope) { | ||
if (!scope) { | ||
if (!scope || !scopes) { | ||
return false; | ||
@@ -278,3 +319,3 @@ } | ||
this.checkScope = function (scope) { | ||
if (!scope) { | ||
if (!scope || !scopes) { | ||
return false; | ||
@@ -299,8 +340,28 @@ } | ||
function fillContext(encodedToken, decodedToken) { | ||
function cleanUpUserAttributes(attr) { | ||
if(!attr) { | ||
return null; | ||
} | ||
var len = 0; | ||
for(var n in attr) { | ||
++len; | ||
} | ||
return len == 0 ? null : attr; | ||
} | ||
function fillContext(encodedToken, info) { | ||
tokenInfo = info; | ||
var decodedToken = tokenInfo.getPayload(); | ||
debugTrace('\nApplication received a token of grant type "' + decodedToken.grant_type + '".'); | ||
token = encodedToken; | ||
scopes = decodedToken.scope; | ||
identityZone = decodedToken.zid; | ||
scopes = decodedToken.scope || []; | ||
zid = decodedToken.zid; | ||
subaccountid = decodedToken["ext_attr"] ? decodedToken["ext_attr"].subaccountid : zid; | ||
if(!subaccountid) { | ||
subaccountid = zid; | ||
} | ||
clientId = decodedToken.cid; | ||
@@ -337,2 +398,4 @@ expirationDate = new Date(decodedToken.exp * 1000); | ||
userAttributes = cleanUpUserAttributes(userAttributes); | ||
if(userAttributes) { | ||
@@ -368,5 +431,5 @@ debugTrace('\nObtained attributes: ' + JSON.stringify(userAttributes, null, 4)); | ||
//Now validate the tokens | ||
jwtValidator.validateToken(encodedToken, function(err, decodedToken) { | ||
jwtValidator.validateToken(encodedToken, function(err, tokenInfo) { | ||
if(err) { | ||
return cb(err); | ||
return cb(err, null, tokenInfo); | ||
} | ||
@@ -377,5 +440,5 @@ | ||
//Token is now validated. So just fill local variables | ||
fillContext.call(this, encodedToken, decodedToken); | ||
fillContext.call(this, encodedToken, tokenInfo); | ||
cb(null, this); | ||
cb(null, this, tokenInfo); | ||
}.bind(this)); | ||
@@ -382,0 +445,0 @@ }; |
@@ -1,1 +0,37 @@ | ||
{"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"} | ||
{ | ||
"name": "@sap/xssec", | ||
"version": "3.0.5", | ||
"description": "XS Advanced Container Security API for node.js", | ||
"main": "./lib", | ||
"scripts": { | ||
"test": "make test", | ||
"prepareRelease": "npm prune --production" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "ssh://git@github.wdf.sap.corp/xs2/node-xs2sec.git" | ||
}, | ||
"files": [ | ||
"lib", | ||
"package.json", | ||
"README.md" | ||
], | ||
"keywords": [ | ||
"xs" | ||
], | ||
"devDependencies": { | ||
"mocha": "^5.1.0", | ||
"istanbul": "^0.4.5", | ||
"should": "^13.2.1", | ||
"jwt-decode": "^2.2.0", | ||
"@sap/xsenv": "^2.2.0" | ||
}, | ||
"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" | ||
} | ||
} |
@@ -57,3 +57,3 @@ @sap/xssec: XS Advanced Container Security API for node.js | ||
```js | ||
xssec.createSecurityContext(access_token, xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa, function(error, securityContext) { | ||
xssec.createSecurityContext(access_token, xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa, function(error, securityContext, tokenInfo) { | ||
if (error) { | ||
@@ -64,2 +64,3 @@ console.log('Security Context creation failed'); | ||
console.log('Security Context created successfully'); | ||
console.log(tokenInfo.getPublicClaims()); | ||
}); | ||
@@ -74,3 +75,2 @@ ``` | ||
### Usage with Passport Strategy | ||
@@ -106,2 +106,3 @@ | ||
* request.authInfo - the [Security Context](#api-description) | ||
* request.tokenInfo - the [TokenInfo](doc/TokenInfo.md) object | ||
@@ -111,3 +112,5 @@ If the `client_credentials` JWT token is present in the request and it is successfully verified, following objects are created: | ||
* request.authInfo - the [Security Context](#api-description) | ||
* request.tokenInfo - the [TokenInfo](doc/TokenInfo.md) object | ||
#### Session | ||
@@ -119,2 +122,54 @@ | ||
### Configure the cache of Verificationkeys | ||
For token verification the library needs a so called `public key`. This key can be requested from the XSUAA server. | ||
The library caches these keys to reduce the load to the XSUAA. (And for better performance!) | ||
There are two values that are used to control the cache. The number of cache entries and an expiration time of each item. The latter is important to easily support key rotation scenarios and should not be too high. | ||
:exclamation: **Normally you don't need to overwrite the default values!** | ||
But in rare situations there is a need to change them. | ||
**Conditions:** | ||
The cacheSize value has to be >=1000 | ||
The expirationTime is measured in minutes and has to be a number >= 10 | ||
**Currently the default values are:** | ||
```json | ||
{ | ||
"cacheSize": 1000, | ||
"expirationTime": 10 | ||
} | ||
``` | ||
**Here are some codesnippets how to do this:** | ||
```js | ||
//just add the keyCache object into the config object and pass it to the constructor functions | ||
var config = xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa; | ||
config.keyCache = { | ||
cacheSize: 5000, | ||
expirationTime: 10 | ||
}; | ||
//if you only want to overwrite one value you can also: | ||
var config = xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa; | ||
config.keyCache = { | ||
cacheSize: 10000 | ||
}; | ||
//then pass the config object to createSecurityConfig | ||
xssec.createSecurityContext(access_token, config, function(error, securityContext, tokenInfo) { | ||
... | ||
}); | ||
//if you use passport: | ||
var config = xsenv.getServices({xsuaa:{tag:'xsuaa'}}).xsuaa; | ||
config.keyCache = { | ||
cacheSize: 10000 | ||
}; | ||
passport.use(new JWTStrategy(config)); | ||
... | ||
``` | ||
### Test Usage without having an Access Token | ||
@@ -146,2 +201,3 @@ | ||
} | ||
var json = null; | ||
@@ -153,3 +209,4 @@ try { | ||
} | ||
xssec.createSecurityContext(json.access_token, uaaService, function(error, securityContext) { | ||
xssec.createSecurityContext(json.access_token, uaaService, function(error, securityContext, tokenInfo) { | ||
if (error) { | ||
@@ -160,2 +217,3 @@ console.log('Security Context creation failed'); | ||
console.log('Security Context created successfully'); | ||
console.log(tokenInfo.getPublicClaims()); | ||
}); | ||
@@ -202,4 +260,4 @@ } | ||
* `access token` ... the access token as received from UAA in the "authorization Bearer" HTTP header | ||
* `config` ... a structure with mandatory elements url, clientid and clientsecret | ||
* `callback(error, securityContext)` | ||
* `config` ... a structure with mandatory elements url, clientid and clientsecret or cache configuration | ||
* `callback(error, securityContext, tokenInfo)` | ||
@@ -260,5 +318,8 @@ ### getLogonName | ||
### getTokenInfo | ||
* returns the [TokenInfo](doc/TokenInfo.md) object, containing all information received from token. | ||
### requestToken | ||
Requests a token based on the given type. The type can be `constants.TYPE_USER_TOKEN` or `constants.TYPE_CLIENT_CREDENTIALS_TOKEN`. Prerequisite for the former is that the requesting client has `grant_type=user_token` and that the current user token includes the scope `uaa.user`. | ||
Requests a token based on the given type. The type can be `constants.TYPE_USER_TOKEN` or `constants.TYPE_CLIENT_CREDENTIALS_TOKEN`. | ||
@@ -265,0 +326,0 @@ * `serviceCredentials` ... the credentials of the service as JSON object. The attributes `clientid`, `clientsecret` and `url` (UAA) are mandatory. Note that the subdomain of the `url` will be adapted to the subdomain of the application token if necessary. |
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
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
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
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
Misc. License Issues
License(Experimental) A package's licensing information has fine-grained problems.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
0
1213
376
0
88342