Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@sap/xssec

Package Overview
Dependencies
Maintainers
3
Versions
85
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sap/xssec - npm Package Compare versions

Comparing version 2.2.3 to 3.0.1

lib/requests.js

24

CHANGELOG.md
# Change Log
All notable changes to this project will be documented in this file.
## 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

@@ -5,0 +29,0 @@

1

lib/index.js

@@ -6,5 +6,4 @@ 'use strict';

exports.createSecurityContextCC = xssec.createSecurityContextCC;
exports.createSecurityContext = xssec.createSecurityContext;
exports.constants = require('./constants');
exports.JWTStrategy = passportStrategy.JWTStrategy;

@@ -130,3 +130,3 @@ 'use strict';

retryDelay: 500,
retrySrategy: request.RetryStrategies.HTTPOrNetworkError
retryStrategy: request.RetryStrategies.HTTPOrNetworkError
};

@@ -133,0 +133,0 @@ debugTrace('Key "' + cacheKey + '" not found in cache. Querying keys from UAA via URL "' + options.url + '".');

'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 subscriptionsAllowed = false;
if (result.cid.indexOf('!t') !== -1 || result.cid.indexOf('!b') !== -1) {
subscriptionsAllowed = true;
}
if ((result.cid === self.config.clientid)
&& (result.zid === self.config.identityzoneid || subscriptionsAllowed === 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 or broker plan it is sufficient that the clientid matches)
if (subscriptionsAllowed === 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 or broker 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,391 +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 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' || !invalidatedTokenHeaderJSON.jku) {
return cb(null, config.verificationkey);
}
validateJku(invalidatedTokenHeaderJSON.jku, config.uaadomain, function(err) {
if (err) {
return cb(null, config.verificationkey);
}
keyCache.getKey(invalidatedTokenHeaderJSON.jku, invalidatedTokenHeaderJSON.kid, function(err, key) {
if (err) {
return cb(null, config.verificationkey);
} else {
return cb(null, key);
}
});
});
} catch (e) {
e.statuscode = 403;
return cb(e);
}
}
return scopes.indexOf(scope) !== -1;
};
function validateJku(jkuUrl, uaaDomain, cb) {
if (uaaDomain === null || uaaDomain === undefined) {
var errorString = 'Service is not properly configured in \'VCAP_SERVICES\', attribute \'uaadomain\' is missing. Use legacy-token-key.';
debugTrace('\n' + errorString);
return cb(new Error(errorString));
}
var tokenKeyUrl = url.parse(jkuUrl);
if (tokenKeyUrl.hostname.substring(tokenKeyUrl.hostname.indexOf(uaaDomain), tokenKeyUrl.hostname.length) !== uaaDomain) {
var errorString = 'JKU of the JWT token (' + jkuUrl + ') does not match with the uaa domain (' + uaaDomain + '). Use legacy-token-key.';
debugTrace('\n' + errorString);
return cb(new Error(errorString));
}
cb(null);
}
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
},
timeout: 2000
};
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) {
if (error.code === 'ETIMEDOUT' && error.connect === true) {
debugError('requestToken: HTTP connection timeout.');
}
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
},
timeout: 2000
};
request.post(
options,
function(error, response, body) {
if (error) {
if (error.code === 'ETIMEDOUT' && error.connect === true) {
debugError('requestToken: HTTP connection timeout.');
}
debugError(error.message);
debugError(error.stack);
return cb(error, null);
}
if (response.statusCode === 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
},
timeout: 2000
};
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) {
if (error.code === 'ETIMEDOUT' && error.connect === true) {
debugError('requestToken: HTTP connection timeout.');
}
debugError(error.message);
debugError(error.stack);
return cb(error, null);
}
if (response.statusCode === 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);
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.');
}
);
};
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);
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, 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.5.2","@sap/xsenv":"^2.0.0","debug":"4.1.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":{"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.2.3","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.1","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));

@@ -220,2 +215,10 @@ app.use(passport.initialize());

### 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

@@ -243,10 +246,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

@@ -269,12 +264,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

@@ -314,6 +299,2 @@

### getIdentityZone (obsolete, use getSubaccountId instead)
* returns the identity zone that the access token has been issued for.
### getSubaccountId

@@ -334,1 +315,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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc