mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 13:54:35 +03:00
🎨 one token endpoint (#7571)
* 🎨 one token endpoint refs #7562 - delete /authentication/ghost - Ghost-Admin will use /authentication/token for all use cases (password, refresh token and ghost.org authorization code) - add new grant_type `authorization_code` * 🎨 update comment description and remove spamPrevention.resetCounter
This commit is contained in:
parent
25b0c9eb9a
commit
4056a6da4a
@ -185,12 +185,6 @@ function apiRoutes() {
|
||||
auth.oauth.generateAccessToken
|
||||
);
|
||||
|
||||
apiRouter.post('/authentication/ghost', [
|
||||
auth.authenticate.authenticateClient,
|
||||
auth.authenticate.authenticateGhostUser,
|
||||
api.http(api.authentication.createTokens)
|
||||
]);
|
||||
|
||||
apiRouter.post('/authentication/revoke', authenticatePrivate, api.http(api.authentication.revoke));
|
||||
|
||||
// ## Uploads
|
||||
|
@ -105,29 +105,6 @@ authenticate = {
|
||||
}));
|
||||
}
|
||||
)(req, res, next);
|
||||
},
|
||||
|
||||
// ### Authenticate Ghost.org User
|
||||
authenticateGhostUser: function authenticateGhostUser(req, res, next) {
|
||||
req.query.code = req.body.authorizationCode;
|
||||
|
||||
if (!req.query.code) {
|
||||
return next(new errors.UnauthorizedError({message: i18n.t('errors.middleware.auth.accessDenied')}));
|
||||
}
|
||||
|
||||
passport.authenticate('ghost', {session: false, failWithError: false}, function authenticate(err, user, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return next(new errors.UnauthorizedError({message: i18n.t('errors.middleware.auth.accessDenied')}));
|
||||
}
|
||||
|
||||
req.authInfo = info;
|
||||
req.user = user;
|
||||
next();
|
||||
})(req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
var oauth2orize = require('oauth2orize'),
|
||||
passport = require('passport'),
|
||||
models = require('../models'),
|
||||
utils = require('../utils'),
|
||||
errors = require('../errors'),
|
||||
@ -62,6 +63,42 @@ function exchangePassword(client, username, password, scope, done) {
|
||||
});
|
||||
}
|
||||
|
||||
function exchangeAuthorizationCode(req, res, next) {
|
||||
if (!req.body.authorizationCode) {
|
||||
return next(new errors.UnauthorizedError({
|
||||
message: i18n.t('errors.middleware.auth.accessDenied')
|
||||
}));
|
||||
}
|
||||
|
||||
req.query.code = req.body.authorizationCode;
|
||||
|
||||
passport.authenticate('ghost', {session: false, failWithError: false}, function authenticate(err, user) {
|
||||
if (err) {
|
||||
return next(new errors.UnauthorizedError({
|
||||
err: err
|
||||
}));
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return next(new errors.UnauthorizedError({
|
||||
message: i18n.t('errors.middleware.auth.accessDenied')
|
||||
}));
|
||||
}
|
||||
|
||||
authenticationAPI.createTokens({}, {context: {client_id: req.client.id, user: user.id}})
|
||||
.then(function then(response) {
|
||||
res.json({
|
||||
access_token: response.access_token,
|
||||
refresh_token: response.refresh_token,
|
||||
expires_in: response.expires_in
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
next(err);
|
||||
});
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
oauth = {
|
||||
|
||||
init: function init() {
|
||||
@ -85,6 +122,23 @@ oauth = {
|
||||
// access token on behalf of the user who authorized the code.
|
||||
oauthServer.exchange(oauth2orize.exchange.refreshToken({userProperty: 'client'},
|
||||
exchangeRefreshToken));
|
||||
|
||||
/**
|
||||
* Exchange authorization_code for an access token.
|
||||
* We forward to authorization code to Ghost.org.
|
||||
*
|
||||
* oauth2orize offers a default implementation via exchange.authorizationCode, but this function
|
||||
* wraps the express request and response. So no chance to get access to it.
|
||||
* We use passport to communicate with Ghost.org. Passport's module design requires the express req/res.
|
||||
*
|
||||
* For now it's OK to not use exchange.authorizationCode. You can read through the implementation here:
|
||||
* https://github.com/jaredhanson/oauth2orize/blob/master/lib/exchange/authorizationCode.js
|
||||
* As you can see, it does some validation and set's some headers, not very very important,
|
||||
* but it's part of the oauth2 spec.
|
||||
*
|
||||
* @TODO: How to use exchange.authorizationCode in combination of passport?
|
||||
*/
|
||||
oauthServer.exchange('authorization_code', exchangeAuthorizationCode);
|
||||
},
|
||||
|
||||
// ### Generate access token Middleware
|
||||
|
@ -27,7 +27,7 @@ spamPrevention = {
|
||||
|
||||
if (req.body.username && req.body.grant_type === 'password') {
|
||||
loginSecurity.push({ip: remoteAddress, time: currentTime, email: req.body.username});
|
||||
} else if (req.body.grant_type === 'refresh_token') {
|
||||
} else if (req.body.grant_type === 'refresh_token' || req.body.grant_type === 'authorization_code') {
|
||||
return next();
|
||||
} else {
|
||||
return next(new errors.BadRequestError({message: i18n.t('errors.middleware.spamprevention.noUsername')}));
|
||||
|
@ -1,15 +1,18 @@
|
||||
var sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
Promise = require('bluebird'),
|
||||
oAuth = require('../../../server/auth/oauth'),
|
||||
Models = require('../../../server/models');
|
||||
var sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
Promise = require('bluebird'),
|
||||
passport = require('passport'),
|
||||
testUtils = require('../../utils'),
|
||||
oAuth = require('../../../server/auth/oauth'),
|
||||
api = require('../../../server/api'),
|
||||
errors = require('../../../server/errors'),
|
||||
models = require('../../../server/models');
|
||||
|
||||
describe('OAuth', function () {
|
||||
var next, req, res, sandbox;
|
||||
|
||||
before(function () {
|
||||
// Loads all the models
|
||||
Models.init();
|
||||
models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
@ -25,10 +28,12 @@ describe('OAuth', function () {
|
||||
|
||||
describe('Generate Token from Password', function () {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(Models.Accesstoken, 'destroyAllExpired')
|
||||
sandbox.stub(models.Accesstoken, 'destroyAllExpired')
|
||||
.returns(new Promise.resolve());
|
||||
sandbox.stub(Models.Refreshtoken, 'destroyAllExpired')
|
||||
sandbox.stub(models.Refreshtoken, 'destroyAllExpired')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
oAuth.init();
|
||||
});
|
||||
|
||||
it('Successfully generate access token.', function (done) {
|
||||
@ -43,18 +48,20 @@ describe('OAuth', function () {
|
||||
res.setHeader = {};
|
||||
res.end = {};
|
||||
|
||||
sandbox.stub(Models.Client, 'findOne')
|
||||
sandbox.stub(models.Client, 'findOne')
|
||||
.withArgs({slug: 'test'}).returns(new Promise.resolve({
|
||||
id: 1
|
||||
}));
|
||||
sandbox.stub(Models.User, 'check')
|
||||
id: 1
|
||||
}));
|
||||
|
||||
sandbox.stub(models.User, 'check')
|
||||
.withArgs({email: 'username', password: 'password'}).returns(new Promise.resolve({
|
||||
id: 1
|
||||
}));
|
||||
sandbox.stub(Models.Accesstoken, 'add')
|
||||
id: 1
|
||||
}));
|
||||
|
||||
sandbox.stub(models.Accesstoken, 'add')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
sandbox.stub(Models.Refreshtoken, 'add')
|
||||
sandbox.stub(models.Refreshtoken, 'add')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
sandbox.stub(res, 'setHeader', function () {});
|
||||
@ -73,7 +80,7 @@ describe('OAuth', function () {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
oAuth.init();
|
||||
|
||||
oAuth.generateAccessToken(req, res, next);
|
||||
});
|
||||
|
||||
@ -89,10 +96,9 @@ describe('OAuth', function () {
|
||||
res.setHeader = {};
|
||||
res.end = {};
|
||||
|
||||
sandbox.stub(Models.Client, 'findOne')
|
||||
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();
|
||||
@ -111,20 +117,21 @@ describe('OAuth', function () {
|
||||
res.setHeader = {};
|
||||
res.end = {};
|
||||
|
||||
sandbox.stub(Models.Client, 'findOne')
|
||||
sandbox.stub(models.Client, 'findOne')
|
||||
.withArgs({slug: 'test'}).returns(new Promise.resolve({
|
||||
id: 1
|
||||
}));
|
||||
sandbox.stub(Models.User, 'check')
|
||||
id: 1
|
||||
}));
|
||||
|
||||
sandbox.stub(models.User, 'check')
|
||||
.withArgs({email: 'username', password: 'password'}).returns(new Promise.resolve({
|
||||
id: 1
|
||||
}));
|
||||
sandbox.stub(Models.Accesstoken, 'add')
|
||||
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();
|
||||
@ -134,10 +141,12 @@ describe('OAuth', function () {
|
||||
|
||||
describe('Generate Token from Refreshtoken', function () {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(Models.Accesstoken, 'destroyAllExpired')
|
||||
sandbox.stub(models.Accesstoken, 'destroyAllExpired')
|
||||
.returns(new Promise.resolve());
|
||||
sandbox.stub(Models.Refreshtoken, 'destroyAllExpired')
|
||||
sandbox.stub(models.Refreshtoken, 'destroyAllExpired')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
oAuth.init();
|
||||
});
|
||||
|
||||
it('Successfully generate access token.', function (done) {
|
||||
@ -151,19 +160,19 @@ describe('OAuth', function () {
|
||||
res.setHeader = {};
|
||||
res.end = {};
|
||||
|
||||
sandbox.stub(Models.Refreshtoken, 'findOne')
|
||||
sandbox.stub(models.Refreshtoken, 'findOne')
|
||||
.withArgs({token: 'token'}).returns(new Promise.resolve({
|
||||
toJSON: function () {
|
||||
return {
|
||||
expires: Date.now() + 3600
|
||||
};
|
||||
}
|
||||
}));
|
||||
toJSON: function () {
|
||||
return {
|
||||
expires: Date.now() + 3600
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
sandbox.stub(Models.Accesstoken, 'add')
|
||||
sandbox.stub(models.Accesstoken, 'add')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
sandbox.stub(Models.Refreshtoken, 'edit')
|
||||
sandbox.stub(models.Refreshtoken, 'edit')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
sandbox.stub(res, 'setHeader', function () {});
|
||||
@ -181,7 +190,7 @@ describe('OAuth', function () {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
oAuth.init();
|
||||
|
||||
oAuth.generateAccessToken(req, res, next);
|
||||
});
|
||||
|
||||
@ -196,10 +205,9 @@ describe('OAuth', function () {
|
||||
res.setHeader = {};
|
||||
res.end = {};
|
||||
|
||||
sandbox.stub(Models.Refreshtoken, 'findOne')
|
||||
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();
|
||||
@ -217,16 +225,15 @@ describe('OAuth', function () {
|
||||
res.setHeader = {};
|
||||
res.end = {};
|
||||
|
||||
sandbox.stub(Models.Refreshtoken, 'findOne')
|
||||
sandbox.stub(models.Refreshtoken, 'findOne')
|
||||
.withArgs({token: 'token'}).returns(new Promise.resolve({
|
||||
toJSON: function () {
|
||||
return {
|
||||
expires: Date.now() - 3600
|
||||
};
|
||||
}
|
||||
}));
|
||||
toJSON: function () {
|
||||
return {
|
||||
expires: Date.now() - 3600
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
oAuth.init();
|
||||
oAuth.generateAccessToken(req, res, function (err) {
|
||||
err.errorType.should.eql('UnauthorizedError');
|
||||
done();
|
||||
@ -244,25 +251,109 @@ describe('OAuth', function () {
|
||||
res.setHeader = {};
|
||||
res.end = {};
|
||||
|
||||
sandbox.stub(Models.Refreshtoken, 'findOne')
|
||||
sandbox.stub(models.Refreshtoken, 'findOne')
|
||||
.withArgs({token: 'token'}).returns(new Promise.resolve({
|
||||
toJSON: function () {
|
||||
return {
|
||||
expires: Date.now() + 3600
|
||||
};
|
||||
}
|
||||
}));
|
||||
toJSON: function () {
|
||||
return {
|
||||
expires: Date.now() + 3600
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
sandbox.stub(Models.Accesstoken, 'add')
|
||||
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 Authorization Code', function () {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(models.Accesstoken, 'destroyAllExpired')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
sandbox.stub(models.Refreshtoken, 'destroyAllExpired')
|
||||
.returns(new Promise.resolve());
|
||||
|
||||
oAuth.init();
|
||||
});
|
||||
|
||||
it('Successfully generate access token.', function (done) {
|
||||
var user = new models.User(testUtils.DataGenerator.forKnex.createUser());
|
||||
|
||||
req.body = {};
|
||||
req.query = {};
|
||||
req.client = {
|
||||
id: 1
|
||||
};
|
||||
|
||||
req.body.grant_type = 'authorization_code';
|
||||
req.body.authorizationCode = '1234';
|
||||
|
||||
res.json = function (data) {
|
||||
data.access_token.should.eql('access-token');
|
||||
data.refresh_token.should.eql('refresh-token');
|
||||
data.expires_in.should.eql(10);
|
||||
done();
|
||||
};
|
||||
|
||||
sandbox.stub(api.authentication, 'createTokens').returns(Promise.resolve({
|
||||
access_token: 'access-token',
|
||||
refresh_token: 'refresh-token',
|
||||
expires_in: 10
|
||||
}));
|
||||
|
||||
sandbox.stub(passport, 'authenticate', function (name, options, onSuccess) {
|
||||
return function () {
|
||||
onSuccess(null, user);
|
||||
};
|
||||
});
|
||||
|
||||
oAuth.generateAccessToken(req, res, next);
|
||||
});
|
||||
|
||||
it('Error: ghost.org', function (done) {
|
||||
req.body = {};
|
||||
req.query = {};
|
||||
req.client = {
|
||||
id: 1
|
||||
};
|
||||
|
||||
req.body.grant_type = 'authorization_code';
|
||||
req.body.authorizationCode = '1234';
|
||||
|
||||
sandbox.stub(passport, 'authenticate', function (name, options, onSuccess) {
|
||||
return function () {
|
||||
onSuccess(new Error('validation error'));
|
||||
};
|
||||
});
|
||||
|
||||
oAuth.generateAccessToken(req, res, function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.UnauthorizedError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Error: no authorization_code provided', function (done) {
|
||||
req.body = {};
|
||||
req.query = {};
|
||||
req.client = {
|
||||
id: 1
|
||||
};
|
||||
|
||||
req.body.grant_type = 'authorization_code';
|
||||
|
||||
oAuth.generateAccessToken(req, res, function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.UnauthorizedError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user