0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Merge pull request #6058 from sebgie/middleware-refactor

OAuth Middleware refactor
This commit is contained in:
Hannah Wolfe 2015-12-02 12:07:01 +08:00
commit 1ba9929c59
5 changed files with 362 additions and 95 deletions

View file

@ -1,18 +1,13 @@
var _ = require('lodash'),
passport = require('passport'),
url = require('url'),
var _ = require('lodash'),
passport = require('passport'),
url = require('url'),
os = require('os'),
errors = require('../errors'),
config = require('../config'),
labs = require('../utils/labs'),
oauthServer,
errors = require('../errors'),
config = require('../config'),
labs = require('../utils/labs'),
auth;
function cacheOauthServer(server) {
oauthServer = server;
}
function isBearerAutorizationHeader(req) {
var parts,
scheme,
@ -174,14 +169,7 @@ auth = {
}
}
});
},
// ### 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;

View file

@ -8,7 +8,6 @@ var bodyParser = require('body-parser'),
slashes = require('connect-slashes'),
storage = require('../storage'),
passport = require('passport'),
oauth2orize = require('oauth2orize'),
utils = require('../utils'),
sitemapHandler = require('../data/xml/sitemap/handler'),
@ -38,8 +37,8 @@ middleware = {
cacheControl: cacheControl,
spamPrevention: spamPrevention,
privateBlogging: privateBlogging,
oauth: oauth,
api: {
cacheOauthServer: auth.cacheOauthServer,
authenticateClient: auth.authenticateClient,
authenticateUser: auth.authenticateUser,
requiresAuthorizedUser: auth.requiresAuthorizedUser,
@ -51,16 +50,13 @@ middleware = {
setupMiddleware = function setupMiddleware(blogApp, adminApp) {
var logging = config.logging,
corePath = config.paths.corePath,
oauthServer = oauth2orize.createServer();
corePath = config.paths.corePath;
// silence JSHint without disabling unused check for the whole file
passport.use(new ClientPasswordStrategy(authStrategies.clientPasswordStrategy));
passport.use(new BearerStrategy(authStrategies.bearerStrategy));
// Cache express server instance
middleware.api.cacheOauthServer(oauthServer);
oauth.init(oauthServer, spamPrevention.resetCounter);
// Initialize OAuth middleware
oauth.init();
// Make sure 'req.secure' is valid for proxied requests
// (X-Forwarded-Proto header will be checked, if present)

View file

@ -1,13 +1,76 @@
var oauth2orize = require('oauth2orize'),
models = require('../models'),
utils = require('../utils'),
errors = require('../errors'),
var oauth2orize = require('oauth2orize'),
models = require('../models'),
utils = require('../utils'),
errors = require('../errors'),
spamPrevention = require('./spam-prevention'),
oauthServer,
oauth;
function exchangeRefreshToken(client, refreshToken, scope, done) {
models.Refreshtoken.findOne({token: refreshToken}).then(function then(model) {
if (!model) {
return done(new errors.NoPermissionError('Invalid refresh token.'), false);
} else {
var token = model.toJSON(),
accessToken = utils.uid(256),
accessExpires = Date.now() + utils.ONE_HOUR_MS,
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
if (token.expires > Date.now()) {
models.Accesstoken.add({
token: accessToken,
user_id: token.user_id,
client_id: token.client_id,
expires: accessExpires
}).then(function then() {
return models.Refreshtoken.edit({expires: refreshExpires}, {id: token.id});
}).then(function then() {
return done(null, accessToken, {expires_in: utils.ONE_HOUR_S});
}).catch(function handleError(error) {
return done(error, false);
});
} else {
done(new errors.UnauthorizedError('Refresh token expired.'), false);
}
}
});
}
function exchangePassword(client, username, password, scope, done) {
// Validate the client
models.Client.findOne({slug: client.slug}).then(function then(client) {
if (!client) {
return done(new errors.NoPermissionError('Invalid client.'), false);
}
// Validate the user
return models.User.check({email: username, password: password}).then(function then(user) {
// Everything validated, return the access- and refreshtoken
var accessToken = utils.uid(256),
refreshToken = utils.uid(256),
accessExpires = Date.now() + utils.ONE_HOUR_MS,
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
return models.Accesstoken.add(
{token: accessToken, user_id: user.id, client_id: client.id, expires: accessExpires}
).then(function then() {
return models.Refreshtoken.add(
{token: refreshToken, user_id: user.id, client_id: client.id, expires: refreshExpires}
);
}).then(function then() {
spamPrevention.resetCounter(username);
return done(null, accessToken, refreshToken, {expires_in: utils.ONE_HOUR_S});
});
}).catch(function handleError(error) {
return done(error, false);
});
});
}
oauth = {
init: function init(oauthServer, resetSpamCounter) {
init: function init() {
oauthServer = oauth2orize.createServer();
// remove all expired accesstokens on startup
models.Accesstoken.destroyAllExpired();
@ -18,75 +81,21 @@ 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({userProperty: 'client'}, function exchange(client, username, password, scope, done) {
// Validate the client
models.Client.forge({slug: client.slug})
.fetch()
.then(function then(client) {
if (!client) {
return done(new errors.NoPermissionError('Invalid client.'), false);
}
// Validate the user
return models.User.check({email: username, password: password}).then(function then(user) {
// Everything validated, return the access- and refreshtoken
var accessToken = utils.uid(256),
refreshToken = utils.uid(256),
accessExpires = Date.now() + utils.ONE_HOUR_MS,
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
return models.Accesstoken.add(
{token: accessToken, user_id: user.id, client_id: client.id, expires: accessExpires}
).then(function then() {
return models.Refreshtoken.add(
{token: refreshToken, user_id: user.id, client_id: client.id, expires: refreshExpires}
);
}).then(function then() {
resetSpamCounter(username);
return done(null, accessToken, refreshToken, {expires_in: utils.ONE_HOUR_S});
}).catch(function handleError(error) {
return done(error, false);
});
}).catch(function handleError(error) {
return done(error);
});
});
}));
oauthServer.exchange(oauth2orize.exchange.password({userProperty: 'client'},
exchangePassword));
// Exchange the refresh token to obtain an access token. The callback accepts the
// `client`, which is exchanging a `refreshToken` previously issued by the server
// 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.refreshToken(function exchange(client, refreshToken, scope, done) {
models.Refreshtoken.forge({token: refreshToken})
.fetch()
.then(function then(model) {
if (!model) {
return done(new errors.NoPermissionError('Invalid refresh token.'), false);
} else {
var token = model.toJSON(),
accessToken = utils.uid(256),
accessExpires = Date.now() + utils.ONE_HOUR_MS,
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
oauthServer.exchange(oauth2orize.exchange.refreshToken({userProperty: 'client'},
exchangeRefreshToken));
},
if (token.expires > Date.now()) {
models.Accesstoken.add({
token: accessToken,
user_id: token.user_id,
client_id: token.client_id,
expires: accessExpires
}).then(function then() {
return models.Refreshtoken.edit({expires: refreshExpires}, {id: token.id});
}).then(function then() {
return done(null, accessToken, {expires_in: utils.ONE_HOUR_S});
}).catch(function handleError(error) {
return done(error, false);
});
} else {
done(new errors.UnauthorizedError('Refresh token expired.'), false);
}
}
});
}));
// ### 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);
}
};

