mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Public API
refs #4180 closes #4181 - added client and user authentication - added authenticatePublic/authenticatePrivate as workaround for missing permissions - added domain validation - added CORS header for valid clients - merged authenticate.js and client-auth.js into auth.js - removed middleware/api-error-handlers.js - removed authentication middleware - added and updated tests
This commit is contained in:
parent
d666fba855
commit
f48dfb09cf
22 changed files with 910 additions and 293 deletions
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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});
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
137
core/server/middleware/auth.js
Normal file
137
core/server/middleware/auth.js
Normal file
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
153
core/test/functional/routes/api/public_api_spec.js
Normal file
153
core/test/functional/routes/api/public_api_spec.js
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue