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:
commit
1ba9929c59
5 changed files with 362 additions and 95 deletions
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
274
core/test/unit/middleware/oauth_spec.js
Normal file
274
core/test/unit/middleware/oauth_spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue