From 7b8f81ac5d4199085281c83af70d2c1df528f809 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Fri, 25 Nov 2016 14:38:25 +0100 Subject: [PATCH 1/3] Update Node SDK dep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 092ad997..8e9f8e10 100644 --- a/package.json +++ b/package.json @@ -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", From 9518409f36174a2b4670bfaefab9159a620340b1 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Fri, 25 Nov 2016 14:38:41 +0100 Subject: [PATCH 2/3] Update client_credentials to use new Node implementation --- lib/controllers/get-token.js | 40 +++++++++------ test/controllers/test-get-token.js | 49 +++++++++++++------ .../test-api-authentication-required.js | 2 +- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/lib/controllers/get-token.js b/lib/controllers/get-token.js index 59567e59..e1a14860 100644 --- a/lib/controllers/get-token.js +++ b/lib/controllers/get-token.js @@ -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 || {}; @@ -88,8 +107,13 @@ module.exports = function (req, res) { break; case 'password': case 'refresh_token': + case 'client_credentials': var authenticator = new stormpath.OAuthAuthenticator(application); + if (grantType === 'client_credentials') { + resolveClientCredentialsAuthFields(req); + } + authenticator.authenticate(req, function (err, authResult) { if (err) { return writeErrorResponse(err); @@ -108,22 +132,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' diff --git a/test/controllers/test-get-token.js b/test/controllers/test-get-token.js index a717cbb8..0528e061 100644 --- a/test/controllers/test-get-token.js +++ b/test/controllers/test-get-token.js @@ -33,7 +33,7 @@ describe('getToken (OAuth2 token exchange endpoint)', function () { function ready() { readyCount++; if (readyCount === 2) { - done(); + setTimeout(done, 1000); } } @@ -62,7 +62,6 @@ describe('getToken (OAuth2 token exchange endpoint)', function () { enabledFixture.expressApp.on('stormpath.ready', ready); disabledFixture.expressApp.on('stormpath.ready', ready); - }); }); }); @@ -103,11 +102,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(); }); @@ -150,19 +151,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) { diff --git a/test/middlewares/test-api-authentication-required.js b/test/middlewares/test-api-authentication-required.js index 87b07b50..b2731b89 100644 --- a/test/middlewares/test-api-authentication-required.js +++ b/test/middlewares/test-api-authentication-required.js @@ -175,4 +175,4 @@ describe('apiAuthenticationRequired', function () { }); -}); \ No newline at end of file +}); From 150658d9da4ae846c67e13d6eb6749afa54953b1 Mon Sep 17 00:00:00 2001 From: Luka Skukan Date: Fri, 25 Nov 2016 15:28:17 +0100 Subject: [PATCH 3/3] Add support for scope factories --- lib/controllers/get-token.js | 5 ++ test/controllers/test-get-token.js | 76 ++++++++++++++++++++++++++++-- test/fixtures/scope-factory.js | 24 ++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/scope-factory.js diff --git a/lib/controllers/get-token.js b/lib/controllers/get-token.js index e1a14860..83d5e5bf 100644 --- a/lib/controllers/get-token.js +++ b/lib/controllers/get-token.js @@ -110,6 +110,11 @@ module.exports = function (req, res) { 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); } diff --git a/test/controllers/test-get-token.js b/test/controllers/test-get-token.js index 0528e061..aedc7bdf 100644 --- a/test/controllers/test-get-token.js +++ b/test/controllers/test-get-token.js @@ -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'; @@ -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) { - setTimeout(done, 1000); + 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); @@ -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) { @@ -62,6 +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); }); }); }); @@ -228,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(); + }); + }); + }); + }); }); diff --git a/test/fixtures/scope-factory.js b/test/fixtures/scope-factory.js new file mode 100644 index 00000000..055e26df --- /dev/null +++ b/test/fixtures/scope-factory.js @@ -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;