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

Commit 0b2f99f

Browse files
committed
Merge pull request #248 from stormpath/feature-async-scope-factory
Feature: Async scope factory
2 parents 80fe3a1 + e883a64 commit 0b2f99f

File tree

3 files changed

+106
-29
lines changed

3 files changed

+106
-29
lines changed

lib/authc/OAuthBasicExchangeAuthenticator.js

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ function OAuthBasicExchangeAuthenticator(application,request,ttl,scopeFactory, r
1515
if(parts.length!==2){
1616
return new ApiAuthRequestError({userMessage: 'Invalid Authorization value', statusCode: 400});
1717
}
18+
1819
if(request.method!=='POST'){
1920
return new ApiAuthRequestError({userMessage: 'Must use POST for token exchange, see http://tools.ietf.org/html/rfc6749#section-3.2'});
2021
}
22+
2123
this.id = parts[0];
2224
this.secret = parts[1];
2325
this.application = application;
@@ -26,8 +28,8 @@ function OAuthBasicExchangeAuthenticator(application,request,ttl,scopeFactory, r
2628
this.requestedScope = requestedScope;
2729
}
2830

29-
OAuthBasicExchangeAuthenticator.prototype.defaultScopeFactory = function defaultScopeFactory() {
30-
return '';
31+
OAuthBasicExchangeAuthenticator.prototype.defaultScopeFactory = function defaultScopeFactory(account, requestedScope, callback) {
32+
callback(null, '');
3133
};
3234

3335
OAuthBasicExchangeAuthenticator.prototype.authenticate = function authenticate(callback) {
@@ -42,30 +44,72 @@ OAuthBasicExchangeAuthenticator.prototype.authenticate = function authenticate(c
4244
(apiKey.account.status==='ENABLED')
4345
){
4446
var result = new AuthenticationResult(apiKey,self.application.dataStore);
47+
4548
result.forApiKey = apiKey;
4649
result.application = self.application;
47-
result.tokenResponse = self.buildTokenResponse(apiKey);
48-
callback(null,result);
50+
51+
self.buildTokenResponse(apiKey, function onTokenResponse(err, tokenResponse) {
52+
if (err) {
53+
callback(err);
54+
return;
55+
}
56+
57+
result.tokenResponse = tokenResponse;
58+
59+
callback(null, result);
60+
});
4961
}else{
5062
callback(new ApiAuthRequestError({userMessage: 'Invalid Client Credentials', error: 'invalid_client', statusCode: 401}));
5163
}
5264
}
5365
});
5466
};
5567

