Skip to content

Commit

Permalink
Public API
Browse files Browse the repository at this point in the history
refs TryGhost#4180
closes TryGhost#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
  • Loading branch information
sebgie committed Oct 22, 2015
1 parent d666fba commit f48dfb0
Show file tree
Hide file tree
Showing 22 changed files with 903 additions and 286 deletions.
14 changes: 7 additions & 7 deletions core/server/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
}

Expand Down Expand Up @@ -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');
});
}

Expand Down Expand Up @@ -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.');
});
}

Expand Down Expand Up @@ -407,7 +407,7 @@ users = {
return Promise.reject(new errors.InternalServerError(error));
});
}, function (error) {
return errors.handleAPIError(error);
return errors.formatAndRejectAPIError(error);
});
}

Expand Down Expand Up @@ -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');
});
}

Expand Down Expand Up @@ -494,7 +494,7 @@ users = {
}).then(function () {
return options;
}).catch(function (error) {
return errors.handleAPIError(error);
return errors.formatAndRejectAPIError(error);
});
}

Expand All @@ -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);
});
}
};
Expand Down
4 changes: 2 additions & 2 deletions core/server/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ utils = {
return permsPromise.then(function permissionGranted() {
return options;
}).catch(function handleError(error) {
return errors.handleAPIError(error);
return errors.formatAndRejectAPIError(error);
});
};
},
Expand Down Expand Up @@ -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);
});
};
},
Expand Down
11 changes: 10 additions & 1 deletion core/server/errors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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,
Expand Down Expand Up @@ -374,6 +382,7 @@ _.each([
'logErrorAndExit',
'logErrorWithRedirect',
'handleAPIError',
'formatAndRejectAPIError',
'formatHttpErrors',
'renderErrorPage',
'error404',
Expand Down
9 changes: 0 additions & 9 deletions core/server/middleware/api-error-handlers.js

This file was deleted.

4 changes: 2 additions & 2 deletions core/server/middleware/auth-strategies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
137 changes: 137 additions & 0 deletions core/server/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -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;
48 changes: 0 additions & 48 deletions core/server/middleware/authenticate.js

This file was deleted.

25 changes: 0 additions & 25 deletions core/server/middleware/client-auth.js

This file was deleted.

17 changes: 7 additions & 10 deletions core/server/middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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
}
};

Expand Down Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion core/server/middleware/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading

0 comments on commit f48dfb0

Please sign in to comment.