View file

@ -100,7 +100,7 @@ apiRoutes = function apiRoutes(middleware) {
router.post('/authentication/token',
middleware.spamPrevention.signin,
middleware.api.authenticateClient,
middleware.api.generateAccessToken
middleware.oauth.generateAccessToken
);
router.post('/authentication/revoke', authenticatePrivate, api.http(api.authentication.revoke));

View file

@ -0,0 +1,274 @@
/*globals describe, before, beforeEach, afterEach, it*/
/*jshint expr:true*/
var sinon = require('sinon'),
should = require('should'),
Promise = require('bluebird'),
oAuth = require('../../../server/middleware/oauth'),
Models = require('../../../server/models');
// To stop jshint complaining
should.equal(true, true);
describe('OAuth', function () {
var next, req, res, sandbox;
before(function (done) {
// Loads all the models
Models.init().then(done).catch(done);
});
beforeEach(function () {
sandbox = sinon.sandbox.create();
req = {};
res = {};
next = sandbox.spy();
});
afterEach(function () {
sandbox.restore();
});
describe('Generate Token from Password', function () {
beforeEach(function () {
sandbox.stub(Models.Accesstoken, 'destroyAllExpired')
.returns(new Promise.resolve());
sandbox.stub(Models.Refreshtoken, 'destroyAllExpired')
.returns(new Promise.resolve());
});
it('Successfully generate access token.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.body.grant_type = 'password';
req.body.username = 'username';
req.body.password = 'password';
res.setHeader = {};
res.end = {};
sandbox.stub(Models.Client, 'findOne')
.withArgs({slug: 'test'}).returns(new Promise.resolve({
id: 1
}));
sandbox.stub(Models.User, 'check')
.withArgs({email: 'username', password: 'password'}).returns(new Promise.resolve({
id: 1
}));
sandbox.stub(Models.Accesstoken, 'add')
.returns(new Promise.resolve());
sandbox.stub(Models.Refreshtoken, 'add')
.returns(new Promise.resolve());
sandbox.stub(res, 'setHeader', function () {});
sandbox.stub(res, 'end', function (json) {
try {
json.should.exist;
json = JSON.parse(json);
json.should.have.property('access_token');
json.should.have.property('refresh_token');
json.should.have.property('expires_in');
json.should.have.property('token_type', 'Bearer');
next.called.should.eql(false);
done();
} catch (err) {
done(err);
}
});
oAuth.init();
oAuth.generateAccessToken(req, res, next);
});
it('Can\'t generate access token without client.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.body.grant_type = 'password';
req.body.username = 'username';
req.body.password = 'password';
res.setHeader = {};
res.end = {};
sandbox.stub(Models.Client, 'findOne')
.withArgs({slug: 'test'}).returns(new Promise.resolve());
oAuth.init();
oAuth.generateAccessToken(req, res, function (err) {
err.errorType.should.eql('NoPermissionError');
done();
});
});
it('Handles database error.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.body.grant_type = 'password';
req.body.username = 'username';
req.body.password = 'password';
res.setHeader = {};
res.end = {};
sandbox.stub(Models.Client, 'findOne')
.withArgs({slug: 'test'}).returns(new Promise.resolve({
id: 1
}));
sandbox.stub(Models.User, 'check')
.withArgs({email: 'username', password: 'password'}).returns(new Promise.resolve({
id: 1
}));
sandbox.stub(Models.Accesstoken, 'add')
.returns(new Promise.reject({
message: 'DB error'
}));
oAuth.init();
oAuth.generateAccessToken(req, res, function (err) {
err.message.should.eql('DB error');
done();
});
});
});
describe('Generate Token from Refreshtoken', function () {
beforeEach(function () {
sandbox.stub(Models.Accesstoken, 'destroyAllExpired')
.returns(new Promise.resolve());
sandbox.stub(Models.Refreshtoken, 'destroyAllExpired')
.returns(new Promise.resolve());
});
it('Successfully generate access token.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = {};
res.end = {};
sandbox.stub(Models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve({
toJSON: function () {
return {
expires: Date.now() + 3600
};
}
}));
sandbox.stub(Models.Accesstoken, 'add')
.returns(new Promise.resolve());
sandbox.stub(Models.Refreshtoken, 'edit')
.returns(new Promise.resolve());
sandbox.stub(res, 'setHeader', function () {});
sandbox.stub(res, 'end', function (json) {
try {
json.should.exist;
json = JSON.parse(json);
json.should.have.property('access_token');
json.should.have.property('expires_in');
json.should.have.property('token_type', 'Bearer');
next.called.should.eql(false);
done();
} catch (err) {
done(err);
}
});
oAuth.init();
oAuth.generateAccessToken(req, res, next);
});
it('Can\'t generate access token without valid refresh token.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = {};
res.end = {};
sandbox.stub(Models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve());
oAuth.init();
oAuth.generateAccessToken(req, res, function (err) {
err.errorType.should.eql('NoPermissionError');
done();
});
});
it('Can\'t generate access token with expired refresh token.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = {};
res.end = {};
sandbox.stub(Models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve({
toJSON: function () {
return {
expires: Date.now() - 3600
};
}
}));
oAuth.init();
oAuth.generateAccessToken(req, res, function (err) {
err.errorType.should.eql('UnauthorizedError');
done();
});
});
it('Handles database error.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = {};
res.end = {};
sandbox.stub(Models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve({
toJSON: function () {
return {
expires: Date.now() + 3600
};
}
}));
sandbox.stub(Models.Accesstoken, 'add')
.returns(new Promise.reject({
message: 'DB error'
}));
oAuth.init();
oAuth.generateAccessToken(req, res, function (err) {
err.message.should.eql('DB error');
done();
});
});
});
});