56-
OAuthBasicExchangeAuthenticator.prototype.buildTokenResponse = function buildTokenResponse(apiKey) {
68+
OAuthBasicExchangeAuthenticator.prototype.buildTokenResponse = function buildTokenResponse(apiKey, callback) {
5769
var self = this;
58-
var scope = self.scopeFactory(apiKey.account,self.requestedScope);
59-
// TODO v1.0.0 - remove string option for tokens, should be array only
60-
return {
61-
"access_token": self.buildAccesstoken(apiKey.account),
62-
"token_type":"bearer",
63-
"expires_in": self.ttl,
64-
"scope": Array.isArray(scope) ? scope.join(' ') : scope
65-
};
70+
71+
var account = apiKey.account;
72+
var requestedScope = self.requestedScope;
73+
74+
function retrieveScopeFactoryResult(callback) {
75+
var hasBeenCalled = false;
76+
77+
function callbackWithResult(err, scope) {
78+
if (hasBeenCalled) {
79+
throw new Error('Callback has already been called once. Assert that your scopeFactory doesn\'t return a result while also calling the callback.');
80+
}
81+
82+
hasBeenCalled = true;
83+
84+
callback(err, scope);
85+
}
86+
87+
var optionalResult = self.scopeFactory(account, requestedScope, callbackWithResult);
88+
89+
// For backward-compatibility: If we have a result then call the callback immediately,
90+
// else expect it to be handled by the scopeFactory function.
91+
if (optionalResult || optionalResult === '') {
92+
callbackWithResult(null, optionalResult);
93+
}
94+
}
95+
96+
retrieveScopeFactoryResult(function onScopeResolved(err, scopeResult) {
97+
if (err) {
98+
callback(err);
99+
return;
100+
}
101+
102+
// TODO v1.0.0 - remove string option for tokens, should be array only
103+
callback(null, {
104+
access_token: self.buildAccessToken(account, scopeResult),
105+
token_type: 'bearer',
106+
expires_in: self.ttl,
107+
scope: Array.isArray(scopeResult) ? scopeResult.join(' ') : scopeResult
108+
});
109+
});
66110
};
67111

68-
OAuthBasicExchangeAuthenticator.prototype.buildAccesstoken = function buildAccesstoken(account) {
112+
OAuthBasicExchangeAuthenticator.prototype.buildAccessToken = function buildAccessToken(account, scope) {
69113
var self = this;
70114
var now = nowEpochSeconds();
71115

@@ -76,8 +120,6 @@ OAuthBasicExchangeAuthenticator.prototype.buildAccesstoken = function buildAcces
76120
exp: now + self.ttl
77121
};
78122

79-
var scope = self.scopeFactory(account,self.requestedScope);
80-
81123
if(scope){
82124
// TODO v1.0.0 - remove string option, should be array only
83125
_jwt.scope = Array.isArray(scope) ? scope.join(' ') : scope;

quickstart.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111

1212
var stormpath = require('stormpath');
1313

14-
var homeDir = process.env[(process.platform === 'win32' ? 'USERPROFILE' : 'HOME')];
15-
var apiKeyFilePath = homeDir + '/.stormpath/apiKey.properties';
16-
1714
//helper function to prevent any data collisions in the tenant while running the quickstart:
1815
function unique(aString) {
1916
return aString + '-' + require('node-uuid').v4().toString();
@@ -25,16 +22,11 @@ var client, application, account, group = null;
2522
var accountEmail = unique('jlpicard') + '@mailinator.com';
2623

2724
// ==================================================
28-
// Step 1 - Load the API Key file and create a Client
25+
// Step 1 - Create a client and wait for it to ready
2926
// ==================================================
30-
stormpath.loadApiKey(apiKeyFilePath, function (err, apiKey) {
31-
if (err) throw err;
32-
33-
client = new stormpath.Client({apiKey: apiKey});
27+
var client = new stormpath.Client();
3428

35-
//client is available, kick off the quickstart steps:
36-
createApplication();
37-
});
29+
client.on('ready', createApplication);
3830

3931
// ==================================================
4032
// Step 2 - Register an application with Stormpath

test/it/api_auth_it.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ describe('Application.authenticateApiRequest',function(){
249249
describe('with a previously issued access token',function(){
250250
var customScope = 'custom-scope';
251251
var accessToken;
252+
252253
before(function(done){
253254
var requestObject = {
254255
headers: {
@@ -270,6 +271,7 @@ describe('Application.authenticateApiRequest',function(){
270271
done();
271272
});
272273
});
274+
273275
describe('using Bearer authorization',function(){
274276
describe('and access_token is passed as Authorization: Bearer <token>',function(){
275277
var result;
@@ -300,6 +302,7 @@ describe('Application.authenticateApiRequest',function(){
300302
assert.equal(result[1].grantedScopes[0],customScope);
301303
});
302304
});
305+
303306
describe('and access_token is tampered with',function(){
304307
var result;
305308
before(function(done){
@@ -545,21 +548,22 @@ describe('Application.authenticateApiRequest',function(){
545548
});
546549

547550
describe('with a scope factory',function(){
548-
549551
var result;
552+
var requestObject;
550553
var requestedScope = 'scope-a scope-b';
551554
var givenScope = ['given-scope-a given-scope-b'];
552555
var scopeFactoryArgs;
553556
var decodedAccessToken;
554557

555558
before(function(done){
556-
var requestObject = {
559+
requestObject = {
557560
headers: {
558561
'authorization': 'Basic ' + new Buffer([apiKey.id,apiKey.secret].join(':')).toString('base64')
559562
},
560563
method: 'POST',
561564
url: '/some/resource?grant_type=client_credentials&scope='+requestedScope
562565
};
566+
563567
app.authenticateApiRequest({
564568
request: requestObject,
565569
scopeFactory: function(account,requestedScope){
@@ -596,6 +600,45 @@ describe('Application.authenticateApiRequest',function(){
596600
assert.equal(result[1].tokenResponse.scope,givenScope.join(' '));
597601
});
598602

603+
describe('that is given a callback', function () {
604+
it('should return an error if given one', function(done) {
605+
var expectedError = new Error('expected_error');
606+
607+
app.authenticateApiRequest({
608+
request: requestObject,
609+
scopeFactory: function(account, requestedScope, callback){
610+
callback(expectedError);
611+
}
612+
},function(err){
613+
assert.equal(err, expectedError);
614+
done();
615+
});
616+
});
617+
618+
it('should call the scope factory with the requested scope', function(done) {
619+
app.authenticateApiRequest({
620+
request: requestObject,
621+
scopeFactory: function(account, requestedScope, callback){
622+
scopeFactoryArgs = [account, requestedScope];
623+
callback(null, givenScope);
624+
}
625+
},function(err, value){
626+
result = [err, value];
627+
628+
decodedAccessToken = nJwt.verify(
629+
result[1].tokenResponse.access_token,
630+
client._dataStore.requestExecutor.options.client.apiKey.secret,
631+
'HS256'
632+
);
633+
634+
var requestedScopes = requestedScope.split(' ');
635+
assert.equal(scopeFactoryArgs[1][0], requestedScopes[0]);
636+
assert.equal(scopeFactoryArgs[1][1], requestedScopes[1]);
637+
638+
done();
639+
});
640+
});
641+
});
599642
});
600643

601644
describe('with a custom ttl',function(){

0 commit comments

Comments
 (0)