From cc13d0a0dfc6d3dbb0a647cdd227ccea249fb8dd Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 18 Jan 2017 20:54:38 -0800 Subject: [PATCH 1/9] wip --- lib/Client.js | 8 ++ lib/cache/CacheHandler.js | 14 ++- lib/ds/DataStore.js | 41 ++++++- ...path-access-token-authentication-result.js | 76 +++++++++++++ .../stormpath-access-token-authenticator.js | 104 ++++++++++++++++++ lib/resource/Application.js | 5 + lib/stormpath.js | 1 + package.json | 3 +- 8 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 lib/oauth/stormpath-access-token-authentication-result.js create mode 100644 lib/oauth/stormpath-access-token-authenticator.js diff --git a/lib/Client.js b/lib/Client.js index b3c2bedb..0c8ac8da 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -313,6 +313,14 @@ Client.prototype.getAccessToken = function() { return this.getResource(args.href, args.options, require('./resource/AccessToken'), args.callback); }; +Client.prototype.getApiKeyById = function() { + var args = utils.resolveArgs(arguments, ['id', 'options', 'callback']); + + var href = this.config.client.baseUrl + '/apiKeys/' + args.id; + + return this.getResource(href, args.options, require('./resource/ApiKey'), args.callback); +}; + /** * Retrieves a {@link RefreshToken} resource. * diff --git a/lib/cache/CacheHandler.js b/lib/cache/CacheHandler.js index 93876bf9..354983f3 100644 --- a/lib/cache/CacheHandler.js +++ b/lib/cache/CacheHandler.js @@ -1,6 +1,7 @@ 'use strict'; var async = require('async'); +var xtend = require('xtend'); var _ = require('../underscore'); var Cache = require('./Cache'); @@ -10,7 +11,7 @@ var utils = require('../utils'); var CACHE_REGIONS = ['applications', 'directories', 'accounts', 'groups', 'groupMemberships', 'tenants', 'accountStoreMappings','apiKeys','idSiteNonces', - 'customData', 'organizations']; + 'customData', 'organizations', 'authTokens']; // singleton of DisabledCache wrapped into Cache instance var disabledCache = new Cache({store: DisabledCache}); @@ -79,6 +80,11 @@ function getCacheByHref(cacheManager, href) { region = href.match(/customData/) ? 'customData' : (href.split('/').slice(-2)[0]); } + // if (href && href.match(/application\/[^\/]+\/authTokens\//)) { + // region = 'authTokens'; + // } + + if (!region || CACHE_REGIONS.indexOf(region) === -1) { return disabledCache; } @@ -123,6 +129,12 @@ function buildCacheableResourcesFromParentObject(object){ if(parentResource.href){ resourcesToCache.push(parentResource); } + + // This happens when a resource is a 302 direct from one to another, + // cache the returned data under the requestedUrl as well + if (object.requestData && object.requestData.originalHref !== object.href) { + resourcesToCache.push(xtend(object,{href: object.requestData.originalHref})); + } } return resourcesToCache; } diff --git a/lib/ds/DataStore.js b/lib/ds/DataStore.js index 3db5e8dd..1b0d6778 100644 --- a/lib/ds/DataStore.js +++ b/lib/ds/DataStore.js @@ -70,7 +70,12 @@ DataStore.prototype._wrapGetResourceResponse = instance = instantiate(InstanceCtor, data, query, this); } if(instance instanceof ApiKey){ - instance._decrypt(cb); + async.parallel([ + instance._decrypt.bind(instance), + _this.cacheHandler.put.bind(_this.cacheHandler, instance.href, instance) + ],function (err, results) { + cb(err, err ? null : results[0]); + }); } else if(utils.isCollectionData(instance) && instance.items[0] && @@ -132,7 +137,16 @@ DataStore.prototype.getResource = function getResource(/* href, [query], [Instan InstanceCtor: (args.length > 0 && args[0] instanceof Function) ? args.shift() : InstanceResource }; - return _this.exec(callback); + function logger(err, resource) { + + if (resource) { + delete resource.requestData; + } + // console.log(resource.originalHref, resource.fromCache ? 'CACHE':' ', resource.end-resource.begin); + callback.apply(null, arguments); + } + + return _this.exec(logger); }; /** @@ -270,6 +284,10 @@ DataStore.prototype._buildRequestQuery = function(){ return queryStringObj; }; +function applyRequestData(resource, data){ + resource.requestData = data; +} + /** * @private * @param callback @@ -289,10 +307,19 @@ DataStore.prototype.exec = function executeRequest(callback){ }; var ctor = _this._request.InstanceCtor; - + var begin = new Date().getTime(); function doRequest(){ try { _this.requestExecutor.execute(request, function onGetResourceRequestResult(err, body) { + if (body) { + applyRequestData(body, { + begin: begin, + end: new Date().getTime(), + originalHref: request.uri, + fromCache: false + }); + } + _this._wrapGetResourceResponse(err, body, ctor, query, callback); }); } catch (err) { @@ -315,6 +342,14 @@ DataStore.prototype.exec = function executeRequest(callback){ _this.cacheHandler.get(cacheKey, function onCacheResult(err, entry) { if (err || entry) { + if (entry) { + applyRequestData(entry, { + begin: begin, + end: new Date().getTime(), + originalHref: request.uri, + fromCache: true + }); + } _this._wrapGetResourceResponse(err, entry, ctor, query, callback); return; } diff --git a/lib/oauth/stormpath-access-token-authentication-result.js b/lib/oauth/stormpath-access-token-authentication-result.js new file mode 100644 index 00000000..e2d10a1a --- /dev/null +++ b/lib/oauth/stormpath-access-token-authentication-result.js @@ -0,0 +1,76 @@ +'use strict'; + +var utils = require('../utils'); + +/** + * @constructor + * + * @description + * + * Encapsulates the access token response from an application's `/oauth/token` + * endpoint. This is a base class which is extended by + * {@link OAuthPasswordGrantAuthenticationResult} and {@link OAuthClientCredentialsAuthenticationResult}. + * + * @param {Client} client + * An initialized Stormpath Client for the tenant the issued the token. + * + * @param {AccessTokenResponse} accessTokenResponse + * The access token response from the Stormpath REST API. + */ +function StormpathAccessTokenAuthenticationResult(client, data) { + if (!(this instanceof StormpathAccessTokenAuthenticationResult)) { + return new StormpathAccessTokenAuthenticationResult(client,data); + } + + Object.defineProperty(this, 'client', { + enumerable: false, + value: client + }); + + for (var key in data) { + if (data.hasOwnProperty(key)) { + this[key] = data[key]; + } + } + + /** + * @name StormpathAccessTokenAuthenticationResult#expandedJwt + * + * @description + * + * An object that allows you to inspect the body, claims, and header of the + * access token. + * + * @type {Object} + */ +} + +StormpathAccessTokenAuthenticationResult.prototype.account = null; + +StormpathAccessTokenAuthenticationResult.prototype.jwt = null; + +StormpathAccessTokenAuthenticationResult.prototype.expandedJwt = null; + +/** + * @function + * + * @description Get the account resource of the account that has authenticated. + * + * @param {ExpansionOptions} options + * Options for expanding the fetched {@link Account} resource. + * + * @param {Function} callback + * The callback to call with the parameters (err, {@link Account}). + */ +StormpathAccessTokenAuthenticationResult.prototype.getAccount = function getAccount(/* [options,] callback */) { + var args = utils.resolveArgs(arguments, ['options', 'callback'], true); + this.client.getAccount(this.account.href, args.options, require('../resource/Account'), args.callback); +}; + +StormpathAccessTokenAuthenticationResult.prototype.getApplication = function getApplication(/* [options,] callback */) { + var args = utils.resolveArgs(arguments, ['options', 'callback'], true); + + this.client.getApplication(this.application.href, args.options, require('../resource/Application'), args.callback); +}; + +module.exports = StormpathAccessTokenAuthenticationResult; diff --git a/lib/oauth/stormpath-access-token-authenticator.js b/lib/oauth/stormpath-access-token-authenticator.js new file mode 100644 index 00000000..3081ca4f --- /dev/null +++ b/lib/oauth/stormpath-access-token-authenticator.js @@ -0,0 +1,104 @@ +var Client = require('../Client'); + +var nJwt = require('njwt'); + +var StormpathAccessTokenAuthenticationResult = require('./stormpath-access-token-authentication-result'); + +function tenantAdminKeyResolver(client, kid, callback){ + client.getApiKeyById(kid, function (err, apiKey) { + if (err) { + return callback(err); + } + apiKey.getAccount(function (err, account) { + if (err) { + return callback(err); + } + account.getDirectory(function (err, directory) { + if (err) { + return callback(err); + } + if (directory.name === 'Stormpath Administrators') { + return callback(null, apiKey.secret); + } + return callback(new Error('Invalid kid')); + }); + }); + }); +} + + +function StormpathAccessTokenAuthenticator(client) { + if (!(this instanceof StormpathAccessTokenAuthenticator)) { + return new StormpathAccessTokenAuthenticator(client); + } + + if (!(client instanceof Client)) { + throw new Error('StormpathAccessTokenAuthenticator must be given a Stormpath client instance'); + } + + this.client = client; + this.verifier = nJwt.createVerifier().withKeyResolver(tenantAdminKeyResolver.bind(null, client)); +} + +StormpathAccessTokenAuthenticator.prototype.localValidation = false; + +/** + * Calling this method will convert this authenticator to "local validation" mode. + * In this mode, the authenticator will skip the REST API call which determines + * if the {@link AccessToken} resource has been revoked. Tokens will be considered + * valid if they are signed by the original signing key (one of your Tenant API Keys) + * and are not expired. **Please be aware of this security tradeoff.** When using + * local validation, we suggest shorter expiration times, as configured by the + * issuing application's {@link OAuthPolicy}. + * + * @example + * + * var authenticator = new stormpath.StormpathAccessTokenAuthenticator(); + * + * authenticator.withLocalValidation(); + */ +StormpathAccessTokenAuthenticator.prototype.withLocalValidation = function withLocalValidation() { + this.localValidation = true; + return this; +}; + +StormpathAccessTokenAuthenticator.prototype.forApplicationHref = function forApplicationHref(href) { + this.forApplicationHref = href; + return this; +}; + +StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate (jwtAccessTokenString, callback){ + var self = this; + self.verifier.verify(jwtAccessTokenString, function (err, jwt) { + if (err) { + return callback(err); + } + + var applicationHref = self.forApplicationHref || jwt.body.iss; + + // Vaildate against the provided application, or just the issuing applicaiotns + + var href = applicationHref + '/authTokens/'+jwtAccessTokenString; + + // Bypass the cache if local validation is disabled + + var query = self.localValidation ? null : { nocache: true}; + + self.client.getResource(href, query, function(err, authTokenResponse){ + if(err){ + return callback(err); + } + + // If the incoming token has scope, preserve that for the developer + + if (jwt.body.scope) { + authTokenResponse.expandedJwt.claims.scope = jwt.claims.scope; + } + + return callback(null, new StormpathAccessTokenAuthenticationResult(self.client, authTokenResponse)); + }); + + }); +}; + +module.exports = StormpathAccessTokenAuthenticator; \ No newline at end of file diff --git a/lib/resource/Application.js b/lib/resource/Application.js index 0c9d2cb6..7e34945a 100644 --- a/lib/resource/Application.js +++ b/lib/resource/Application.js @@ -1143,6 +1143,11 @@ Application.prototype.getAccountStoreMappings = function getAccountStoreMappings return this.dataStore.getResource(this.accountStoreMappings.href, args.options, ApplicationAccountStoreMapping, args.callback); }; +Application.prototype.getAuthToken = function getAccountStoreMappings(/* jwtAccessTokenString, [options,] callback */) { + var args = utils.resolveArgs(arguments, ['jwtAccessTokenString', 'options', 'callback'], true); + return this.dataStore.getResource(this.href + '/authTokens/' + args. jwtAccessTokenString, args.options, InstanceResource, args.callback); +}; + /** * Retrieves the {@link ApplicationAccountStoreMapping} that represents the link * to the Application's default Account Store, which is the {@link Directory} diff --git a/lib/stormpath.js b/lib/stormpath.js index f06e55ea..b61e0377 100644 --- a/lib/stormpath.js +++ b/lib/stormpath.js @@ -25,6 +25,7 @@ module.exports = { OAuthClientCredentialsAuthenticationResult: clientCredentialsGrant.authenticationResult, SamlIdpUrlBuilder: require('./saml/SamlIdpUrlBuilder'), AssertionAuthenticationResult: require('./authc/AssertionAuthenticationResult'), + StormpathAccessTokenAuthenticator: require('./oauth/stormpath-access-token-authenticator'), StormpathAssertionAuthenticator: require('./authc/StormpathAssertionAuthenticator'), JwtAuthenticator: require('./jwt/jwt-authenticator'), OAuthAuthenticator: require('./oauth/authenticator') diff --git a/package.json b/package.json index 32b64c5a..2c4fbdf5 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "stormpath-config": "0.0.27", "underscore": "~1.5.2", "underscore.string": "~3.2.3", - "uuid": "^3.0.0" + "uuid": "^3.0.0", + "xtend": "^4.0.1" }, "devDependencies": { "benchmark": "^2.0.0", From 20e09837d2e4b9ea64f0758b7ae3caf510633278 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 23 Jan 2017 09:11:19 -0800 Subject: [PATCH 2/9] request token resource or application resource, more docs --- ...path-access-token-authentication-result.js | 56 +++++++--- .../stormpath-access-token-authenticator.js | 104 +++++++++++++++--- 2 files changed, 133 insertions(+), 27 deletions(-) diff --git a/lib/oauth/stormpath-access-token-authentication-result.js b/lib/oauth/stormpath-access-token-authentication-result.js index e2d10a1a..52ab9a8b 100644 --- a/lib/oauth/stormpath-access-token-authentication-result.js +++ b/lib/oauth/stormpath-access-token-authentication-result.js @@ -7,9 +7,7 @@ var utils = require('../utils'); * * @description * - * Encapsulates the access token response from an application's `/oauth/token` - * endpoint. This is a base class which is extended by - * {@link OAuthPasswordGrantAuthenticationResult} and {@link OAuthClientCredentialsAuthenticationResult}. + * Encapsulates the access token resource response, obtained from the `/accessTokens` collection. * * @param {Client} client * An initialized Stormpath Client for the tenant the issued the token. @@ -33,22 +31,54 @@ function StormpathAccessTokenAuthenticationResult(client, data) { } } - /** - * @name StormpathAccessTokenAuthenticationResult#expandedJwt - * - * @description - * - * An object that allows you to inspect the body, claims, and header of the - * access token. - * - * @type {Object} - */ + + + } +/** + * @name StormpathAccessTokenAuthenticationResult#account + * + * @description + * + * An object literal with an href pointer to the account that has authenticated. + * Use {@link StormpathAccessTokenAuthenticationResult#getAccount StormpathAccessTokenAuthenticationResult.getAccount()} + * to fetch the full {@link Account} resource. + * + * @type {Object} + */ StormpathAccessTokenAuthenticationResult.prototype.account = null; +/** + * An object literal with an href pointer to the application that issued this + * token. Use {@link StormpathAccessTokenAuthenticationResult#getApplication StormpathAccessTokenAuthenticationResult.getApplication()} + * to fetch the full {@link Application} resource. + * + * @type {Object} + */ +StormpathAccessTokenAuthenticationResult.prototype.application = null; + +/** + * @name StormpathAccessTokenAuthenticationResult#jwt + * + * @description + * + * The JWT access token string that was provided for authentication. + * + * @type {String} + */ StormpathAccessTokenAuthenticationResult.prototype.jwt = null; +/** + * @name StormpathAccessTokenAuthenticationResult#expandedJwt + * + * @description + * + * An object that allows you to inspect the body, claims, and header of the + * access token. + * + * @type {Object} + */ StormpathAccessTokenAuthenticationResult.prototype.expandedJwt = null; /** diff --git a/lib/oauth/stormpath-access-token-authenticator.js b/lib/oauth/stormpath-access-token-authenticator.js index 3081ca4f..cf647b47 100644 --- a/lib/oauth/stormpath-access-token-authenticator.js +++ b/lib/oauth/stormpath-access-token-authenticator.js @@ -26,7 +26,31 @@ function tenantAdminKeyResolver(client, kid, callback){ }); } - +/** + * @class + * + * @constructor + * + * @description + * + * Creates an authenticator that can be used to validate access tokens that have + * been issued by a Stormpath Tenant, using API Keys created for accounts in that + * tenant's "Stormpath Administrators" directory. Access tokens can be issued + * with one of the following methods: + * + * - {@link OAuthClientCredentialsAuthenticator#authenticate OAuthClientCredentialsAuthenticator.authenticate()} + * - {@link OAuthPasswordGrantRequestAuthenticator#authenticate OAuthPasswordGrantRequestAuthenticator.authenticate()} + * - {@link OAuthStormpathTokenAuthenticator#authenticate OAuthStormpathTokenAuthenticator.authenticate()} + * + * @param {Client} client A constructed {@link Client} instance, which will be bound to + * the Stormpath Tenant in which access token tokens will be validated. + * + * @example + * var client = new stormpath.Client(); + * + * var authenticator = new stormpath.StormpathAccessTokenAuthenticator(client); + * + */ function StormpathAccessTokenAuthenticator(client) { if (!(this instanceof StormpathAccessTokenAuthenticator)) { return new StormpathAccessTokenAuthenticator(client); @@ -44,16 +68,22 @@ StormpathAccessTokenAuthenticator.prototype.localValidation = false; /** * Calling this method will convert this authenticator to "local validation" mode. - * In this mode, the authenticator will skip the REST API call which determines - * if the {@link AccessToken} resource has been revoked. Tokens will be considered - * valid if they are signed by the original signing key (one of your Tenant API Keys) - * and are not expired. **Please be aware of this security tradeoff.** When using - * local validation, we suggest shorter expiration times, as configured by the - * issuing application's {@link OAuthPolicy}. + * In this mode, the authenticator will cache the related access token resource, + * and subsequent authentication attempts will skip the REST API request for the + * access token resource, until that resource expires from the local cache, as + * configured by your caching rules (see {@link Client}). This can speed up your + * authentication layer, as the authentication is now done locally without a + * network request. + * + * **Warning. This mode has a security tradeoff.** Because of the caching nature + * of this mode, access tokens will be considred valid until the expire, and your + * local application will not know if another process as deleted this resource from + * the Stormpath REST API. When using local validation, we suggest shorter + * expiration times, as configured by the issuing application's {@link OAuthPolicy}. * * @example * - * var authenticator = new stormpath.StormpathAccessTokenAuthenticator(); + * var authenticator = new stormpath.StormpathAccessTokenAuthenticator(client); * * authenticator.withLocalValidation(); */ @@ -62,11 +92,51 @@ StormpathAccessTokenAuthenticator.prototype.withLocalValidation = function withL return this; }; -StormpathAccessTokenAuthenticator.prototype.forApplicationHref = function forApplicationHref(href) { - this.forApplicationHref = href; +/** + * Indicate a specific Stormpath Application as an additional authorization check. + * If an application is specified, the authentication attempt will fail if the + * access token was not issued by the specified application. + * + * @param {Application|Object} application An {@link Application} instance, or object + * literal with an href property that indicates the desired application. + * + * @example + * + * var client = new stormpath.Client(); + * + * var authenticator = new stormpath.StormpathAccessTokenAuthenticator(client); + * + * var applicationHref = 'https://api.stormpath.com/v1/applications/3WIeKpaEjPHfLmy6GIvbwv'; + * + * authenticator.forApplication({ + * href: applicationHref + * }); + */ +StormpathAccessTokenAuthenticator.prototype.forApplication = function forApplication(application) { + this.forApplicationHref = application.href; return this; }; +/** + * Authenticates an access token, in the form of compact JWT string. + * + * @param {String} jwtAccessTokenString The compact JWT string + * @param {Function} callback Will be called with (err, {@link StormpathAccessTokenAuthenticationResult}). + * + * @example + * var jwtAccessTokenString = 'eyJraWQiOiI2NldURFJVM1paSkNZVFJVVlZTUUw3WEJOIiwic3R0IjoiYWNjZXNzIiwiYWxnIjoiSFMyNTYifQ.eyJqdGkiOiIzV0llS3N1SmR6YWR5YzN4U1ltc1l6IiwiaWF0IjoxNDY5ODMzNzQ3LCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy8yNGs3SG5ET3o0dFE5QVJzQnRQVU42Iiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8yRWRHb3htbGpuODBlRHZjM0JzS05EIiwiZXhwIjoxNDY5ODM0MzQ3LCJydGkiOiIzV0llS3BhRWpQSGZMbXk2R0l2Ynd2In0.9J7HvhgJZxvxuE-0PiarTDTFPCVVLR_nvRByULNA01Q'; + * + * authenticator.authenticate(jwtAccessTokenString, function(err, authenticationResult) { + * if (err) { + * console.log(err); + * } else { + * authenticationResult.getAccount(function(err, account){ + * console.log('Authenticated Account', account); + * }); + * } + * }); + * + */ StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate (jwtAccessTokenString, callback){ var self = this; self.verifier.verify(jwtAccessTokenString, function (err, jwt) { @@ -74,17 +144,23 @@ StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate return callback(err); } - var applicationHref = self.forApplicationHref || jwt.body.iss; + var resourceHref; + + // Validate against the provided application, if configured. + // Otherwise, validate directly against the access token resource - // Vaildate against the provided application, or just the issuing applicaiotns + if (self.forApplicationHref) { + resourceHref = self.forApplicationHref + '/authTokens/'+ jwtAccessTokenString; + } else { + resourceHref = self.config.client.baseUrl + '/accessTokens/'+ jwt.header.jti; + } - var href = applicationHref + '/authTokens/'+jwtAccessTokenString; // Bypass the cache if local validation is disabled var query = self.localValidation ? null : { nocache: true}; - self.client.getResource(href, query, function(err, authTokenResponse){ + self.client.getResource(resourceHref, query, function(err, authTokenResponse){ if(err){ return callback(err); } From 876fbabfad7f52d02c08900e2448a239bae1c623 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 23 Jan 2017 09:11:39 -0800 Subject: [PATCH 3/9] wip on the resource request logger --- lib/ds/DataStore.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/ds/DataStore.js b/lib/ds/DataStore.js index 1b0d6778..8148597a 100644 --- a/lib/ds/DataStore.js +++ b/lib/ds/DataStore.js @@ -50,6 +50,7 @@ function DataStore(config) { this.cacheHandler = config.cacheHandler || new CacheHandler(config); this.nonceStore = config.nonceStore || new NonceStore(this); this.apiKeyEncryptionOptions = new ApiKeyEncryptedOptions(config.apiKeyEncryptionOptions); + this.resourceRequestLogger = config.resourceRequestLogger; var _request = null; Object.defineProperty(this, '_request', { get: function(){return _request;}, @@ -137,16 +138,19 @@ DataStore.prototype.getResource = function getResource(/* href, [query], [Instan InstanceCtor: (args.length > 0 && args[0] instanceof Function) ? args.shift() : InstanceResource }; - function logger(err, resource) { + function loggerProxy(err, resource) { if (resource) { + if (_this.resourceRequestLogger) { + _this.resourceRequestLogger(resource.requestData); + } delete resource.requestData; } // console.log(resource.originalHref, resource.fromCache ? 'CACHE':' ', resource.end-resource.begin); callback.apply(null, arguments); } - return _this.exec(logger); + return _this.exec(loggerProxy); }; /** From 6d4ba1b61d159be8a54fcb3e97ef6bda0bb69c3e Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 23 Jan 2017 19:57:06 -0800 Subject: [PATCH 4/9] Adding tests, depend on updated nJwt --- lib/cache/CacheHandler.js | 2 +- lib/ds/DataStore.js | 2 +- .../stormpath-access-token-authenticator.js | 4 +- package.json | 2 +- test/it/access-token-authenticator.js | 225 ++++++++++++++++++ 5 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 test/it/access-token-authenticator.js diff --git a/lib/cache/CacheHandler.js b/lib/cache/CacheHandler.js index 354983f3..7c30b55d 100644 --- a/lib/cache/CacheHandler.js +++ b/lib/cache/CacheHandler.js @@ -11,7 +11,7 @@ var utils = require('../utils'); var CACHE_REGIONS = ['applications', 'directories', 'accounts', 'groups', 'groupMemberships', 'tenants', 'accountStoreMappings','apiKeys','idSiteNonces', - 'customData', 'organizations', 'authTokens']; + 'customData', 'organizations', 'authTokens', 'accessTokens']; // singleton of DisabledCache wrapped into Cache instance var disabledCache = new Cache({store: DisabledCache}); diff --git a/lib/ds/DataStore.js b/lib/ds/DataStore.js index 8148597a..1a035375 100644 --- a/lib/ds/DataStore.js +++ b/lib/ds/DataStore.js @@ -146,7 +146,7 @@ DataStore.prototype.getResource = function getResource(/* href, [query], [Instan } delete resource.requestData; } - // console.log(resource.originalHref, resource.fromCache ? 'CACHE':' ', resource.end-resource.begin); + callback.apply(null, arguments); } diff --git a/lib/oauth/stormpath-access-token-authenticator.js b/lib/oauth/stormpath-access-token-authenticator.js index cf647b47..54e570bb 100644 --- a/lib/oauth/stormpath-access-token-authenticator.js +++ b/lib/oauth/stormpath-access-token-authenticator.js @@ -139,6 +139,7 @@ StormpathAccessTokenAuthenticator.prototype.forApplication = function forApplica */ StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate (jwtAccessTokenString, callback){ var self = this; + self.verifier.verify(jwtAccessTokenString, function (err, jwt) { if (err) { return callback(err); @@ -152,10 +153,9 @@ StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate if (self.forApplicationHref) { resourceHref = self.forApplicationHref + '/authTokens/'+ jwtAccessTokenString; } else { - resourceHref = self.config.client.baseUrl + '/accessTokens/'+ jwt.header.jti; + resourceHref = self.client.config.client.baseUrl + '/accessTokens/'+ jwt.body.jti; } - // Bypass the cache if local validation is disabled var query = self.localValidation ? null : { nocache: true}; diff --git a/package.json b/package.json index 2c4fbdf5..3fe0ee3c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "jwt-simple": "~0.4.0", "memcached": "~2.2.2", "moment": "^2.15.2", - "njwt": "^0.3.1", + "njwt": "^0.4.0", "properties-parser": "~0.3.1", "redis": "~2.6.2", "request": "~2.74.0", diff --git a/test/it/access-token-authenticator.js b/test/it/access-token-authenticator.js new file mode 100644 index 00000000..5978d411 --- /dev/null +++ b/test/it/access-token-authenticator.js @@ -0,0 +1,225 @@ + +var common = require('../common'); +var helpers = require('./helpers'); +var assert = common.assert; + +var stormpath = require('../../'); + +var async = require('async'); +var nJwt = require('njwt'); +var uuid = require('uuid'); + +function getAccessTokenOrFail(account, application, done, callback) { + stormpath.OAuthAuthenticator(application).authenticate({ + body: { + grant_type: 'password', + username: account.username, + password: account.password + } + },function(err, result){ + + if (err) { + return done(err); + } + + callback(result.accessTokenResponse.access_token); + + }); +} + +var StormpathAccessTokenAuthenticationResult = require('../../lib/oauth/stormpath-access-token-authentication-result'); + +describe('StormpathAccessTokenAuthenticator',function(){ + + var application1, application2; + + var environmentApiKey, environmentClient; + + var newAccount = helpers.fakeAccount(); + + var otherApiKey, otherClient; + + before(function(done){ + + async.parallel({ + application: helpers.createApplication.bind(null), + apiKey: function (next) { + + // Create another API Key in this tenant, so that we can test the ability to + // validate a token with multiple keys within the same tenant. + + helpers.getClient(function (client) { + + environmentClient = client; + environmentApiKey = client.config.client.apiKey; + + client.getDirectories({name: 'Stormpath Administrators', expand: 'accounts'}, function (err, collection) { + if (err) { + return done(err); + } + + var href = collection.items[0].accounts.items[0].href + '/apiKeys'; + + client.createResource(href, null, function (err, apiKey) { + if (err) { + return done(err); + } + next(null, apiKey); + }); + }); + }); + } + }, function (err, results) { + + if (err) { + return done(err); + } + + otherApiKey = results.apiKey; + application1 = results.application; + + otherClient = new stormpath.Client({ + apiKey: results.apiKey + }); + + async.parallel({ + account: application1.createAccount.bind(application1, newAccount), + application: helpers.createApplication.bind(null) + }, function (err, results) { + + if (err) { + return done(err); + } + + application2 = results.application; + done(); + }); + }); + + }); + + after(function(done){ + + async.parallel([ + helpers.cleanupApplicationAndStores.bind(null, application1), + helpers.cleanupApplicationAndStores.bind(null, application2), + otherApiKey.delete.bind(otherApiKey) + ], done); + + }); + + it('Should validate tokens that are issued by the current tenant', function (done) { + + // Test that two different keys for the same tenant will yield the same result + + getAccessTokenOrFail(newAccount, application1, done, function(accessToken) { + + var authenticatorA = stormpath.StormpathAccessTokenAuthenticator(environmentClient); + + var authenticatorB = stormpath.StormpathAccessTokenAuthenticator(otherClient); + + async.parallel({ + resultA: authenticatorA.authenticate.bind(authenticatorA, accessToken), + resultB: authenticatorB.authenticate.bind(authenticatorB, accessToken) + }, function (err, results) { + + if (err) { + return done(err); + } + + assert.instanceOf(results.resultA, StormpathAccessTokenAuthenticationResult); + assert.instanceOf(results.resultB, StormpathAccessTokenAuthenticationResult); + done(); + }); + }); + + }); + + it('Should reject tokens that are not signed by a key belonging to the current tenant', function (done) { + + // A token that is not signed by this tenant + + var otherJwt = nJwt.create({ hello:'world' }, uuid()); + otherJwt.header.kid = 'foo'; + var otherToken = otherJwt.compact(); + + var authenticator = stormpath.StormpathAccessTokenAuthenticator(environmentClient); + + authenticator.authenticate(otherToken, function (err) { + assert.instanceOf(err, Error); + assert.equal(err.message, 'Error while resolving signing key for kid "foo"'); + done(); + }); + }); + + it('Should reject tokens that try to forge the kid header', function (done) { + + var otherJwt = nJwt.create({ hello:'world' }, uuid()); + otherJwt.header.kid = environmentApiKey.id; + var otherToken = otherJwt.compact(); + + var authenticator = stormpath.StormpathAccessTokenAuthenticator(environmentClient); + + authenticator.authenticate(otherToken, function (err) { + + assert.instanceOf(err, Error); + assert.equal(err.message, 'Signature verification failed'); + done(); + + }); + }); + + it('Should reject tokens if they are not issued by the expected application', function (done) { + + getAccessTokenOrFail(newAccount, application1, done, function(accessToken) { + + var authenticator = stormpath.StormpathAccessTokenAuthenticator(environmentClient); + + authenticator.forApplication(application2); + + authenticator.authenticate(accessToken, function (err) { + + assert.instanceOf(err, Error); + assert.equal(err.code, 10014); + done(); + + }); + }); + + }); + + it('Should allow caching for faster authentication', function (done) { + + getAccessTokenOrFail(newAccount, application1, done, function(accessToken) { + + var authenticator = stormpath.StormpathAccessTokenAuthenticator(new stormpath.Client()); + + authenticator.withLocalValidation(); + + // First attempt with a new client, will take longer due to fetching resources for the first time. + + authenticator.authenticate(accessToken, function (err) { + if (err) { + return done(err); + } + + // Second attempt should come back immediately, because + // we use a memory cache by default + + var now = new Date().getTime(); + + authenticator.authenticate(accessToken, function (err) { + if (err) { + return done(err); + } + + var delta = new Date().getTime() - now; + + assert.isBelow(delta, 10); + done(); + }); + }); + }); + }); + +}); From 5696bf6d885b7768c9e8f94de22fa26e0e30ab91 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 23 Jan 2017 21:51:17 -0800 Subject: [PATCH 5/9] Deprecate JwtAuthenticator --- lib/jwt/jwt-authenticator.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/jwt/jwt-authenticator.js b/lib/jwt/jwt-authenticator.js index 1c9c0379..c48099e7 100644 --- a/lib/jwt/jwt-authenticator.js +++ b/lib/jwt/jwt-authenticator.js @@ -1,6 +1,7 @@ 'use strict'; var njwt = require('njwt'); +var util = require('util'); var OauthAccessTokenAuthenticator = require('../authc/OauthAccessTokenAuthenticator'); var ApiAuthRequestError = require('../error/ApiAuthRequestError'); @@ -11,6 +12,8 @@ var JwtAuthenticationResult = require('./jwt-authentication-result'); * * @constructor * + * @deprecated Please use {@link StormpathAccessTokenAuthenticator} instead. + * * @description * * Creates an authenticator that can be used to validate JWTs that have been @@ -89,7 +92,7 @@ JwtAuthenticator.prototype.unauthenticated = function unauthenticated(){ * }); * */ -JwtAuthenticator.prototype.authenticate = function authenticate(token,cb){ +JwtAuthenticator.prototype.authenticate = util.deprecate(function authenticate(token,cb){ var self = this; var secret = self.application.dataStore.requestExecutor.options.client.apiKey.secret; @@ -159,11 +162,11 @@ JwtAuthenticator.prototype.authenticate = function authenticate(token,cb){ } } }); - } catch (err) { + } catch (err) { cb(err); } return this; -}; +},'JwtAuthenticator is deprecated, please use StormpathAccessTokenAuthenticator instead.'); module.exports = JwtAuthenticator; From 2ee9d93837024736d3386715a96b640e7389d516 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 23 Jan 2017 22:57:10 -0800 Subject: [PATCH 6/9] cleanup --- lib/cache/CacheHandler.js | 5 ----- lib/ds/DataStore.js | 5 +++++ lib/oauth/stormpath-access-token-authentication-result.js | 3 --- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/cache/CacheHandler.js b/lib/cache/CacheHandler.js index 7c30b55d..25234c7a 100644 --- a/lib/cache/CacheHandler.js +++ b/lib/cache/CacheHandler.js @@ -80,11 +80,6 @@ function getCacheByHref(cacheManager, href) { region = href.match(/customData/) ? 'customData' : (href.split('/').slice(-2)[0]); } - // if (href && href.match(/application\/[^\/]+\/authTokens\//)) { - // region = 'authTokens'; - // } - - if (!region || CACHE_REGIONS.indexOf(region) === -1) { return disabledCache; } diff --git a/lib/ds/DataStore.js b/lib/ds/DataStore.js index 1a035375..89292334 100644 --- a/lib/ds/DataStore.js +++ b/lib/ds/DataStore.js @@ -140,6 +140,11 @@ DataStore.prototype.getResource = function getResource(/* href, [query], [Instan function loggerProxy(err, resource) { + // TODO - finish this idea of a "resourceRequestLogger" so that it is ready + // for public consumption. Until then, don't bloat resources with the requestData + // object, though we need to keep it until this point so that we can make use + // of the originalHref for caching of access token redirects. + if (resource) { if (_this.resourceRequestLogger) { _this.resourceRequestLogger(resource.requestData); diff --git a/lib/oauth/stormpath-access-token-authentication-result.js b/lib/oauth/stormpath-access-token-authentication-result.js index 52ab9a8b..eca9437f 100644 --- a/lib/oauth/stormpath-access-token-authentication-result.js +++ b/lib/oauth/stormpath-access-token-authentication-result.js @@ -31,9 +31,6 @@ function StormpathAccessTokenAuthenticationResult(client, data) { } } - - - } /** From d28f1d5b2cae535ae058330556b5d3986f879260 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 24 Jan 2017 14:59:03 +0100 Subject: [PATCH 7/9] remove indentation by simplifying conditionals --- lib/jwt/jwt-authenticator.js | 128 ++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/lib/jwt/jwt-authenticator.js b/lib/jwt/jwt-authenticator.js index c48099e7..1b7fdfcb 100644 --- a/lib/jwt/jwt-authenticator.js +++ b/lib/jwt/jwt-authenticator.js @@ -63,13 +63,16 @@ JwtAuthenticator.prototype.withLocalValidation = function withLocalValidation() return this; }; -JwtAuthenticator.prototype.withCookie = function withCookie(cookieName){ +JwtAuthenticator.prototype.withCookie = function withCookie(cookieName) { this.configuredCookieName = cookieName; return this; }; -JwtAuthenticator.prototype.unauthenticated = function unauthenticated(){ - return new ApiAuthRequestError({userMessage:'Unauthorized', statusCode: 401}); +JwtAuthenticator.prototype.unauthenticated = function unauthenticated() { + return new ApiAuthRequestError({ + userMessage: 'Unauthorized', + statusCode: 401 + }); }; /** @@ -92,81 +95,80 @@ JwtAuthenticator.prototype.unauthenticated = function unauthenticated(){ * }); * */ -JwtAuthenticator.prototype.authenticate = util.deprecate(function authenticate(token,cb){ +JwtAuthenticator.prototype.authenticate = util.deprecate(function authenticate(token, cb) { var self = this; - var secret = self.application.dataStore.requestExecutor.options.client.apiKey.secret; try { - njwt.verify(token,secret,function(err,jwt){ - if(err){ + njwt.verify(token, secret, function(err, jwt) { + if (err) { err.statusCode = 401; - cb(err); - }else{ - - // If the KID exists, this was issued by our API from a password grant - - if(jwt.header.kid){ - if(self.localValidation){ - // Transfers all body fields to `claims` to maintain consistency - // with remote results. Does not remove the body as to preserve - // backwards compatibility. - jwt.claims = {}; - Object.keys(jwt.body).forEach(function(key) { - if (jwt.body.hasOwnProperty(key)) { - jwt.claims[key] = jwt.body[key]; - } - }); - - return cb(null, new JwtAuthenticationResult(self.application,{ - jwt: token, - expandedJwt: jwt, - localValidation: true, - account: { - href: jwt.body.sub - } - })); + return cb(err); + } + + // If there is no KID, this means it was issued by the SDK (not the + // API) from a client credentials grant so we have to do remote + // validation in a different way. + if (!jwt.header.kid) { + var authenticator = new OauthAccessTokenAuthenticator(self.application, token); + return authenticator.authenticate(cb); + } + + // If the KID exists, this was issued by our API from a password grant + if (self.localValidation) { + // Transfers all body fields to `claims` to maintain consistency + // with remote results. Does not remove the body as to preserve + // backwards compatibility. + jwt.claims = {}; + + Object.keys(jwt.body).forEach(function(key) { + if (jwt.body.hasOwnProperty(key)) { + jwt.claims[key] = jwt.body[key]; + } + }); + + return cb(null, new JwtAuthenticationResult(self.application, { + jwt: token, + expandedJwt: jwt, + localValidation: true, + account: { + href: jwt.body.sub } - var href = self.application.href + '/authTokens/' + token; - self.application.dataStore.getResource(href,function(err,response){ - if(err){ + })); + } + + var href = self.application.href + '/authTokens/' + token; + + self.application.dataStore.getResource(href, function(err, response) { + if (err) { + return cb(err); + } + + // Preserve scope + if (jwt.body.scope) { + return njwt.verify(response.jwt, secret, function(err, newJwt) { + if (err) { cb(err); - }else{ - // Preserve scope - if (jwt.body.scope) { - return njwt.verify(response.jwt, secret, function(err, newJwt) { - if (err) { - cb(err); - } - - // Copy the scope on the authorized token - newJwt.body.scope = jwt.body.scope; - newJwt.setSigningKey(secret); - response.jwt = newJwt.compact(); - response.expandedJwt.claims.scope = jwt.body.scope; - - cb(null, new JwtAuthenticationResult(self.application,response)); - }); - } - - cb(null, new JwtAuthenticationResult(self.application,response)); } - }); - }else{ - // If there is no KID, this means it was issued by the SDK (not the - // API) from a client credentials grant so we have to do remote - // validation in a different way. - var authenticator = new OauthAccessTokenAuthenticator(self.application, token); - authenticator.authenticate(cb); + // Copy the scope on the authorized token + newJwt.body.scope = jwt.body.scope; + newJwt.setSigningKey(secret); + response.jwt = newJwt.compact(); + response.expandedJwt.claims.scope = jwt.body.scope; + + cb(null, new JwtAuthenticationResult(self.application, response)); + }); } - } + + cb(null, new JwtAuthenticationResult(self.application, response)); + }); }); } catch (err) { cb(err); } return this; -},'JwtAuthenticator is deprecated, please use StormpathAccessTokenAuthenticator instead.'); +}, 'JwtAuthenticator is deprecated, please use StormpathAccessTokenAuthenticator instead.'); module.exports = JwtAuthenticator; From ef1eb3234a2c72fa9d9926c35501d8defa6e7c77 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 24 Jan 2017 15:04:45 +0100 Subject: [PATCH 8/9] fix formatting --- ...path-access-token-authentication-result.js | 7 +- .../stormpath-access-token-authenticator.js | 47 ++++---- test/it/access-token-authenticator.js | 104 +++++++----------- 3 files changed, 65 insertions(+), 93 deletions(-) diff --git a/lib/oauth/stormpath-access-token-authentication-result.js b/lib/oauth/stormpath-access-token-authentication-result.js index eca9437f..f586f9dd 100644 --- a/lib/oauth/stormpath-access-token-authentication-result.js +++ b/lib/oauth/stormpath-access-token-authentication-result.js @@ -17,7 +17,7 @@ var utils = require('../utils'); */ function StormpathAccessTokenAuthenticationResult(client, data) { if (!(this instanceof StormpathAccessTokenAuthenticationResult)) { - return new StormpathAccessTokenAuthenticationResult(client,data); + return new StormpathAccessTokenAuthenticationResult(client, data); } Object.defineProperty(this, 'client', { @@ -30,7 +30,6 @@ function StormpathAccessTokenAuthenticationResult(client, data) { this[key] = data[key]; } } - } /** @@ -89,12 +88,12 @@ StormpathAccessTokenAuthenticationResult.prototype.expandedJwt = null; * @param {Function} callback * The callback to call with the parameters (err, {@link Account}). */ -StormpathAccessTokenAuthenticationResult.prototype.getAccount = function getAccount(/* [options,] callback */) { +StormpathAccessTokenAuthenticationResult.prototype.getAccount = function getAccount( /* [options,] callback */ ) { var args = utils.resolveArgs(arguments, ['options', 'callback'], true); this.client.getAccount(this.account.href, args.options, require('../resource/Account'), args.callback); }; -StormpathAccessTokenAuthenticationResult.prototype.getApplication = function getApplication(/* [options,] callback */) { +StormpathAccessTokenAuthenticationResult.prototype.getApplication = function getApplication( /* [options,] callback */ ) { var args = utils.resolveArgs(arguments, ['options', 'callback'], true); this.client.getApplication(this.application.href, args.options, require('../resource/Application'), args.callback); diff --git a/lib/oauth/stormpath-access-token-authenticator.js b/lib/oauth/stormpath-access-token-authenticator.js index 54e570bb..69d8db80 100644 --- a/lib/oauth/stormpath-access-token-authenticator.js +++ b/lib/oauth/stormpath-access-token-authenticator.js @@ -1,26 +1,28 @@ -var Client = require('../Client'); - var nJwt = require('njwt'); - +var Client = require('../Client'); var StormpathAccessTokenAuthenticationResult = require('./stormpath-access-token-authentication-result'); -function tenantAdminKeyResolver(client, kid, callback){ - client.getApiKeyById(kid, function (err, apiKey) { +function tenantAdminKeyResolver(client, kid, callback) { + client.getApiKeyById(kid, function(err, apiKey) { if (err) { return callback(err); } - apiKey.getAccount(function (err, account) { + + apiKey.getAccount(function(err, account) { if (err) { return callback(err); } - account.getDirectory(function (err, directory) { + + account.getDirectory(function(err, directory) { if (err) { return callback(err); } + if (directory.name === 'Stormpath Administrators') { return callback(null, apiKey.secret); } - return callback(new Error('Invalid kid')); + + callback(new Error('Invalid kid')); }); }); }); @@ -131,16 +133,15 @@ StormpathAccessTokenAuthenticator.prototype.forApplication = function forApplica * console.log(err); * } else { * authenticationResult.getAccount(function(err, account){ - * console.log('Authenticated Account', account); + * console.log('Authenticated Account', account); * }); * } * }); - * */ -StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate (jwtAccessTokenString, callback){ +StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate(jwtAccessTokenString, callback) { var self = this; - self.verifier.verify(jwtAccessTokenString, function (err, jwt) { + self.verifier.verify(jwtAccessTokenString, function(err, jwt) { if (err) { return callback(err); } @@ -149,32 +150,30 @@ StormpathAccessTokenAuthenticator.prototype.authenticate = function authenticate // Validate against the provided application, if configured. // Otherwise, validate directly against the access token resource - if (self.forApplicationHref) { - resourceHref = self.forApplicationHref + '/authTokens/'+ jwtAccessTokenString; + resourceHref = self.forApplicationHref + '/authTokens/' + jwtAccessTokenString; } else { - resourceHref = self.client.config.client.baseUrl + '/accessTokens/'+ jwt.body.jti; + resourceHref = self.client.config.client.baseUrl + '/accessTokens/' + jwt.body.jti; } - // Bypass the cache if local validation is disabled - - var query = self.localValidation ? null : { nocache: true}; + // Bypass the cache if local validation is disabled. + var query = self.localValidation ? null : { + nocache: true + }; - self.client.getResource(resourceHref, query, function(err, authTokenResponse){ - if(err){ + self.client.getResource(resourceHref, query, function(err, authTokenResponse) { + if (err) { return callback(err); } - // If the incoming token has scope, preserve that for the developer - + // If the incoming token has scope, preserve that for the developer. if (jwt.body.scope) { authTokenResponse.expandedJwt.claims.scope = jwt.claims.scope; } return callback(null, new StormpathAccessTokenAuthenticationResult(self.client, authTokenResponse)); }); - }); }; -module.exports = StormpathAccessTokenAuthenticator; \ No newline at end of file +module.exports = StormpathAccessTokenAuthenticator; diff --git a/test/it/access-token-authenticator.js b/test/it/access-token-authenticator.js index 5978d411..02e26886 100644 --- a/test/it/access-token-authenticator.js +++ b/test/it/access-token-authenticator.js @@ -1,13 +1,13 @@ +var async = require('async'); +var nJwt = require('njwt'); +var uuid = require('uuid'); var common = require('../common'); var helpers = require('./helpers'); var assert = common.assert; var stormpath = require('../../'); - -var async = require('async'); -var nJwt = require('njwt'); -var uuid = require('uuid'); +var StormpathAccessTokenAuthenticationResult = require('../../lib/oauth/stormpath-access-token-authentication-result'); function getAccessTokenOrFail(account, application, done, callback) { stormpath.OAuthAuthenticator(application).authenticate({ @@ -16,61 +16,52 @@ function getAccessTokenOrFail(account, application, done, callback) { username: account.username, password: account.password } - },function(err, result){ - + }, function(err, result) { if (err) { return done(err); } callback(result.accessTokenResponse.access_token); - }); } -var StormpathAccessTokenAuthenticationResult = require('../../lib/oauth/stormpath-access-token-authentication-result'); - -describe('StormpathAccessTokenAuthenticator',function(){ - +describe('StormpathAccessTokenAuthenticator', function() { var application1, application2; - var environmentApiKey, environmentClient; - var newAccount = helpers.fakeAccount(); - var otherApiKey, otherClient; - before(function(done){ - + before(function(done) { async.parallel({ application: helpers.createApplication.bind(null), - apiKey: function (next) { - + apiKey: function(next) { // Create another API Key in this tenant, so that we can test the ability to // validate a token with multiple keys within the same tenant. - - helpers.getClient(function (client) { - + helpers.getClient(function(client) { environmentClient = client; environmentApiKey = client.config.client.apiKey; - client.getDirectories({name: 'Stormpath Administrators', expand: 'accounts'}, function (err, collection) { + client.getDirectories({ + name: 'Stormpath Administrators', + expand: 'accounts' + }, function(err, collection) { if (err) { return done(err); } var href = collection.items[0].accounts.items[0].href + '/apiKeys'; - client.createResource(href, null, function (err, apiKey) { + client.createResource(href, null, function(err, apiKey) { if (err) { return done(err); } + next(null, apiKey); }); }); }); } - }, function (err, results) { - + }, function(err, results) { if (err) { return done(err); } @@ -85,43 +76,36 @@ describe('StormpathAccessTokenAuthenticator',function(){ async.parallel({ account: application1.createAccount.bind(application1, newAccount), application: helpers.createApplication.bind(null) - }, function (err, results) { - + }, function(err, results) { if (err) { return done(err); } application2 = results.application; + done(); }); }); - }); - after(function(done){ - + after(function(done) { async.parallel([ helpers.cleanupApplicationAndStores.bind(null, application1), helpers.cleanupApplicationAndStores.bind(null, application2), otherApiKey.delete.bind(otherApiKey) ], done); - }); - it('Should validate tokens that are issued by the current tenant', function (done) { - + it('should validate tokens that are issued by the current tenant', function(done) { // Test that two different keys for the same tenant will yield the same result - getAccessTokenOrFail(newAccount, application1, done, function(accessToken) { - var authenticatorA = stormpath.StormpathAccessTokenAuthenticator(environmentClient); - var authenticatorB = stormpath.StormpathAccessTokenAuthenticator(otherClient); async.parallel({ resultA: authenticatorA.authenticate.bind(authenticatorA, accessToken), resultB: authenticatorB.authenticate.bind(authenticatorB, accessToken) - }, function (err, results) { + }, function(err, results) { if (err) { return done(err); @@ -129,97 +113,87 @@ describe('StormpathAccessTokenAuthenticator',function(){ assert.instanceOf(results.resultA, StormpathAccessTokenAuthenticationResult); assert.instanceOf(results.resultB, StormpathAccessTokenAuthenticationResult); + done(); }); }); - }); - it('Should reject tokens that are not signed by a key belonging to the current tenant', function (done) { - + it('should reject tokens that are not signed by a key belonging to the current tenant', function(done) { // A token that is not signed by this tenant + var otherJwt = nJwt.create({ + hello: 'world' + }, uuid()); - var otherJwt = nJwt.create({ hello:'world' }, uuid()); otherJwt.header.kid = 'foo'; - var otherToken = otherJwt.compact(); + var otherToken = otherJwt.compact(); var authenticator = stormpath.StormpathAccessTokenAuthenticator(environmentClient); - authenticator.authenticate(otherToken, function (err) { + authenticator.authenticate(otherToken, function(err) { assert.instanceOf(err, Error); assert.equal(err.message, 'Error while resolving signing key for kid "foo"'); done(); }); }); - it('Should reject tokens that try to forge the kid header', function (done) { + it('should reject tokens that try to forge the kid header', function(done) { + var otherJwt = nJwt.create({ + hello: 'world' + }, uuid()); - var otherJwt = nJwt.create({ hello:'world' }, uuid()); otherJwt.header.kid = environmentApiKey.id; var otherToken = otherJwt.compact(); var authenticator = stormpath.StormpathAccessTokenAuthenticator(environmentClient); - authenticator.authenticate(otherToken, function (err) { - + authenticator.authenticate(otherToken, function(err) { assert.instanceOf(err, Error); assert.equal(err.message, 'Signature verification failed'); done(); - }); }); - it('Should reject tokens if they are not issued by the expected application', function (done) { - + it('should reject tokens if they are not issued by the expected application', function(done) { getAccessTokenOrFail(newAccount, application1, done, function(accessToken) { - var authenticator = stormpath.StormpathAccessTokenAuthenticator(environmentClient); - authenticator.forApplication(application2); - authenticator.authenticate(accessToken, function (err) { - + authenticator.authenticate(accessToken, function(err) { assert.instanceOf(err, Error); assert.equal(err.code, 10014); done(); - }); }); - }); - it('Should allow caching for faster authentication', function (done) { - + it('should allow caching for faster authentication', function(done) { getAccessTokenOrFail(newAccount, application1, done, function(accessToken) { - var authenticator = stormpath.StormpathAccessTokenAuthenticator(new stormpath.Client()); authenticator.withLocalValidation(); // First attempt with a new client, will take longer due to fetching resources for the first time. - - authenticator.authenticate(accessToken, function (err) { + authenticator.authenticate(accessToken, function(err) { if (err) { return done(err); } // Second attempt should come back immediately, because // we use a memory cache by default - var now = new Date().getTime(); - authenticator.authenticate(accessToken, function (err) { + authenticator.authenticate(accessToken, function(err) { if (err) { return done(err); } var delta = new Date().getTime() - now; - assert.isBelow(delta, 10); + done(); }); }); }); }); - }); From 9ada1a79b265ce3fc7a35d8c7bbfb02e81b29e2d Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 24 Jan 2017 12:56:41 -0800 Subject: [PATCH 9/9] add test for client.getApiKeyById required some fixes for env hacks that are done earlier in this script --- test/sp.client_test.js | 55 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/test/sp.client_test.js b/test/sp.client_test.js index e489b610..fc867d85 100644 --- a/test/sp.client_test.js +++ b/test/sp.client_test.js @@ -7,6 +7,7 @@ var expect = common.expect; var Account = require('../lib/resource/Account'); var Application = require('../lib/resource/Application'); +var ApiKey = require('../lib/resource/ApiKey'); var Challenge = require('../lib/resource/Challenge'); var Client = require('../lib/Client'); var DataStore = require('../lib/ds/DataStore'); @@ -79,7 +80,11 @@ describe('Client', function () { client.on('ready', function () { expect(client._dataStore.requestExecutor.baseUrl).to.equal('https://api.stormpath.com/v1'); // restore environment value - process.env.STORMPATH_CLIENT_BASEURL = oldValue; + if (oldValue) { + process.env.STORMPATH_CLIENT_BASEURL = oldValue; + } else { + delete process.env.STORMPATH_CLIENT_BASEURL; + } done(); }); @@ -99,7 +104,11 @@ describe('Client', function () { client.on('ready', function () { expect(client._dataStore.requestExecutor.baseUrl).to.equal(url); // restore environment value - process.env.STORMPATH_CLIENT_BASEURL = oldValue; + if (oldValue) { + process.env.STORMPATH_CLIENT_BASEURL = oldValue; + } else { + delete process.env.STORMPATH_CLIENT_BASEURL; + } done(); }); }); @@ -107,6 +116,7 @@ describe('Client', function () { it('should allow me to change the base url through the environment',function(done){ // temporarily set a new environment provided url, save the old one if it exists var oldValue = process.env.STORMPATH_CLIENT_BASEURL; + process.env.STORMPATH_CLIENT_BASEURL = 'https://foo/v1'; var client = makeTestClient({apiKey: apiKey }); @@ -118,7 +128,11 @@ describe('Client', function () { client.on('ready', function () { expect(client._dataStore.requestExecutor.baseUrl).to.equal('https://foo/v1'); // restore environment value - process.env.STORMPATH_CLIENT_BASEURL = oldValue; + if (oldValue) { + process.env.STORMPATH_CLIENT_BASEURL = oldValue; + } else { + delete process.env.STORMPATH_CLIENT_BASEURL; + } done(); }); }); @@ -1027,6 +1041,41 @@ describe('Client', function () { }); }); + describe('call to get ApiKeyById', function () { + var sandbox, client, getResourceStub, cbSpy, href; + + before(function (done) { + sandbox = sinon.sandbox.create(); + cbSpy = sandbox.spy(); + href = 'https://api.stormpath.com/v1/apiKeys/foo'; + + client = makeTestClient(); + + client.on('error', function (err) { + throw err; + }); + + client.on('ready', function () { + getResourceStub = sandbox.stub(client._dataStore, 'getResource', function (href, options, ctor, cb) { + cb(); + }); + + client.getApiKeyById('foo', null, cbSpy); + + done(); + }); + }); + + after(function () { + sandbox.restore(); + }); + + it('should get account', function () { + getResourceStub.should.have.been + .calledWith(href, null, ApiKey, cbSpy); + }); + }); + describe('call to get application', function () { var sandbox, client, getResourceStub, cbSpy, href, opt; before(function (done) {