Skip to content
This repository has been archived by the owner on Dec 12, 2018. It is now read-only.

Commit

Permalink
Merge pull request #597 from stormpath/access-token-authenticator
Browse files Browse the repository at this point in the history
Access token authenticator
  • Loading branch information
robertjd committed Jan 24, 2017
2 parents c36ac6b + 9ada1a7 commit 680c58a
Show file tree
Hide file tree
Showing 11 changed files with 673 additions and 73 deletions.
8 changes: 8 additions & 0 deletions lib/Client.js
Expand Up @@ -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.
*
Expand Down
9 changes: 8 additions & 1 deletion 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');
Expand All @@ -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', 'accessTokens'];

// singleton of DisabledCache wrapped into Cache instance
var disabledCache = new Cache({store: DisabledCache});
Expand Down Expand Up @@ -123,6 +124,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;
}
Expand Down
50 changes: 47 additions & 3 deletions lib/ds/DataStore.js
Expand Up @@ -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;},
Expand All @@ -70,7 +71,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] &&
Expand Down Expand Up @@ -132,7 +138,24 @@ DataStore.prototype.getResource = function getResource(/* href, [query], [Instan
InstanceCtor: (args.length > 0 && args[0] instanceof Function) ? args.shift() : InstanceResource
};

return _this.exec(callback);
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);
}
delete resource.requestData;
}

callback.apply(null, arguments);
}

return _this.exec(loggerProxy);
};

/**
Expand Down Expand Up @@ -270,6 +293,10 @@ DataStore.prototype._buildRequestQuery = function(){
return queryStringObj;
};

function applyRequestData(resource, data){
resource.requestData = data;
}

/**
* @private
* @param callback
Expand All @@ -289,10 +316,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) {
Expand All @@ -315,6 +351,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;
}
Expand Down
133 changes: 69 additions & 64 deletions 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');
Expand All @@ -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
Expand Down Expand Up @@ -60,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
});
};

/**
Expand All @@ -89,81 +95,80 @@ 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;

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) {
} catch (err) {
cb(err);
}

return this;
};
}, 'JwtAuthenticator is deprecated, please use StormpathAccessTokenAuthenticator instead.');

module.exports = JwtAuthenticator;

0 comments on commit 680c58a

Please sign in to comment.