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

Scope factory and client_credentials #550

Open
wants to merge 3 commits into
base: 4.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions lib/controllers/get-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ module.exports = function (req, res) {
res.json(authResult.accessTokenResponse);
}

function resolveClientCredentialsAuthFields(req) {
var authHeader = req && req.headers && req.headers.authorization;

if (authHeader && authHeader.match(/Basic/i)) {
var authorization = authHeader.split(' ').pop();
var parts = new Buffer(authorization, 'base64').toString('utf8').split(':');

req.body.apiKey = {
id: parts[0],
secret: parts[1]
};
} else if (req.body && req.body.client_id && req.body.client_secret) {
req.body.apiKey = {
id: req.body.client_id,
secret: req.body.client_secret
};
}
}

function continueWithHandlers(authResult, preHandler, postHandler, onCompleted) {
var options = req.body || {};

Expand Down Expand Up @@ -88,8 +107,18 @@ module.exports = function (req, res) {
break;
case 'password':
case 'refresh_token':
case 'client_credentials':
var authenticator = new stormpath.OAuthAuthenticator(application);

if (config.web.scopeFactory) {
authenticator.setScopeFactory(config.web.scopeFactory);
authenticator.setScopeFactorySigningKey(config.client.apiKey.secret);
}

if (grantType === 'client_credentials') {
resolveClientCredentialsAuthFields(req);
}

authenticator.authenticate(req, function (err, authResult) {
if (err) {
return writeErrorResponse(err);
Expand All @@ -108,22 +137,6 @@ module.exports = function (req, res) {
});
break;

case 'client_credentials':
application.authenticateApiRequest({
request: req,
ttl: config.web.oauth2.client_credentials.accessToken.ttl,
scopeFactory: function (account, requestedScopes) {
return requestedScopes;
}
}, function (err, authResult) {
if (err) {
return writeErrorResponse(err);
}

res.json(authResult.tokenResponse);
});
break;

default:
writeErrorResponse({
error: 'unsupported_grant_type'
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"parse-iso-duration": "^1.0.0",
"qs": "^6.0.2",
"request": "^2.63.0",
"stormpath": "^0.18.5",
"stormpath": "^0.19.0",
"stormpath-config": "0.0.26",
"utils-merge": "^1.0.0",
"uuid": "^2.0.1",
Expand Down
123 changes: 105 additions & 18 deletions test/controllers/test-get-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
var assert = require('assert');
var request = require('supertest');
var uuid = require('uuid');
var nJwt = require('njwt');

var DefaultExpressApplicationFixture = require('../fixtures/default-express-application');
var helpers = require('../helpers');
var Oauth2DisabledFixture = require('../fixtures/oauth2-disabled');
var ScopeFactoryFixture = require('../fixtures/scope-factory');

describe('getToken (OAuth2 token exchange endpoint)', function () {
var username = uuid.v4() + '@stormpath.com';
Expand All @@ -22,21 +24,36 @@ describe('getToken (OAuth2 token exchange endpoint)', function () {
var stormpathApplication;
var enabledFixture;
var disabledFixture;
var scopeFactoryFixture;
var refreshToken;
var scopeFactory;
var requestScope;
var createScope;

before(function (done) {

/**
* Epic hack to observe two ready events and know when they are both done
* Epic hack to observe all ready events and know when they are both done
*/
var readyCount = 0;
function ready() {
readyCount++;
if (readyCount === 2) {
done();
if (readyCount === 3) {
setTimeout(done, 1500); // HACK see what's up with this!
}
}

requestScope = 'admin';

createScope = function (scope) {
return scope + '-' + username;
};

scopeFactory = function (authenticationResult, requestedScope, callback) {
assert.equal(requestScope, requestedScope);
callback(null, createScope(requestedScope));
};

helpers.createApplication(helpers.createClient(), function (err, app) {
if (err) {
return done(err);
Expand All @@ -46,6 +63,7 @@ describe('getToken (OAuth2 token exchange endpoint)', function () {

enabledFixture = new DefaultExpressApplicationFixture(stormpathApplication);
disabledFixture = new Oauth2DisabledFixture(stormpathApplication);
scopeFactoryFixture = new ScopeFactoryFixture(stormpathApplication, scopeFactory);

app.createAccount(accountData, function (err, account) {
if (err) {
Expand All @@ -62,7 +80,7 @@ describe('getToken (OAuth2 token exchange endpoint)', function () {

enabledFixture.expressApp.on('stormpath.ready', ready);
disabledFixture.expressApp.on('stormpath.ready', ready);

scopeFactoryFixture.expressApp.on('stormpath.ready', ready);
});
});
});
Expand Down Expand Up @@ -103,11 +121,13 @@ describe('getToken (OAuth2 token exchange endpoint)', function () {

request(enabledFixture.expressApp)
.post('/oauth/token')
.auth('woot', 'woot')
.send('client_id=woot')
.send('client_secret=woot')
.send('grant_type=client_credentials')
.auth('woot', 'woot')
.expect(401)
.end(function (err, res) {
assert.equal(res.body && res.body.message, 'Invalid Client Credentials');
assert.equal(res.body && res.body.message, 'API Key Authentication failed.');
assert.equal(res.body && res.body.error, 'invalid_client');
done();
});
Expand Down Expand Up @@ -150,19 +170,35 @@ describe('getToken (OAuth2 token exchange endpoint)', function () {

});

it('should return an access token if grant_type=client_credentials and the credentials are valid', function (done) {

request(enabledFixture.expressApp)
.post('/oauth/token')
.auth(stormpathAccountApiKey.id, stormpathAccountApiKey.secret)
.send('grant_type=client_credentials')
.expect(200)
.end(function (err, res) {
assert(res.body && res.body.access_token);
assert.equal(res.body && res.body.expires_in && res.body.expires_in, 3600);
done();
});
describe('with Auth header', function () {
it('should return an access token if grant_type=client_credentials and the credentials are valid', function (done) {
request(enabledFixture.expressApp)
.post('/oauth/token')
.auth(stormpathAccountApiKey.id, stormpathAccountApiKey.secret)
.send('grant_type=client_credentials')
.expect(200)
.end(function (err, res) {
assert(res.body && res.body.access_token);
assert.equal(res.body && res.body.expires_in && res.body.expires_in, 3600);
done();
});
});
});

describe('with data fields', function () {
it('should return an access token if grant_type=client_credentials and the credentials are valid', function (done) {
request(enabledFixture.expressApp)
.post('/oauth/token')
.send('client_id=' + stormpathAccountApiKey.id)
.send('client_secret=' + stormpathAccountApiKey.secret)
.send('grant_type=client_credentials')
.expect(200)
.end(function (err, res) {
assert(res.body && res.body.access_token);
assert.equal(res.body && res.body.expires_in && res.body.expires_in, 3600);
done();
});
});
});

it('should return an access token & refresh token if grant_type=password and the username & password are valid', function (done) {
Expand Down Expand Up @@ -211,4 +247,55 @@ describe('getToken (OAuth2 token exchange endpoint)', function () {
});

});

describe('scope factories', function () {
var secret;

before(function () {
var config = scopeFactoryFixture.expressApp.get('stormpathConfig');
secret = config.client.apiKey.secret;
});

it('should utilize the scope factory if defined for password grant type', function (done) {
request(scopeFactoryFixture.expressApp)
.post('/oauth/token')
.send('grant_type=password')
.send('username=' + accountData.email)
.send('password=' + accountData.password)
.send('scope=' + requestScope)
.expect(200)
.end(function (err, res) {
assert(res.body && res.body.access_token);
nJwt.verify(res.body.access_token, secret, function (err, token) {
if (err) {
return done(err);
}

assert.equal(token.body.scope, createScope(requestScope));
done();
});
});
});

it('should utilize the scope factory if defined for client_credentials grant type', function (done) {
request(scopeFactoryFixture.expressApp)
.post('/oauth/token')
.send('client_id=' + stormpathAccountApiKey.id)
.send('client_secret=' + stormpathAccountApiKey.secret)
.send('grant_type=client_credentials')
.send('scope=' + requestScope)
.expect(200)
.end(function (err, res) {
assert(res.body && res.body.access_token);
nJwt.verify(res.body.access_token, secret, function (err, token) {
if (err) {
return done(err);
}

assert.equal(token.body.scope, createScope(requestScope));
done();
});
});
});
});
});
24 changes: 24 additions & 0 deletions test/fixtures/scope-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

var helpers = require('../helpers');

/**
* This fixture creates an Express application which has express-stormpath
* integrated and uses a scope factory.
*
* It takes the Stormpath application reference and the requisite scope factory
* as its fixture constructor arguments. It is assumed that API Keys for
* Stormpath are already in the environment.
*
* @param {object} stormpathApplication
*/
function DefaultExpressApplicationFixtureFixture(stormpathApplication, scopeFactory) {
this.expressApp = helpers.createStormpathExpressApp({
application: stormpathApplication,
web: {
scopeFactory: scopeFactory
}
});
}

module.exports = DefaultExpressApplicationFixtureFixture;
2 changes: 1 addition & 1 deletion test/middlewares/test-api-authentication-required.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,4 @@ describe('apiAuthenticationRequired', function () {
});


});
});