diff --git a/core/server/api/users.js b/core/server/api/users.js index 84c1c00d52..89cc28a8d1 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -212,7 +212,7 @@ users = { }); }); }).catch(function handleError(error) { - return errors.handleAPIError(error, 'You do not have permission to edit this user'); + return errors.formatAndRejectAPIError(error, 'You do not have permission to edit this user'); }); } @@ -280,7 +280,7 @@ users = { return options; }).catch(function handleError(error) { - return errors.handleAPIError(error, 'You do not have permission to add this user'); + return errors.formatAndRejectAPIError(error, 'You do not have permission to add this user'); }); } @@ -375,7 +375,7 @@ users = { options.status = 'all'; return options; }).catch(function handleError(error) { - return errors.handleAPIError(error, 'You do not have permission to destroy this user.'); + return errors.formatAndRejectAPIError(error, 'You do not have permission to destroy this user.'); }); } @@ -407,7 +407,7 @@ users = { return Promise.reject(new errors.InternalServerError(error)); }); }, function (error) { - return errors.handleAPIError(error); + return errors.formatAndRejectAPIError(error); }); } @@ -442,7 +442,7 @@ users = { return canThis(options.context).edit.user(options.data.password[0].user_id).then(function permissionGranted() { return options; }).catch(function (error) { - return errors.handleAPIError(error, 'You do not have permission to change the password for this user'); + return errors.formatAndRejectAPIError(error, 'You do not have permission to change the password for this user'); }); } @@ -494,7 +494,7 @@ users = { }).then(function () { return options; }).catch(function (error) { - return errors.handleAPIError(error); + return errors.formatAndRejectAPIError(error); }); } @@ -520,7 +520,7 @@ users = { return pipeline(tasks, object, options).then(function formatResult(result) { return Promise.resolve({users: result}); }).catch(function (error) { - return errors.handleAPIError(error); + return errors.formatAndRejectAPIError(error); }); } }; diff --git a/core/server/api/utils.js b/core/server/api/utils.js index 1d3ca57df7..4c0f8b6106 100644 --- a/core/server/api/utils.js +++ b/core/server/api/utils.js @@ -182,7 +182,7 @@ utils = { return permsPromise.then(function permissionGranted() { return options; }).catch(function handleError(error) { - return errors.handleAPIError(error); + return errors.formatAndRejectAPIError(error); }); }; }, @@ -213,7 +213,7 @@ utils = { // forward error to next catch() return Promise.reject(error); }).catch(function handleError(error) { - return errors.handleAPIError(error); + return errors.formatAndRejectAPIError(error); }); }; }, diff --git a/core/server/errors/index.js b/core/server/errors/index.js index df223e16df..6c010ab8e3 100644 --- a/core/server/errors/index.js +++ b/core/server/errors/index.js @@ -207,7 +207,7 @@ errors = { return {errors: errors, statusCode: statusCode}; }, - handleAPIError: function (error, permsMessage) { + formatAndRejectAPIError: function (error, permsMessage) { if (!error) { return this.rejectError( new this.NoPermissionError(permsMessage || 'You do not have permission to perform this action') @@ -234,6 +234,14 @@ errors = { return this.rejectError(new this.InternalServerError(error)); }, + handleAPIError: function errorHandler(err, req, res, next) { + /*jshint unused:false */ + var httpErrors = this.formatHttpErrors(err); + this.logError(err); + // Send a properly formatted HTTP response containing the errors + res.status(httpErrors.statusCode).json({errors: httpErrors.errors}); + }, + renderErrorPage: function (code, err, req, res, next) { /*jshint unused:false*/ var self = this, @@ -374,6 +382,7 @@ _.each([ 'logErrorAndExit', 'logErrorWithRedirect', 'handleAPIError', + 'formatAndRejectAPIError', 'formatHttpErrors', 'renderErrorPage', 'error404', diff --git a/core/server/middleware/api-error-handlers.js b/core/server/middleware/api-error-handlers.js deleted file mode 100644 index 42a21a98e9..0000000000 --- a/core/server/middleware/api-error-handlers.js +++ /dev/null @@ -1,9 +0,0 @@ -var errors = require('../errors'); - -module.exports.errorHandler = function errorHandler(err, req, res, next) { - /*jshint unused:false */ - var httpErrors = errors.formatHttpErrors(err); - errors.logError(err); - // Send a properly formatted HTTP response containing the errors - res.status(httpErrors.statusCode).json({errors: httpErrors.errors}); -}; diff --git a/core/server/middleware/auth-strategies.js b/core/server/middleware/auth-strategies.js index 4814652491..877d85eeb2 100644 --- a/core/server/middleware/auth-strategies.js +++ b/core/server/middleware/auth-strategies.js @@ -13,10 +13,10 @@ strategies = { * Use of the client password strategy is implemented to support ember-simple-auth. */ clientPasswordStrategy: function clientPasswordStrategy(clientId, clientSecret, done) { - return models.Client.findOne({slug: clientId}) + return models.Client.findOne({slug: clientId}, {withRelated: ['trustedDomains']}) .then(function then(model) { if (model) { - var client = model.toJSON(); + var client = model.toJSON({include: ['trustedDomains']}); if (client.secret === clientSecret) { return done(null, client); } diff --git a/core/server/middleware/auth.js b/core/server/middleware/auth.js new file mode 100644 index 0000000000..28b7416db2 --- /dev/null +++ b/core/server/middleware/auth.js @@ -0,0 +1,137 @@ +var _ = require('lodash'), + passport = require('passport'), + url = require('url'), + errors = require('../errors'), + config = require('../config'), + oauthServer, + + auth; + +function cacheOauthServer(server) { + oauthServer = server; +} + +function isBearerAutorizationHeader(req) { + var parts, + scheme, + credentials; + + if (req.headers && req.headers.authorization) { + parts = req.headers.authorization.split(' '); + } else { + return false; + } + + if (parts.length === 2) { + scheme = parts[0]; + credentials = parts[1]; + if (/^Bearer$/i.test(scheme)) { + return true; + } + } + return false; +} + +function isValidOrigin(origin, client) { + if (origin && client && client.type === 'ua' && ( + _.some(client.trustedDomains, {trusted_domain: origin}) + || origin === url.parse(config.url).hostname + || origin === url.parse(config.urlSSL ? config.urlSSL : '').hostname + )) { + return true; + } else { + return false; + } +} + +auth = { + + // ### Authenticate Client Middleware + authenticateClient: function authenticateClient(req, res, next) { + // skip client authentication if bearer token is present + if (isBearerAutorizationHeader(req)) { + return next(); + } + + if (req.query && req.query.client_id) { + req.body.client_id = req.query.client_id; + } + + if (req.query && req.query.client_secret) { + req.body.client_secret = req.query.client_secret; + } + + if (!req.body.client_id || !req.body.client_secret) { + return errors.handleAPIError(new errors.UnauthorizedError('Access denied.'), req, res, next); + } + + return passport.authenticate(['oauth2-client-password'], {session: false, failWithError: false}, + function authenticate(err, client) { + var origin = null; + if (err) { + return next(err); // will generate a 500 error + } + + if (req.headers && req.headers.origin) { + origin = url.parse(req.headers.origin).hostname; + } + + if (!origin && client && client.type === 'ua') { + res.header('Access-Control-Allow-Origin', config.url); + req.client = client; + return next(null, client); + } + + if (isValidOrigin(origin, client)) { + res.header('Access-Control-Allow-Origin', req.headers.origin); + req.client = client; + return next(null, client); + } else { + return errors.handleAPIError(new errors.UnauthorizedError('Access denied.'), req, res, next); + } + } + )(req, res, next); + }, + + // ### Authenticate User Middleware + authenticateUser: function authenticateUser(req, res, next) { + return passport.authenticate('bearer', {session: false, failWithError: false}, + function authenticate(err, user, info) { + if (err) { + return next(err); // will generate a 500 error + } + + if (user) { + req.authInfo = info; + req.user = user; + return next(null, user, info); + } else if (isBearerAutorizationHeader(req)) { + return errors.handleAPIError(new errors.UnauthorizedError('Access denied.'), req, res, next); + } else if (req.client) { + return next(); + } + + return errors.handleAPIError(new errors.UnauthorizedError('Access denied.'), req, res, next); + } + )(req, res, next); + }, + + // Workaround for missing permissions + // TODO: rework when https://github.com/TryGhost/Ghost/issues/3911 is done + requiresAuthorizedUser: function requiresAuthorizedUser(req, res, next) { + if (req.user) { + return next(); + } else { + return errors.handleAPIError(new errors.NoPermissionError('Please Sign In'), req, res, next); + } + }, + + // ### Generate access token Middleware + // register the oauth2orize middleware for password and refresh token grants + generateAccessToken: function generateAccessToken(req, res, next) { + return oauthServer.token()(req, res, next); + } +}; + +module.exports = auth; +module.exports.cacheOauthServer = cacheOauthServer; diff --git a/core/server/middleware/authenticate.js b/core/server/middleware/authenticate.js deleted file mode 100644 index f084e08e6c..0000000000 --- a/core/server/middleware/authenticate.js +++ /dev/null @@ -1,48 +0,0 @@ -var passport = require('passport'), - apiErrorHandlers = require('./api-error-handlers'); - -// ### Authenticate Middleware -// authentication has to be done for /ghost/* routes with -// exceptions for signin, signout, signup, forgotten, reset only -// api and frontend use different authentication mechanisms atm -function authenticate(req, res, next) { - var path, - subPath; - - // SubPath is the url path starting after any default subdirectories - // it is stripped of anything after the two levels `/ghost/.*?/` as the reset link has an argument - path = req.path; - /*jslint regexp:true, unparam:true*/ - subPath = path.replace(/^(\/.*?\/.*?\/)(.*)?/, function replace(match, a) { - return a; - }); - - if (subPath.indexOf('/ghost/api/') === 0 - && (path.indexOf('/ghost/api/v0.1/authentication/') !== 0 - || (path.indexOf('/ghost/api/v0.1/authentication/setup/') === 0 && req.method === 'PUT'))) { - return passport.authenticate('bearer', {session: false, failWithError: true}, - function authenticate(err, user, info) { - if (err) { - return next(err); // will generate a 500 error - } - // Generate a JSON response reflecting authentication status - if (!user) { - var error = { - code: 401, - errorType: 'NoPermissionError', - message: 'Please Sign In' - }; - - return apiErrorHandlers.errorHandler(error, req, res, next); - } - // TODO: figure out, why user & authInfo is lost - req.authInfo = info; - req.user = user; - return next(null, user, info); - } - )(req, res, next); - } - next(); -} - -module.exports = authenticate; diff --git a/core/server/middleware/client-auth.js b/core/server/middleware/client-auth.js deleted file mode 100644 index 2ff7303a80..0000000000 --- a/core/server/middleware/client-auth.js +++ /dev/null @@ -1,25 +0,0 @@ -var passport = require('passport'), - oauthServer, - - clientAuth; - -function cacheOauthServer(server) { - oauthServer = server; -} - -clientAuth = { - // ### Authenticate Client Middleware - // authenticate client that is asking for an access token - authenticateClient: function authenticateClient(req, res, next) { - return passport.authenticate(['oauth2-client-password'], {session: false})(req, res, next); - }, - - // ### Generate access token Middleware - // register the oauth2orize middleware for password and refresh token grants - generateAccessToken: function generateAccessToken(req, res, next) { - return oauthServer.token()(req, res, next); - } -}; - -module.exports = clientAuth; -module.exports.cacheOauthServer = cacheOauthServer; diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index a076d91407..41aa34c9f8 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -12,11 +12,9 @@ var bodyParser = require('body-parser'), utils = require('../utils'), sitemapHandler = require('../data/xml/sitemap/handler'), - apiErrorHandlers = require('./api-error-handlers'), - authenticate = require('./authenticate'), authStrategies = require('./auth-strategies'), busboy = require('./ghost-busboy'), - clientAuth = require('./client-auth'), + auth = require('./auth'), cacheControl = require('./cache-control'), checkSSL = require('./check-ssl'), decideIsAdmin = require('./decide-is-admin'), @@ -41,10 +39,12 @@ middleware = { spamPrevention: spamPrevention, privateBlogging: privateBlogging, api: { - cacheOauthServer: clientAuth.cacheOauthServer, - authenticateClient: clientAuth.authenticateClient, - generateAccessToken: clientAuth.generateAccessToken, - errorHandler: apiErrorHandlers.errorHandler + cacheOauthServer: auth.cacheOauthServer, + authenticateClient: auth.authenticateClient, + authenticateUser: auth.authenticateUser, + requiresAuthorizedUser: auth.requiresAuthorizedUser, + generateAccessToken: auth.generateAccessToken, + errorHandler: errors.handleAPIError } }; @@ -135,9 +135,6 @@ setupMiddleware = function setupMiddleware(blogApp, adminApp) { // API shouldn't be cached blogApp.use(routes.apiBaseUri, cacheControl('private')); - // enable authentication - blogApp.use(authenticate); - // local data blogApp.use(themeHandler.ghostLocals); diff --git a/core/server/middleware/oauth.js b/core/server/middleware/oauth.js index 93b03702ce..48342c5357 100644 --- a/core/server/middleware/oauth.js +++ b/core/server/middleware/oauth.js @@ -18,7 +18,7 @@ oauth = { // `client`, which is exchanging the user's name and password from the // authorization request for verification. If these values are validated, the // application issues an access token on behalf of the user who authorized the code. - oauthServer.exchange(oauth2orize.exchange.password(function exchange(client, username, password, scope, done) { + oauthServer.exchange(oauth2orize.exchange.password({userProperty: 'client'}, function exchange(client, username, password, scope, done) { // Validate the client models.Client.forge({slug: client.slug}) .fetch() diff --git a/core/server/models/client.js b/core/server/models/client.js index e35e28378f..0f614125d9 100644 --- a/core/server/models/client.js +++ b/core/server/models/client.js @@ -8,6 +8,27 @@ Client = ghostBookshelf.Model.extend({ trustedDomains: function trustedDomains() { return this.hasMany('ClientTrustedDomain', 'client_id'); } +}, { + /** + * Returns an array of keys permitted in a method's `options` hash, depending on the current method. + * @param {String} methodName The name of the method to check valid options for. + * @return {Array} Keys allowed in the `options` hash of the model's method. + */ + permittedOptions: function permittedOptions(methodName) { + var options = ghostBookshelf.Model.permittedOptions(), + + // whitelists for the `options` hash argument on methods, by method name. + // these are the only options that can be passed to Bookshelf / Knex. + validOptions = { + findOne: ['withRelated'] + }; + + if (validOptions[methodName]) { + options = options.concat(validOptions[methodName]); + } + + return options; + } }); Clients = ghostBookshelf.Collection.extend({ diff --git a/core/server/models/user.js b/core/server/models/user.js index 9ef496a6f2..34a4e10ab0 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -456,17 +456,17 @@ User = ghostBookshelf.Model.extend({ if (action === 'edit') { // Owner can only be editted by owner - if (userModel.hasRole('Owner')) { + if (loadedPermissions.user && userModel.hasRole('Owner')) { hasUserPermission = _.any(loadedPermissions.user.roles, {name: 'Owner'}); } // Users with the role 'Editor' and 'Author' have complex permissions when the action === 'edit' // We now have all the info we need to construct the permissions - if (_.any(loadedPermissions.user.roles, {name: 'Author'})) { + if (loadedPermissions.user && _.any(loadedPermissions.user.roles, {name: 'Author'})) { // If this is the same user that requests the operation allow it. hasUserPermission = hasUserPermission || context.user === userModel.get('id'); } - if (_.any(loadedPermissions.user.roles, {name: 'Editor'})) { + if (loadedPermissions.user && _.any(loadedPermissions.user.roles, {name: 'Editor'})) { // If this is the same user that requests the operation allow it. hasUserPermission = context.user === userModel.get('id'); @@ -477,12 +477,12 @@ User = ghostBookshelf.Model.extend({ if (action === 'destroy') { // Owner cannot be deleted EVER - if (userModel.hasRole('Owner')) { + if (loadedPermissions.user && userModel.hasRole('Owner')) { return Promise.reject(new errors.NoPermissionError('You do not have permission to perform this action')); } // Users with the role 'Editor' have complex permissions when the action === 'destroy' - if (_.any(loadedPermissions.user.roles, {name: 'Editor'})) { + if (loadedPermissions.user && _.any(loadedPermissions.user.roles, {name: 'Editor'})) { // If this is the same user that requests the operation allow it. hasUserPermission = context.user === userModel.get('id'); diff --git a/core/server/permissions/index.js b/core/server/permissions/index.js index e5eca61c16..ae0557bd4e 100644 --- a/core/server/permissions/index.js +++ b/core/server/permissions/index.js @@ -169,7 +169,6 @@ CanThisResult.prototype.buildObjectTypeHandlers = function (objTypes, actType, c // TODO: String vs Int comparison possibility here? return modelId === permObjId; }; - // Check user permissions for matching action, object and id. if (loadedPermissions.user && _.any(loadedPermissions.user.roles, {name: 'Owner'})) { hasUserPermission = true; diff --git a/core/server/routes/api.js b/core/server/routes/api.js index 5c57da6137..b8b0baa993 100644 --- a/core/server/routes/api.js +++ b/core/server/routes/api.js @@ -4,72 +4,86 @@ var express = require('express'), apiRoutes; apiRoutes = function apiRoutes(middleware) { - var router = express.Router(); + var router = express.Router(), + // Authentication for public endpoints + authenticatePublic = [ + middleware.api.authenticateClient, + middleware.api.authenticateUser + ], + // Require user for private endpoints + authenticatePrivate = [ + middleware.api.authenticateClient, + middleware.api.authenticateUser, + middleware.api.requiresAuthorizedUser + ]; + // alias delete with del router.del = router.delete; // ## Configuration - router.get('/configuration', api.http(api.configuration.browse)); - router.get('/configuration/:key', api.http(api.configuration.read)); + router.get('/configuration', authenticatePrivate, api.http(api.configuration.browse)); + router.get('/configuration/:key', authenticatePrivate, api.http(api.configuration.read)); // ## Posts - router.get('/posts', api.http(api.posts.browse)); - router.post('/posts', api.http(api.posts.add)); - router.get('/posts/:id', api.http(api.posts.read)); - router.get('/posts/slug/:slug', api.http(api.posts.read)); - router.put('/posts/:id', api.http(api.posts.edit)); - router.del('/posts/:id', api.http(api.posts.destroy)); + router.get('/posts', authenticatePublic, api.http(api.posts.browse)); + + router.post('/posts', authenticatePrivate, api.http(api.posts.add)); + router.get('/posts/:id', authenticatePublic, api.http(api.posts.read)); + router.get('/posts/slug/:slug', authenticatePublic, api.http(api.posts.read)); + router.put('/posts/:id', authenticatePrivate, api.http(api.posts.edit)); + router.del('/posts/:id', authenticatePrivate, api.http(api.posts.destroy)); // ## Settings - router.get('/settings', api.http(api.settings.browse)); - router.get('/settings/:key', api.http(api.settings.read)); - router.put('/settings', api.http(api.settings.edit)); + router.get('/settings', authenticatePrivate, api.http(api.settings.browse)); + router.get('/settings/:key', authenticatePrivate, api.http(api.settings.read)); + router.put('/settings', authenticatePrivate, api.http(api.settings.edit)); // ## Users - router.get('/users', api.http(api.users.browse)); - router.get('/users/:id', api.http(api.users.read)); - router.get('/users/slug/:slug', api.http(api.users.read)); - router.get('/users/email/:email', api.http(api.users.read)); - router.put('/users/password', api.http(api.users.changePassword)); - router.put('/users/owner', api.http(api.users.transferOwnership)); - router.put('/users/:id', api.http(api.users.edit)); - router.post('/users', api.http(api.users.add)); - router.del('/users/:id', api.http(api.users.destroy)); + router.get('/users', authenticatePublic, api.http(api.users.browse)); + + router.get('/users/:id', authenticatePublic, api.http(api.users.read)); + router.get('/users/slug/:slug', authenticatePublic, api.http(api.users.read)); + router.get('/users/email/:email', authenticatePublic, api.http(api.users.read)); + router.put('/users/password', authenticatePrivate, api.http(api.users.changePassword)); + router.put('/users/owner', authenticatePrivate, api.http(api.users.transferOwnership)); + router.put('/users/:id', authenticatePrivate, api.http(api.users.edit)); + router.post('/users', authenticatePrivate, api.http(api.users.add)); + router.del('/users/:id', authenticatePrivate, api.http(api.users.destroy)); // ## Tags - router.get('/tags', api.http(api.tags.browse)); - router.get('/tags/:id', api.http(api.tags.read)); - router.get('/tags/slug/:slug', api.http(api.tags.read)); - router.post('/tags', api.http(api.tags.add)); - router.put('/tags/:id', api.http(api.tags.edit)); - router.del('/tags/:id', api.http(api.tags.destroy)); + router.get('/tags', authenticatePublic, api.http(api.tags.browse)); + router.get('/tags/:id', authenticatePublic, api.http(api.tags.read)); + router.get('/tags/slug/:slug', authenticatePublic, api.http(api.tags.read)); + router.post('/tags', authenticatePrivate, api.http(api.tags.add)); + router.put('/tags/:id', authenticatePrivate, api.http(api.tags.edit)); + router.del('/tags/:id', authenticatePrivate, api.http(api.tags.destroy)); // ## Roles - router.get('/roles/', api.http(api.roles.browse)); + router.get('/roles/', authenticatePrivate, api.http(api.roles.browse)); // ## Clients router.get('/clients/slug/:slug', api.http(api.clients.read)); // ## Slugs - router.get('/slugs/:type/:name', api.http(api.slugs.generate)); + router.get('/slugs/:type/:name', authenticatePrivate, api.http(api.slugs.generate)); // ## Themes - router.get('/themes', api.http(api.themes.browse)); - router.put('/themes/:name', api.http(api.themes.edit)); + router.get('/themes', authenticatePrivate, api.http(api.themes.browse)); + router.put('/themes/:name', authenticatePrivate, api.http(api.themes.edit)); // ## Notifications - router.get('/notifications', api.http(api.notifications.browse)); - router.post('/notifications', api.http(api.notifications.add)); - router.del('/notifications/:id', api.http(api.notifications.destroy)); + router.get('/notifications', authenticatePrivate, api.http(api.notifications.browse)); + router.post('/notifications', authenticatePrivate, api.http(api.notifications.add)); + router.del('/notifications/:id', authenticatePrivate, api.http(api.notifications.destroy)); // ## DB - router.get('/db', api.http(api.db.exportContent)); - router.post('/db', middleware.busboy, api.http(api.db.importContent)); - router.del('/db', api.http(api.db.deleteAllContent)); + router.get('/db', authenticatePrivate, api.http(api.db.exportContent)); + router.post('/db', authenticatePrivate, middleware.busboy, api.http(api.db.importContent)); + router.del('/db', authenticatePrivate, api.http(api.db.deleteAllContent)); // ## Mail - router.post('/mail', api.http(api.mail.send)); - router.post('/mail/test', api.http(api.mail.sendTest)); + router.post('/mail', authenticatePrivate, api.http(api.mail.send)); + router.post('/mail/test', authenticatePrivate, api.http(api.mail.sendTest)); // ## Authentication router.post('/authentication/passwordreset', @@ -87,10 +101,10 @@ apiRoutes = function apiRoutes(middleware) { middleware.api.authenticateClient, middleware.api.generateAccessToken ); - router.post('/authentication/revoke', api.http(api.authentication.revoke)); + router.post('/authentication/revoke', authenticatePrivate, api.http(api.authentication.revoke)); // ## Uploads - router.post('/uploads', middleware.busboy, api.http(api.uploads.add)); + router.post('/uploads', authenticatePrivate, middleware.busboy, api.http(api.uploads.add)); // API Router middleware router.use(middleware.api.errorHandler); diff --git a/core/test/functional/routes/api/authentication_spec.js b/core/test/functional/routes/api/authentication_spec.js index bc21d9f8d8..05d345dea7 100644 --- a/core/test/functional/routes/api/authentication_spec.js +++ b/core/test/functional/routes/api/authentication_spec.js @@ -5,6 +5,7 @@ var supertest = require('supertest'), testUtils = require('../../../utils'), user = testUtils.DataGenerator.forModel.users[0], ghost = require('../../../../../core'), + config = require('../../../../../core/server/config'), request; describe('Authentication API', function () { @@ -31,8 +32,14 @@ describe('Authentication API', function () { it('can authenticate', function (done) { request.post(testUtils.API.getApiQuery('authentication/token')) - .send({grant_type: 'password', username: user.email, password: user.password, client_id: 'ghost-admin', client_secret: 'not_available'}) - .expect('Content-Type', /json/) + .set('Origin', config.url) + .send({ + grant_type: 'password', + username: user.email, + password: user.password, + client_id: 'ghost-admin', + client_secret: 'not_available' + }).expect('Content-Type', /json/) // TODO: make it possible to override oauth2orize's header so that this is consistent .expect('Cache-Control', 'no-store') .expect(200) @@ -52,8 +59,14 @@ describe('Authentication API', function () { it('can\'t authenticate unknown user', function (done) { request.post(testUtils.API.getApiQuery('authentication/token')) - .send({grant_type: 'password', username: 'invalid@email.com', password: user.password, client_id: 'ghost-admin', client_secret: 'not_available'}) - .expect('Content-Type', /json/) + .set('Origin', config.url) + .send({ + grant_type: 'password', + username: 'invalid@email.com', + password: user.password, + client_id: 'ghost-admin', + client_secret: 'not_available' + }).expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(404) .end(function (err, res) { @@ -69,8 +82,14 @@ describe('Authentication API', function () { it('can\'t authenticate invalid password user', function (done) { request.post(testUtils.API.getApiQuery('authentication/token')) - .send({grant_type: 'password', username: user.email, password: 'invalid', client_id: 'ghost-admin', client_secret: 'not_available'}) - .expect('Content-Type', /json/) + .set('Origin', config.url) + .send({ + grant_type: 'password', + username: user.email, + password: 'invalid', + client_id: 'ghost-admin', + client_secret: 'not_available' + }).expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(401) .end(function (err, res) { @@ -86,8 +105,14 @@ describe('Authentication API', function () { it('can request new access token', function (done) { request.post(testUtils.API.getApiQuery('authentication/token')) - .send({grant_type: 'password', username: user.email, password: user.password, client_id: 'ghost-admin', client_secret: 'not_available'}) - .expect('Content-Type', /json/) + .set('Origin', config.url) + .send({ + grant_type: 'password', + username: user.email, + password: user.password, + client_id: 'ghost-admin', + client_secret: 'not_available' + }).expect('Content-Type', /json/) // TODO: make it possible to override oauth2orize's header so that this is consistent .expect('Cache-Control', 'no-store') .expect(200) @@ -97,10 +122,15 @@ describe('Authentication API', function () { } var refreshToken = res.body.refresh_token; request.post(testUtils.API.getApiQuery('authentication/token')) - .send({grant_type: 'refresh_token', refresh_token: refreshToken, client_id: 'ghost-admin', client_secret: 'not_available'}) - .expect('Content-Type', /json/) + .set('Origin', config.url) + .send({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: 'ghost-admin', + client_secret: 'not_available' + }).expect('Content-Type', /json/) // TODO: make it possible to override oauth2orize's header so that this is consistent - .expect('Cache-Control', 'no-store') + .expect('Cache-Control', 'no-store') .expect(200) .end(function (err, res) { if (err) { @@ -116,8 +146,13 @@ describe('Authentication API', function () { it('can\'t request new access token with invalid refresh token', function (done) { request.post(testUtils.API.getApiQuery('authentication/token')) - .send({grant_type: 'refresh_token', refresh_token: 'invalid', client_id: 'ghost-admin', client_secret: 'not_available'}) - .expect('Content-Type', /json/) + .set('Origin', config.url) + .send({ + grant_type: 'refresh_token', + refresh_token: 'invalid', + client_id: 'ghost-admin', + client_secret: 'not_available' + }).expect('Content-Type', /json/) .expect('Cache-Control', testUtils.cacheRules.private) .expect(403) .end(function (err, res) { diff --git a/core/test/functional/routes/api/public_api_spec.js b/core/test/functional/routes/api/public_api_spec.js new file mode 100644 index 0000000000..68a0e4866a --- /dev/null +++ b/core/test/functional/routes/api/public_api_spec.js @@ -0,0 +1,153 @@ +/*global describe, it, before, after */ +/*jshint expr:true*/ +var testUtils = require('../../../utils'), + should = require('should'), + supertest = require('supertest'), + _ = require('lodash'), + + ghost = require('../../../../../core'), + + request; + +describe('Public API', function () { + before(function (done) { + // starting ghost automatically populates the db + // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves + ghost().then(function (ghostServer) { + request = supertest.agent(ghostServer.rootApp); + done(); + }).catch(done); + }); + + after(function (done) { + testUtils.clearData().then(function () { + done(); + }).catch(done); + }); + + it('browse posts', function (done) { + request.get(testUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available')) + .set('Origin', testUtils.API.getURL()) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + jsonResponse.posts.should.exist; + testUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); + _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + done(); + }); + }); + + it('browse tags', function (done) { + request.get(testUtils.API.getApiQuery('tags/?client_id=ghost-admin&client_secret=not_available')) + .set('Origin', testUtils.API.getURL()) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + jsonResponse.tags.should.exist; + testUtils.API.checkResponse(jsonResponse, 'tags'); + jsonResponse.tags.should.have.length(1); + testUtils.API.checkResponse(jsonResponse.tags[0], 'tag'); + testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + done(); + }); + }); + + it('denies access with invalid client_secret', function (done) { + request.get(testUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=invalid_secret')) + .set('Origin', testUtils.API.getURL()) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(401) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.errors.should.exist; + testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']); + done(); + }); + }); + + it('denies access with invalid client_id', function (done) { + request.get(testUtils.API.getApiQuery('posts/?client_id=invalid-id&client_secret=not_available')) + .set('Origin', testUtils.API.getURL()) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(401) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.errors.should.exist; + testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']); + done(); + }); + }); + + it('denies access from invalid origin', function (done) { + request.get(testUtils.API.getApiQuery('posts/?client_id=ghost-admin&client_secret=not_available')) + .set('Origin', 'http://invalid-origin') + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(401) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.errors.should.exist; + testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']); + done(); + }); + }); + + it('denies access to settings endpoint', function (done) { + request.get(testUtils.API.getApiQuery('settings/?client_id=ghost-admin&client_secret=not_available')) + .set('Origin', testUtils.API.getURL()) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(403) + .end(function (err, res) { + if (err) { + return done(err); + } + + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; + jsonResponse.should.exist; + jsonResponse.errors.should.exist; + testUtils.API.checkResponseValue(jsonResponse.errors[0], ['message', 'errorType']); + done(); + }); + }); +}); diff --git a/core/test/unit/error_handling_spec.js b/core/test/unit/error_handling_spec.js index 2912a1f70c..e0dee3b36a 100644 --- a/core/test/unit/error_handling_spec.js +++ b/core/test/unit/error_handling_spec.js @@ -276,6 +276,42 @@ describe('Error handling', function () { }); }); + describe('API Error Handlers', function () { + var sandbox, req, res, next; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + req = {}; + res = {}; + res.json = sandbox.spy(); + res.status = sandbox.stub().returns(res); + next = sandbox.spy(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('handleAPIError: sends a JSON error response', function () { + errors.logError = sandbox.spy(errors, 'logError'); + errors.formatHttpErrors = sandbox.spy(errors, 'formatHttpErrors'); + + var msg = 'Something got lost', + err = new errors.NotFoundError(msg); + + errors.handleAPIError(err, req, res, next); + + next.called.should.be.false; + errors.logError.calledOnce.should.be.true; + errors.formatHttpErrors.calledOnce.should.be.true; + + res.status.calledWith(404).should.be.true; + res.json.calledOnce.should.be.true; + res.json.firstCall.args[0].errors[0].message.should.eql(msg); + res.json.firstCall.args[0].errors[0].errorType.should.eql('NotFoundError'); + }); + }); + describe('Rendering', function () { var sandbox, originalConfig; diff --git a/core/test/unit/middleware/api-error-handlers_spec.js b/core/test/unit/middleware/api-error-handlers_spec.js deleted file mode 100644 index 6f7b2f8c6a..0000000000 --- a/core/test/unit/middleware/api-error-handlers_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -/*globals describe, beforeEach, afterEach, it*/ -/*jshint expr:true*/ -var should = require('should'), - sinon = require('sinon'), - - middleware = require('../../../server/middleware').middleware, - errors = require('../../../server/errors'); - -// To stop jshint complaining -should.equal(true, true); - -describe('Middleware: API Error Handlers', function () { - var sandbox, req, res, next; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - req = {}; - res = {}; - res.json = sandbox.spy(); - res.status = sandbox.stub().returns(res); - next = sandbox.spy(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - describe('errorHandler', function () { - it('sends a JSON error response', function () { - errors.logError = sandbox.spy(errors, 'logError'); - errors.formatHttpErrors = sandbox.spy(errors, 'formatHttpErrors'); - - var msg = 'Something got lost', - err = new errors.NotFoundError(msg); - - middleware.api.errorHandler(err, req, res, next); - - next.called.should.be.false; - errors.logError.calledOnce.should.be.true; - errors.formatHttpErrors.calledOnce.should.be.true; - - res.status.calledWith(404).should.be.true; - res.json.calledOnce.should.be.true; - res.json.firstCall.args[0].errors[0].message.should.eql(msg); - res.json.firstCall.args[0].errors[0].errorType.should.eql('NotFoundError'); - }); - }); -}); diff --git a/core/test/unit/middleware/authentication_spec.js b/core/test/unit/middleware/authentication_spec.js index 6a041d484c..136fbcb7f6 100644 --- a/core/test/unit/middleware/authentication_spec.js +++ b/core/test/unit/middleware/authentication_spec.js @@ -1,13 +1,24 @@ /*globals describe, it, beforeEach, afterEach */ /*jshint expr:true*/ -var sinon = require('sinon'), - should = require('should'), - passport = require('passport'), - authenticate = require('../../../server/middleware/authenticate'), - BearerStrategy = require('passport-http-bearer').Strategy, - user = {id: 1}, - info = {scope: '*'}, - token = 'test_token'; +var _ = require('lodash'), + sinon = require('sinon'), + should = require('should'), + passport = require('passport'), + rewire = require('rewire'), + config = require('../../../server/config'), + defaultConfig = rewire('../../../../config.example')[process.env.NODE_ENV], + auth = rewire('../../../server/middleware/auth'), + BearerStrategy = require('passport-http-bearer').Strategy, + ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy, + user = {id: 1}, + info = {scope: '*'}, + token = 'test_token', + testClient = 'test_client', + testSecret = 'not_available', + client = { + id: 2, + type: 'ua' + }; should.equal(true, true); @@ -31,7 +42,50 @@ function registerUnsuccessfulBearerStrategy() { )); } -describe('authenticate', function () { +function registerFaultyBearerStrategy() { + // register fake BearerStrategy which always authenticates + passport.use(new BearerStrategy( + function strategy(accessToken, done) { + accessToken.should.eql(token); + return done('error'); + } + )); +} + +function registerSuccessfulClientPasswordStrategy() { + // register fake BearerStrategy which always authenticates + passport.use(new ClientPasswordStrategy( + function strategy(clientId, clientSecret, done) { + clientId.should.eql(testClient); + clientSecret.should.eql('not_available'); + return done(null, client); + } + )); +} + +function registerUnsuccessfulClientPasswordStrategy() { + // register fake BearerStrategy which always authenticates + passport.use(new ClientPasswordStrategy( + function strategy(clientId, clientSecret, done) { + clientId.should.eql(testClient); + clientSecret.should.eql('not_available'); + return done(null, false); + } + )); +} + +function registerFaultyClientPasswordStrategy() { + // register fake BearerStrategy which always authenticates + passport.use(new ClientPasswordStrategy( + function strategy(clientId, clientSecret, done) { + clientId.should.eql(testClient); + clientSecret.should.eql('not_available'); + return done('error'); + } + )); +} + +describe('Auth', function () { var res, req, next, sandbox; beforeEach(function () { @@ -45,82 +99,21 @@ describe('authenticate', function () { sandbox.restore(); }); - it('should skip authentication if not hitting /ghost', function (done) { - req.path = '/tag/foo/'; - req.method = 'GET'; - - registerSuccessfulBearerStrategy(); - authenticate(req, res, next); + it('should require authorized user (user exists)', function (done) { + req.user = {id: 1}; + auth.requiresAuthorizedUser(req, res, next); next.called.should.be.true; next.calledWith().should.be.true; done(); }); - it('should skip authentication if hitting /ghost/api/v0.1/authenticaton/', function (done) { - req.path = '/ghost/api/v0.1/authentication/'; - req.method = 'GET'; - - registerSuccessfulBearerStrategy(); - authenticate(req, res, next); - - next.called.should.be.true; - next.calledWith().should.be.true; - done(); - }); - - it('should skip authentication if hitting GET /ghost/api/v0.1/authenticaton/setup/', function (done) { - req.path = '/ghost/api/v0.1/authentication/setup/'; - req.method = 'GET'; - - registerSuccessfulBearerStrategy(); - authenticate(req, res, next); - - next.called.should.be.true; - next.calledWith().should.be.true; - done(); - }); - - it('should authentication if hitting PUT /ghost/api/v0.1/authenticaton/setup/', function (done) { - req.path = '/ghost/api/v0.1/authentication/setup/'; - req.method = 'PUT'; - req.headers = {}; - req.headers.authorization = 'Bearer ' + token; - - registerSuccessfulBearerStrategy(); - authenticate(req, res, next); - - next.called.should.be.true; - next.calledWith(null, user, info).should.be.true; - done(); - }); - - it('should authenticate if hitting /ghost/api/ endpoint', function (done) { - req.path = '/ghost/api/v0.1/test/'; - req.method = 'PUT'; - req.headers = {}; - req.headers.authorization = 'Bearer ' + token; - - registerSuccessfulBearerStrategy(); - authenticate(req, res, next); - - next.called.should.be.true; - next.calledWith(null, user, info).should.be.true; - done(); - }); - - it('shouldn\'t authenticate if hitting /ghost/ auth endpoint with invalid credentials', function (done) { + it('should require authorized user (user is missing)', function (done) { + req.user = false; res.status = {}; - req.path = '/ghost/api/v0.1/test/'; - req.method = 'PUT'; - req.headers = {}; - req.headers.authorization = 'Bearer ' + token; - registerUnsuccessfulBearerStrategy(); - - // stub res.status for error handling sandbox.stub(res, 'status', function (statusCode) { - statusCode.should.eql(401); + statusCode.should.eql(403); return { json: function (err) { err.errors[0].errorType.should.eql('NoPermissionError'); @@ -128,8 +121,349 @@ describe('authenticate', function () { }; }); - authenticate(req, res, next); + auth.requiresAuthorizedUser(req, res, next); next.called.should.be.false; done(); }); + + describe('User Authentication', function () { + beforeEach(function () { + var newConfig = _.extend({}, config, defaultConfig); + + auth.__get__('config', newConfig); + config.set(newConfig); + }); + + it('should authenticate user', function (done) { + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + + registerSuccessfulBearerStrategy(); + auth.authenticateUser(req, res, next); + + next.called.should.be.true; + next.calledWith(null, user, info).should.be.true; + done(); + }); + + it('shouldn\'t pass with client, no bearer token', function (done) { + req.headers = {}; + req.client = {id: 1}; + res.status = {}; + + auth.authenticateUser(req, res, next); + + next.called.should.be.true; + next.calledWith().should.be.true; + done(); + }); + + it('shouldn\'t authenticate user', function (done) { + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + registerUnsuccessfulBearerStrategy(); + auth.authenticateUser(req, res, next); + + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate without bearer token', function (done) { + req.headers = {}; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + registerUnsuccessfulBearerStrategy(); + auth.authenticateUser(req, res, next); + + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate with bearer token and client', function (done) { + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + req.client = {id: 1}; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + registerUnsuccessfulBearerStrategy(); + auth.authenticateUser(req, res, next); + + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate when error', function (done) { + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + + registerFaultyBearerStrategy(); + auth.authenticateUser(req, res, next); + + next.called.should.be.true; + next.calledWith('error').should.be.true; + done(); + }); + }); + + describe('Client Authentication', function () { + it('shouldn\'t require authorized client with bearer token', function (done) { + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + + auth.authenticateClient(req, res, next); + next.called.should.be.true; + next.calledWith().should.be.true; + done(); + }); + + it('shouldn\'t authenticate client with broken bearer token', function (done) { + req.body = {}; + req.headers = {}; + req.headers.authorization = 'Bearer'; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + auth.authenticateClient(req, res, next); + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate client without client_id/client_secret', function (done) { + req.body = {}; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + auth.authenticateClient(req, res, next); + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate client without client_id', function (done) { + req.body = {}; + req.body.client_secret = testSecret; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + auth.authenticateClient(req, res, next); + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate client without client_secret', function (done) { + req.body = {}; + req.body.client_id = testClient; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + auth.authenticateClient(req, res, next); + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate client', function (done) { + req.body = {}; + req.body.client_id = testClient; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + registerUnsuccessfulClientPasswordStrategy(); + auth.authenticateClient(req, res, next); + next.called.should.be.false; + done(); + }); + + it('shouldn\'t authenticate client with invalid origin', function (done) { + req.body = {}; + req.body.client_id = testClient; + req.body.client_secret = testSecret; + req.headers = {}; + req.headers.origin = 'http://invalid.origin.com'; + res.status = {}; + + sandbox.stub(res, 'status', function (statusCode) { + statusCode.should.eql(401); + return { + json: function (err) { + err.errors[0].errorType.should.eql('UnauthorizedError'); + } + }; + }); + + registerSuccessfulClientPasswordStrategy(); + auth.authenticateClient(req, res, next); + next.called.should.be.false; + done(); + }); + + it('should authenticate client', function (done) { + req.body = {}; + req.body.client_id = testClient; + req.body.client_secret = testSecret; + req.headers = {}; + req.headers.origin = config.url; + + res.header = {}; + + sandbox.stub(res, 'header', function (key, value) { + key.should.equal('Access-Control-Allow-Origin'); + value.should.equal(config.url); + }); + + registerSuccessfulClientPasswordStrategy(); + auth.authenticateClient(req, res, next); + + next.called.should.be.true; + next.calledWith(null, client).should.be.true; + done(); + }); + + it('should authenticate client without origin', function (done) { + req.body = {}; + req.body.client_id = testClient; + req.body.client_secret = testSecret; + + res.header = {}; + + sandbox.stub(res, 'header', function (key, value) { + key.should.equal('Access-Control-Allow-Origin'); + value.should.equal(config.url); + }); + + registerSuccessfulClientPasswordStrategy(); + auth.authenticateClient(req, res, next); + + next.called.should.be.true; + next.calledWith(null, client).should.be.true; + done(); + }); + + it('should authenticate client with id in query', function (done) { + req.body = {}; + req.query = {}; + req.query.client_id = testClient; + req.query.client_secret = testSecret; + req.headers = {}; + req.headers.origin = config.url; + + res.header = {}; + + sandbox.stub(res, 'header', function (key, value) { + key.should.equal('Access-Control-Allow-Origin'); + value.should.equal(config.url); + }); + + registerSuccessfulClientPasswordStrategy(); + auth.authenticateClient(req, res, next); + + next.called.should.be.true; + next.calledWith(null, client).should.be.true; + done(); + }); + + it('should authenticate client with id + secret in query', function (done) { + req.body = {}; + req.query = {}; + req.query.client_id = testClient; + req.query.client_secret = testSecret; + req.headers = {}; + req.headers.origin = config.url; + + res.header = {}; + + sandbox.stub(res, 'header', function (key, value) { + key.should.equal('Access-Control-Allow-Origin'); + value.should.equal(config.url); + }); + + registerSuccessfulClientPasswordStrategy(); + auth.authenticateClient(req, res, next); + + next.called.should.be.true; + next.calledWith(null, client).should.be.true; + done(); + }); + + it('shouldn\'t authenticate when error', function (done) { + req.body = {}; + req.body.client_id = testClient; + req.body.client_secret = testSecret; + res.status = {}; + + registerFaultyClientPasswordStrategy(); + auth.authenticateClient(req, res, next); + + next.called.should.be.true; + next.calledWith('error').should.be.true; + done(); + }); + }); }); diff --git a/core/test/utils/api.js b/core/test/utils/api.js index 8a68c6908b..ab99efa29d 100644 --- a/core/test/utils/api.js +++ b/core/test/utils/api.js @@ -9,6 +9,7 @@ var _ = require('lodash'), expectedProperties = { configuration: ['key', 'value'], posts: ['posts', 'meta'], + tags: ['tags', 'meta'], users: ['users', 'meta'], roles: ['roles'], pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'], @@ -75,11 +76,16 @@ function isISO8601(date) { return moment(date).parsingFlags().iso; } +function getURL() { + return schema + host; +} + module.exports = { getApiURL: getApiURL, getApiQuery: getApiQuery, getSigninURL: getSigninURL, getAdminURL: getAdminURL, + getURL: getURL, checkResponse: checkResponse, checkResponseValue: checkResponseValue, isISO8601: isISO8601 diff --git a/core/test/utils/fixtures/data-generator.js b/core/test/utils/fixtures/data-generator.js index 6e2774bdec..e6d3226f98 100644 --- a/core/test/utils/fixtures/data-generator.js +++ b/core/test/utils/fixtures/data-generator.js @@ -377,7 +377,7 @@ DataGenerator.forKnex = (function () { ]; clients = [ - createBasic({name: 'Ghost Admin', slug: 'ghost-admin', secret: 'not_available'}) + createBasic({name: 'Ghost Admin', slug: 'ghost-admin', secret: 'not_available', type: 'ua', status: 'enabled'}) ]; roles_users = [ diff --git a/core/test/utils/index.js b/core/test/utils/index.js index 03a1ef6b33..bf9eea6010 100644 --- a/core/test/utils/index.js +++ b/core/test/utils/index.js @@ -525,8 +525,14 @@ login = function login(request) { return new Promise(function (resolve, reject) { request.post('/ghost/api/v0.1/authentication/token/') - .send({grant_type: 'password', username: user.email, password: user.password, client_id: 'ghost-admin', client_secret: 'not_available'}) - .end(function (err, res) { + .set('Origin', config.url) + .send({ + grant_type: 'password', + username: user.email, + password: user.password, + client_id: 'ghost-admin', + client_secret: 'not_available' + }).end(function (err, res) { if (err) { return reject(err); }