371 lines
12 KiB
JavaScript
371 lines
12 KiB
JavaScript
|
'use strict';
|
||
|
const constants = require('../constants');
|
||
|
const requests = require('../requests');
|
||
|
const {JwtTokenValidatorXSUAA} = require('../validator')
|
||
|
|
||
|
// 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');
|
||
|
|
||
|
|
||
|
debugError.log = console.error.bind(console);
|
||
|
debugTrace.log = console.log.bind(console);
|
||
|
|
||
|
module.exports.SecurityContext = function(config, configArr) {
|
||
|
var userInfo = {
|
||
|
logonName: '',
|
||
|
givenName: '',
|
||
|
familyName: '',
|
||
|
email: ''
|
||
|
};
|
||
|
|
||
|
const xsappname = config.xsappname;
|
||
|
var token;
|
||
|
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;
|
||
|
|
||
|
this.getConfigType = function () {
|
||
|
return "XSUAA";
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
this.getSubaccountId = function () {
|
||
|
return subaccountid;
|
||
|
};
|
||
|
|
||
|
this.getZoneId = function () {
|
||
|
return zid;
|
||
|
};
|
||
|
|
||
|
this.getAppTID = 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);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
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 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;
|
||
|
}
|
||
|
|
||
|
this.getAttributes = function () {
|
||
|
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;
|
||
|
}
|
||
|
return userAttributes;
|
||
|
}
|
||
|
|
||
|
this.getAttribute = function (name) {
|
||
|
const attributes = this.getAttributes();
|
||
|
if (!attributes) return null;
|
||
|
if (!name) {
|
||
|
debugTrace('\nInvalid attribute name (may not be null, empty, or undefined).');
|
||
|
return null;
|
||
|
}
|
||
|
if (!attributes[name]) {
|
||
|
debugTrace('\nNo attribute "' + name + '" found for user "' + this.getLogonName() + '".');
|
||
|
return null;
|
||
|
}
|
||
|
return attributes[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];
|
||
|
};
|
||
|
|
||
|
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) {
|
||
|
return false;
|
||
|
}
|
||
|
var scopeName = xsappname + '.' + scope;
|
||
|
return scopes.indexOf(scopeName) !== -1;
|
||
|
};
|
||
|
|
||
|
this.getGrantType = function () {
|
||
|
return grantType;
|
||
|
};
|
||
|
|
||
|
this.checkScope = function (scope) {
|
||
|
if (!scope || !scopes) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (scope.substring(0, constants.XSAPPNAMEPREFIX.length) === constants.XSAPPNAMEPREFIX) {
|
||
|
scope = scope.replace(constants.XSAPPNAMEPREFIX, xsappname + '.');
|
||
|
}
|
||
|
return scopes.indexOf(scope) !== -1;
|
||
|
};
|
||
|
|
||
|
this.checkFollowingInstanceScope = function (scope) {
|
||
|
if (!scope || !scopes) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
const payload = this.getTokenInfo() ? this.getTokenInfo().getPayload() : null;
|
||
|
|
||
|
const cID = payload ? payload["client_id"] : "";
|
||
|
|
||
|
if(cID.indexOf('sb-') != 0) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
const appId = cID.substring(3);
|
||
|
|
||
|
if(!appId.indexOf("|") === -1) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
const scopeToSearch = appId + "." + scope;
|
||
|
return scopes.indexOf(scopeToSearch) !== -1;
|
||
|
}
|
||
|
|
||
|
function cleanUpUserAttributes(attr) {
|
||
|
for (var n in attr) {
|
||
|
return attr;
|
||
|
}
|
||
|
return 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 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 || [];
|
||
|
zid = tokenInfo.getAppTID();
|
||
|
subaccountid = decodedToken["ext_attr"] ? decodedToken["ext_attr"].subaccountid : zid;
|
||
|
if (!subaccountid) {
|
||
|
subaccountid = zid;
|
||
|
}
|
||
|
|
||
|
clientId = tokenInfo.getClientId();
|
||
|
expirationDate = new Date(decodedToken.exp * 1000);
|
||
|
grantType = decodedToken.grant_type;
|
||
|
|
||
|
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));
|
||
|
} else {
|
||
|
debugTrace('\nObtained attributes: no additional authentication attributes in JWT token available.');
|
||
|
}
|
||
|
|
||
|
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));
|
||
|
}
|
||
|
|
||
|
this.verifyToken = function (encodedToken, attributes, cb) {
|
||
|
const validator = new JwtTokenValidatorXSUAA(configArr, config, attributes);
|
||
|
|
||
|
validator.validateToken(encodedToken, function (err, tokenInfo) {
|
||
|
if (err) {
|
||
|
try {
|
||
|
cb(err, null, tokenInfo);
|
||
|
} catch(e) {
|
||
|
debugError("xssec: Unhandled Exception in Callback");
|
||
|
debugError(e);
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
isForeignMode = validator.isForeignMode();
|
||
|
|
||
|
//Token is now validated. So just fill local variables
|
||
|
fillContext.call(this, encodedToken, tokenInfo);
|
||
|
|
||
|
try {
|
||
|
cb(null, this, tokenInfo);
|
||
|
} catch(e) {
|
||
|
debugError("xssec: Unhandled Exception in Callback");
|
||
|
debugError(e);
|
||
|
}
|
||
|
}.bind(this));
|
||
|
};
|
||
|
};
|