diff --git a/core/server/api/app.js b/core/server/api/app.js index 80b01f197d..5b0cd4a06a 100644 --- a/core/server/api/app.js +++ b/core/server/api/app.js @@ -185,12 +185,6 @@ function apiRoutes() { auth.oauth.generateAccessToken ); - apiRouter.post('/authentication/ghost', [ - auth.authenticate.authenticateClient, - auth.authenticate.authenticateGhostUser, - api.http(api.authentication.createTokens) - ]); - apiRouter.post('/authentication/revoke', authenticatePrivate, api.http(api.authentication.revoke)); // ## Uploads diff --git a/core/server/auth/authenticate.js b/core/server/auth/authenticate.js index 4aaee3cb99..07b608d22a 100644 --- a/core/server/auth/authenticate.js +++ b/core/server/auth/authenticate.js @@ -105,29 +105,6 @@ authenticate = { })); } )(req, res, next); - }, - - // ### Authenticate Ghost.org User - authenticateGhostUser: function authenticateGhostUser(req, res, next) { - req.query.code = req.body.authorizationCode; - - if (!req.query.code) { - return next(new errors.UnauthorizedError({message: i18n.t('errors.middleware.auth.accessDenied')})); - } - - passport.authenticate('ghost', {session: false, failWithError: false}, function authenticate(err, user, info) { - if (err) { - return next(err); - } - - if (!user) { - return next(new errors.UnauthorizedError({message: i18n.t('errors.middleware.auth.accessDenied')})); - } - - req.authInfo = info; - req.user = user; - next(); - })(req, res, next); } }; diff --git a/core/server/auth/oauth.js b/core/server/auth/oauth.js index bc045a0c90..eddda7238b 100644 --- a/core/server/auth/oauth.js +++ b/core/server/auth/oauth.js @@ -1,4 +1,5 @@ var oauth2orize = require('oauth2orize'), + passport = require('passport'), models = require('../models'), utils = require('../utils'), errors = require('../errors'), @@ -62,6 +63,42 @@ function exchangePassword(client, username, password, scope, done) { }); } +function exchangeAuthorizationCode(req, res, next) { + if (!req.body.authorizationCode) { + return next(new errors.UnauthorizedError({ + message: i18n.t('errors.middleware.auth.accessDenied') + })); + } + + req.query.code = req.body.authorizationCode; + + passport.authenticate('ghost', {session: false, failWithError: false}, function authenticate(err, user) { + if (err) { + return next(new errors.UnauthorizedError({ + err: err + })); + } + + if (!user) { + return next(new errors.UnauthorizedError({ + message: i18n.t('errors.middleware.auth.accessDenied') + })); + } + + authenticationAPI.createTokens({}, {context: {client_id: req.client.id, user: user.id}}) + .then(function then(response) { + res.json({ + access_token: response.access_token, + refresh_token: response.refresh_token, + expires_in: response.expires_in + }); + }) + .catch(function (err) { + next(err); + }); + })(req, res, next); +} + oauth = { init: function init() { @@ -85,6 +122,23 @@ oauth = { // access token on behalf of the user who authorized the code. oauthServer.exchange(oauth2orize.exchange.refreshToken({userProperty: 'client'}, exchangeRefreshToken)); + + /** + * Exchange authorization_code for an access token. + * We forward to authorization code to Ghost.org. + * + * oauth2orize offers a default implementation via exchange.authorizationCode, but this function + * wraps the express request and response. So no chance to get access to it. + * We use passport to communicate with Ghost.org. Passport's module design requires the express req/res. + * + * For now it's OK to not use exchange.authorizationCode. You can read through the implementation here: + * https://github.com/jaredhanson/oauth2orize/blob/master/lib/exchange/authorizationCode.js + * As you can see, it does some validation and set's some headers, not very very important, + * but it's part of the oauth2 spec. + * + * @TODO: How to use exchange.authorizationCode in combination of passport? + */ + oauthServer.exchange('authorization_code', exchangeAuthorizationCode); }, // ### Generate access token Middleware diff --git a/core/server/middleware/api/spam-prevention.js b/core/server/middleware/api/spam-prevention.js index 823e8ea0c5..4afa623179 100644 --- a/core/server/middleware/api/spam-prevention.js +++ b/core/server/middleware/api/spam-prevention.js @@ -27,7 +27,7 @@ spamPrevention = { if (req.body.username && req.body.grant_type === 'password') { loginSecurity.push({ip: remoteAddress, time: currentTime, email: req.body.username}); - } else if (req.body.grant_type === 'refresh_token') { + } else if (req.body.grant_type === 'refresh_token' || req.body.grant_type === 'authorization_code') { return next(); } else { return next(new errors.BadRequestError({message: i18n.t('errors.middleware.spamprevention.noUsername')})); diff --git a/core/test/unit/auth/oauth_spec.js b/core/test/unit/auth/oauth_spec.js index 9cbb79f705..891fc70237 100644 --- a/core/test/unit/auth/oauth_spec.js +++ b/core/test/unit/auth/oauth_spec.js @@ -1,15 +1,18 @@ -var sinon = require('sinon'), - should = require('should'), - Promise = require('bluebird'), - oAuth = require('../../../server/auth/oauth'), - Models = require('../../../server/models'); +var sinon = require('sinon'), + should = require('should'), + Promise = require('bluebird'), + passport = require('passport'), + testUtils = require('../../utils'), + oAuth = require('../../../server/auth/oauth'), + api = require('../../../server/api'), + errors = require('../../../server/errors'), + models = require('../../../server/models'); describe('OAuth', function () { var next, req, res, sandbox; before(function () { - // Loads all the models - Models.init(); + models.init(); }); beforeEach(function () { @@ -25,10 +28,12 @@ describe('OAuth', function () { describe('Generate Token from Password', function () { beforeEach(function () { - sandbox.stub(Models.Accesstoken, 'destroyAllExpired') + sandbox.stub(models.Accesstoken, 'destroyAllExpired') .returns(new Promise.resolve()); - sandbox.stub(Models.Refreshtoken, 'destroyAllExpired') + sandbox.stub(models.Refreshtoken, 'destroyAllExpired') .returns(new Promise.resolve()); + + oAuth.init(); }); it('Successfully generate access token.', function (done) { @@ -43,18 +48,20 @@ describe('OAuth', function () { res.setHeader = {}; res.end = {}; - sandbox.stub(Models.Client, 'findOne') + sandbox.stub(models.Client, 'findOne') .withArgs({slug: 'test'}).returns(new Promise.resolve({ - id: 1 - })); - sandbox.stub(Models.User, 'check') + id: 1 + })); + + sandbox.stub(models.User, 'check') .withArgs({email: 'username', password: 'password'}).returns(new Promise.resolve({ - id: 1 - })); - sandbox.stub(Models.Accesstoken, 'add') + id: 1 + })); + + sandbox.stub(models.Accesstoken, 'add') .returns(new Promise.resolve()); - sandbox.stub(Models.Refreshtoken, 'add') + sandbox.stub(models.Refreshtoken, 'add') .returns(new Promise.resolve()); sandbox.stub(res, 'setHeader', function () {}); @@ -73,7 +80,7 @@ describe('OAuth', function () { done(err); } }); - oAuth.init(); + oAuth.generateAccessToken(req, res, next); }); @@ -89,10 +96,9 @@ describe('OAuth', function () { res.setHeader = {}; res.end = {}; - sandbox.stub(Models.Client, 'findOne') + sandbox.stub(models.Client, 'findOne') .withArgs({slug: 'test'}).returns(new Promise.resolve()); - oAuth.init(); oAuth.generateAccessToken(req, res, function (err) { err.errorType.should.eql('NoPermissionError'); done(); @@ -111,20 +117,21 @@ describe('OAuth', function () { res.setHeader = {}; res.end = {}; - sandbox.stub(Models.Client, 'findOne') + sandbox.stub(models.Client, 'findOne') .withArgs({slug: 'test'}).returns(new Promise.resolve({ - id: 1 - })); - sandbox.stub(Models.User, 'check') + id: 1 + })); + + sandbox.stub(models.User, 'check') .withArgs({email: 'username', password: 'password'}).returns(new Promise.resolve({ - id: 1 - })); - sandbox.stub(Models.Accesstoken, 'add') + id: 1 + })); + + sandbox.stub(models.Accesstoken, 'add') .returns(new Promise.reject({ message: 'DB error' })); - oAuth.init(); oAuth.generateAccessToken(req, res, function (err) { err.message.should.eql('DB error'); done(); @@ -134,10 +141,12 @@ describe('OAuth', function () { describe('Generate Token from Refreshtoken', function () { beforeEach(function () { - sandbox.stub(Models.Accesstoken, 'destroyAllExpired') + sandbox.stub(models.Accesstoken, 'destroyAllExpired') .returns(new Promise.resolve()); - sandbox.stub(Models.Refreshtoken, 'destroyAllExpired') + sandbox.stub(models.Refreshtoken, 'destroyAllExpired') .returns(new Promise.resolve()); + + oAuth.init(); }); it('Successfully generate access token.', function (done) { @@ -151,19 +160,19 @@ describe('OAuth', function () { res.setHeader = {}; res.end = {}; - sandbox.stub(Models.Refreshtoken, 'findOne') + sandbox.stub(models.Refreshtoken, 'findOne') .withArgs({token: 'token'}).returns(new Promise.resolve({ - toJSON: function () { - return { - expires: Date.now() + 3600 - }; - } - })); + toJSON: function () { + return { + expires: Date.now() + 3600 + }; + } + })); - sandbox.stub(Models.Accesstoken, 'add') + sandbox.stub(models.Accesstoken, 'add') .returns(new Promise.resolve()); - sandbox.stub(Models.Refreshtoken, 'edit') + sandbox.stub(models.Refreshtoken, 'edit') .returns(new Promise.resolve()); sandbox.stub(res, 'setHeader', function () {}); @@ -181,7 +190,7 @@ describe('OAuth', function () { done(err); } }); - oAuth.init(); + oAuth.generateAccessToken(req, res, next); }); @@ -196,10 +205,9 @@ describe('OAuth', function () { res.setHeader = {}; res.end = {}; - sandbox.stub(Models.Refreshtoken, 'findOne') + sandbox.stub(models.Refreshtoken, 'findOne') .withArgs({token: 'token'}).returns(new Promise.resolve()); - oAuth.init(); oAuth.generateAccessToken(req, res, function (err) { err.errorType.should.eql('NoPermissionError'); done(); @@ -217,16 +225,15 @@ describe('OAuth', function () { res.setHeader = {}; res.end = {}; - sandbox.stub(Models.Refreshtoken, 'findOne') + sandbox.stub(models.Refreshtoken, 'findOne') .withArgs({token: 'token'}).returns(new Promise.resolve({ - toJSON: function () { - return { - expires: Date.now() - 3600 - }; - } - })); + toJSON: function () { + return { + expires: Date.now() - 3600 + }; + } + })); - oAuth.init(); oAuth.generateAccessToken(req, res, function (err) { err.errorType.should.eql('UnauthorizedError'); done(); @@ -244,25 +251,109 @@ describe('OAuth', function () { res.setHeader = {}; res.end = {}; - sandbox.stub(Models.Refreshtoken, 'findOne') + sandbox.stub(models.Refreshtoken, 'findOne') .withArgs({token: 'token'}).returns(new Promise.resolve({ - toJSON: function () { - return { - expires: Date.now() + 3600 - }; - } - })); + toJSON: function () { + return { + expires: Date.now() + 3600 + }; + } + })); - sandbox.stub(Models.Accesstoken, 'add') + sandbox.stub(models.Accesstoken, 'add') .returns(new Promise.reject({ message: 'DB error' })); - oAuth.init(); oAuth.generateAccessToken(req, res, function (err) { err.message.should.eql('DB error'); done(); }); }); }); + + describe('Generate Token from Authorization Code', function () { + beforeEach(function () { + sandbox.stub(models.Accesstoken, 'destroyAllExpired') + .returns(new Promise.resolve()); + + sandbox.stub(models.Refreshtoken, 'destroyAllExpired') + .returns(new Promise.resolve()); + + oAuth.init(); + }); + + it('Successfully generate access token.', function (done) { + var user = new models.User(testUtils.DataGenerator.forKnex.createUser()); + + req.body = {}; + req.query = {}; + req.client = { + id: 1 + }; + + req.body.grant_type = 'authorization_code'; + req.body.authorizationCode = '1234'; + + res.json = function (data) { + data.access_token.should.eql('access-token'); + data.refresh_token.should.eql('refresh-token'); + data.expires_in.should.eql(10); + done(); + }; + + sandbox.stub(api.authentication, 'createTokens').returns(Promise.resolve({ + access_token: 'access-token', + refresh_token: 'refresh-token', + expires_in: 10 + })); + + sandbox.stub(passport, 'authenticate', function (name, options, onSuccess) { + return function () { + onSuccess(null, user); + }; + }); + + oAuth.generateAccessToken(req, res, next); + }); + + it('Error: ghost.org', function (done) { + req.body = {}; + req.query = {}; + req.client = { + id: 1 + }; + + req.body.grant_type = 'authorization_code'; + req.body.authorizationCode = '1234'; + + sandbox.stub(passport, 'authenticate', function (name, options, onSuccess) { + return function () { + onSuccess(new Error('validation error')); + }; + }); + + oAuth.generateAccessToken(req, res, function (err) { + should.exist(err); + (err instanceof errors.UnauthorizedError).should.eql(true); + done(); + }); + }); + + it('Error: no authorization_code provided', function (done) { + req.body = {}; + req.query = {}; + req.client = { + id: 1 + }; + + req.body.grant_type = 'authorization_code'; + + oAuth.generateAccessToken(req, res, function (err) { + should.exist(err); + (err instanceof errors.UnauthorizedError).should.eql(true); + done(); + }); + }); + }); });