🔥 Removed v0.1 auth services (#11104)

This commit is contained in:
Naz Gargol 2019-09-11 19:40:48 +02:00 committed by GitHub
parent 5b59c7b542
commit 95ea5265d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 0 additions and 1605 deletions

View File

@ -25,7 +25,6 @@ function initialiseServices() {
routing.bootstrap.start(); routing.bootstrap.start();
const permissions = require('./services/permissions'), const permissions = require('./services/permissions'),
auth = require('./services/auth'),
apps = require('./services/apps'), apps = require('./services/apps'),
xmlrpc = require('./services/xmlrpc'), xmlrpc = require('./services/xmlrpc'),
slack = require('./services/slack'), slack = require('./services/slack'),
@ -58,9 +57,6 @@ function initialiseServices() {
require('./analytics-events').init(); require('./analytics-events').init();
} }
}).then(function () { }).then(function () {
parentApp.use(auth.init());
debug('Auth done');
debug('...`initialiseServices` End'); debug('...`initialiseServices` End');
}); });
} }

View File

@ -1,73 +0,0 @@
const models = require('../../models');
const common = require('../../lib/common');
let strategies;
strategies = {
/**
* ClientPasswordStrategy
*
* This strategy is used to authenticate registered OAuth clients. It is
* employed to protect the `token` endpoint, which consumers use to obtain
* access tokens. The OAuth 2.0 specification suggests that clients use the
* HTTP Basic scheme to authenticate (not implemented yet).
* 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}, {withRelated: ['trustedDomains']})
.then(function then(model) {
if (model) {
var client = model.toJSON({withRelated: ['trustedDomains']});
if (client.status === 'enabled' && client.secret === clientSecret) {
return done(null, client);
}
}
return done(null, false);
});
},
/**
* BearerStrategy
*
* This strategy is used to authenticate users based on an access token (aka a
* bearer token). The user must have previously authorized a client
* application, which is issued an access token to make requests on behalf of
* the authorizing user.
*/
bearerStrategy: function bearerStrategy(accessToken, done) {
return models.Accesstoken.findOne({token: accessToken})
.then(function then(model) {
if (model) {
var token = model.toJSON();
if (token.expires > Date.now()) {
return models.User.findOne({id: token.user_id})
.then(function then(model) {
if (!model) {
return done(null, false);
}
if (!model.isActive()) {
throw new common.errors.NoPermissionError({
message: common.i18n.t('errors.models.user.accountSuspended')
});
}
var user = model.toJSON(),
info = {scope: '*'};
return done(null, {id: user.id}, info);
})
.catch(function (err) {
return done(err);
});
} else {
return done(null, false);
}
} else {
return done(null, false);
}
});
}
};
module.exports = strategies;

View File

@ -1,107 +1,8 @@
const passport = require('passport');
const authUtils = require('./utils');
const models = require('../../models');
const common = require('../../lib/common');
const session = require('./session'); const session = require('./session');
const apiKeyAuth = require('./api-key'); const apiKeyAuth = require('./api-key');
const members = require('./members'); const members = require('./members');
const authenticate = { const authenticate = {
// ### Authenticate Client Middleware
authenticateClient: function authenticateClient(req, res, next) {
/**
* In theory, client authentication is not required for public clients, only for confidential clients.
* See e.g. https://tools.ietf.org/html/rfc6749#page-38. Ghost has no differentiation for this at the moment.
* See also See https://tools.ietf.org/html/rfc6749#section-2.1.
*
* Ghost requires client authentication for `grant_type: password`, because we have to ensure that
* we tie a client to a new access token. That means `grant_type: refresh_token` does not require
* client authentication, because binding a client already happened.
*
* To sum up:
* - password authentication requires client authentication
* - refreshing a token does not require client authentication
* - public API requires client authentication
* - as soon as you send an access token in the header or via query
* - we deny public API access
* - API access with a Bearer does not require client authentication
*/
if (authUtils.getBearerAutorizationToken(req) && !authUtils.hasGrantType(req, 'password')) {
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 next(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied'),
context: common.i18n.t('errors.middleware.auth.clientCredentialsNotProvided'),
help: common.i18n.t('errors.middleware.auth.forInformationRead', {url: 'https://ghost.org/faq/upgrade-to-ghost-2-0/'})
}));
}
return passport.authenticate(['oauth2-client-password'], {session: false, failWithError: false},
function authenticate(err, client) {
if (err) {
return next(err); // will generate a 500 error
}
// req.body needs to be null for GET requests to build options correctly
delete req.body.client_id;
delete req.body.client_secret;
if (!client) {
return next(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied'),
context: common.i18n.t('errors.middleware.auth.clientCredentialsNotValid'),
help: common.i18n.t('errors.middleware.auth.forInformationRead', {url: 'https://ghost.org/faq/upgrade-to-ghost-2-0/'})
}));
}
req.client = client;
common.events.emit('client.authenticated', client);
return next(null, client);
}
)(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;
common.events.emit('user.authenticated', user);
return next(null, user, info);
} else if (authUtils.getBearerAutorizationToken(req)) {
return next(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
}));
} else if (req.client) {
req.user = {id: models.User.externalUser};
return next();
}
return next(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
}));
}
)(req, res, next);
},
authenticateAdminApi: [apiKeyAuth.admin.authenticate, session.authenticate], authenticateAdminApi: [apiKeyAuth.admin.authenticate, session.authenticate],
authenticateContentApi: [apiKeyAuth.content.authenticateContentApiKey, members.authenticateMembersToken] authenticateContentApi: [apiKeyAuth.content.authenticateContentApiKey, members.authenticateMembersToken]

View File

@ -2,55 +2,6 @@ const labs = require('../labs');
const common = require('../../lib/common'); const common = require('../../lib/common');
const authorize = { const authorize = {
// 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 && req.user.id) {
return next();
} else {
return next(new common.errors.NoPermissionError({
message: common.i18n.t('errors.middleware.auth.pleaseSignIn')
}));
}
},
// ### Require user depending on public API being activated.
requiresAuthorizedUserPublicAPI: function requiresAuthorizedUserPublicAPI(req, res, next) {
if (labs.isSet('publicAPI') === true) {
return next();
} else {
if (req.user && req.user.id) {
return next();
} else {
// CASE: has no user access and public api is disabled
if (labs.isSet('publicAPI') !== true) {
return next(new common.errors.NoPermissionError({
message: common.i18n.t('errors.middleware.auth.publicAPIDisabled.error'),
context: common.i18n.t('errors.middleware.auth.publicAPIDisabled.context'),
help: common.i18n.t('errors.middleware.auth.forInformationRead', {url: 'https://ghost.org/docs/api/content/'})
}));
}
return next(new common.errors.NoPermissionError({
message: common.i18n.t('errors.middleware.auth.pleaseSignIn')
}));
}
}
},
// Requires the authenticated client to match specific client
requiresAuthorizedClient: function requiresAuthorizedClient(client) {
return function doAuthorizedClient(req, res, next) {
if (client && (!req.client || !req.client.name || req.client.name !== client)) {
return next(new common.errors.NoPermissionError({
message: common.i18n.t('errors.permissions.noPermissionToAction')
}));
}
return next();
};
},
authorizeContentApi(req, res, next) { authorizeContentApi(req, res, next) {
const hasApiKey = req.api_key && req.api_key.id; const hasApiKey = req.api_key && req.api_key.id;
const hasMember = req.member; const hasMember = req.member;

View File

@ -17,18 +17,5 @@ module.exports = {
get passwordreset() { get passwordreset() {
return require('./passwordreset'); return require('./passwordreset');
},
/*
* TODO: Get rid of these when v0.1 is gone
*/
get init() {
return (options) => {
require('./oauth').init(options);
return require('./passport').init(options);
};
},
get oauth() {
return require('./oauth');
} }
}; };

View File

@ -1,195 +0,0 @@
var oauth2orize = require('oauth2orize'),
_ = require('lodash'),
passport = require('passport'),
models = require('../../models'),
authUtils = require('./utils'),
web = require('../../web'),
common = require('../../lib/common'),
oauthServer,
oauth;
function exchangeRefreshToken(client, refreshToken, scope, body, authInfo, done) {
models.Base.transaction(function (transacting) {
var options = {
transacting: transacting
};
return models.Refreshtoken.findOne({token: refreshToken}, _.merge({forUpdate: true}, options))
.then(function then(model) {
if (!model) {
throw new common.errors.NoPermissionError({
message: common.i18n.t('errors.middleware.oauth.invalidRefreshToken')
});
}
var token = model.toJSON();
if (token.expires <= Date.now()) {
throw new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.oauth.refreshTokenExpired')
});
}
// @TODO: this runs outside of the transaction
web.shared.middlewares.api.spamPrevention.userLogin()
.reset(authInfo.ip, body.refresh_token + 'login');
return authUtils.createTokens({
clientId: token.client_id,
userId: token.user_id,
oldAccessToken: authInfo.accessToken,
oldRefreshToken: refreshToken,
oldRefreshId: token.id
}, options).then(function (response) {
return {
access_token: response.access_token,
expires_in: response.expires_in
};
});
});
}).then(function (response) {
done(null, response.access_token, {expires_in: response.expires_in});
}).catch(function (err) {
if (common.errors.utils.isIgnitionError(err)) {
return done(err, false);
}
done(new common.errors.InternalServerError({
err: err
}), false);
});
}
// We are required to pass in authInfo in order to reset spam counter for user login
function exchangePassword(client, username, password, scope, body, authInfo, done) {
if (!client || !client.id) {
return done(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.clientCredentialsNotProvided')
}), false);
}
// Validate the user
return models.User.check({email: username, password: password})
.then(function then(user) {
return authUtils.createTokens({
clientId: client.id,
userId: user.id
});
})
.then(function then(response) {
web.shared.middlewares.api.spamPrevention.userLogin()
.reset(authInfo.ip, username + 'login');
return done(null, response.access_token, response.refresh_token, {expires_in: response.expires_in});
})
.catch(function (err) {
done(err, false);
});
}
function exchangeAuthorizationCode(req, res, next) {
if (!req.body.authorizationCode) {
return next(new common.errors.UnauthorizedError({
message: common.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(err);
}
if (!user) {
return next(new common.errors.UnauthorizedError({
message: common.i18n.t('errors.middleware.auth.accessDenied')
}));
}
web.shared.middlewares.api.spamPrevention.userLogin()
.reset(req.authInfo.ip, req.body.authorizationCode + 'login');
authUtils.createTokens({
clientId: req.client.id,
userId: 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() {
oauthServer = oauth2orize.createServer();
// remove all expired accesstokens on startup
models.Accesstoken.destroyAllExpired();
// remove all expired refreshtokens on startup
models.Refreshtoken.destroyAllExpired();
// Exchange user id and password for access tokens. The callback accepts the
// `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'},
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({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
// register the oauth2orize middleware for password and refresh token grants
generateAccessToken: function generateAccessToken(req, res, next) {
/**
* TODO:
* https://github.com/jaredhanson/oauth2orize/issues/182
* oauth2orize only offers the option to forward request information via authInfo object
*
* Important: only used for resetting the brute count (access to req.ip)
*/
req.authInfo = {
ip: req.ip,
accessToken: authUtils.getBearerAutorizationToken(req)
};
return oauthServer.token()(req, res, function (err) {
if (err && err.status === 400) {
err = new common.errors.BadRequestError({err: err, message: err.message});
}
next(err);
});
}
};
module.exports = oauth;

View File

@ -1,15 +0,0 @@
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
BearerStrategy = require('passport-http-bearer').Strategy,
passport = require('passport'),
authStrategies = require('./auth-strategies');
/**
* auth types:
* - password: local login
*/
exports.init = function initPassport() {
passport.use(new ClientPasswordStrategy(authStrategies.clientPasswordStrategy));
passport.use(new BearerStrategy(authStrategies.bearerStrategy));
return passport.initialize();
};

View File

@ -1,137 +0,0 @@
var Promise = require('bluebird'),
_ = require('lodash'),
debug = require('ghost-ignition').debug('auth:utils'),
models = require('../../models'),
security = require('../../lib/security'),
constants = require('../../lib/constants'),
_private = {};
/**
* The initial idea was to delete all old tokens connected to a user and a client.
* But if multiple browsers/apps are using the same client, we would log out them out.
* So the idea is to always decrease the expiry of the old access token if available.
* This access token auto expires and get's cleaned up on bootstrap (see oauth.js).
*/
_private.decreaseOldAccessTokenExpiry = function decreaseOldAccessTokenExpiry(data, options) {
debug('decreaseOldAccessTokenExpiry', data);
if (!data.token) {
return Promise.resolve();
}
return models.Accesstoken.findOne(data, options)
.then(function (oldAccessToken) {
if (!oldAccessToken) {
return Promise.resolve();
}
return models.Accesstoken.edit({
expires: Date.now() + constants.FIVE_MINUTES_MS
}, _.merge({id: oldAccessToken.id}, options));
});
};
_private.handleOldRefreshToken = function handleOldRefreshToken(data, options) {
debug('handleOldRefreshToken', data.oldRefreshToken);
if (!data.oldRefreshToken) {
return models.Refreshtoken.add({
token: data.newRefreshToken,
user_id: data.userId,
client_id: data.clientId,
expires: data.refreshExpires
}, options);
}
// extend refresh token expiry
return models.Refreshtoken.edit({
expires: data.refreshExpires
}, _.merge({id: data.oldRefreshId}, options));
};
_private.handleTokenCreation = function handleTokenCreation(data, options) {
var oldAccessToken = data.oldAccessToken,
oldRefreshToken = data.oldRefreshToken,
oldRefreshId = data.oldRefreshId,
newAccessToken = security.identifier.uid(191),
newRefreshToken = security.identifier.uid(191),
accessExpires = Date.now() + constants.ONE_MONTH_MS,
refreshExpires = Date.now() + constants.SIX_MONTH_MS,
clientId = data.clientId,
userId = data.userId;
return _private.decreaseOldAccessTokenExpiry({token: oldAccessToken}, options)
.then(function () {
return _private.handleOldRefreshToken({
userId: userId,
clientId: clientId,
oldRefreshToken: oldRefreshToken,
oldRefreshId: oldRefreshId,
newRefreshToken: newRefreshToken,
refreshExpires: refreshExpires
}, options);
})
.then(function (refreshToken) {
return models.Accesstoken.add({
token: newAccessToken,
user_id: userId,
client_id: clientId,
issued_by: refreshToken.id,
expires: accessExpires
}, options);
})
.then(function () {
return {
access_token: newAccessToken,
refresh_token: newRefreshToken,
expires_in: constants.ONE_MONTH_S
};
});
};
/**
* A user can have one token per client at a time.
* If the user requests a new pair of tokens, we decrease the expiry of the old access token
* and re-add the refresh token (this happens because this function is used for 3 different cases).
* If the operation fails in between, the user can still use e.g. the refresh token and try again.
*/
module.exports.createTokens = function createTokens(data, modelOptions) {
data = data || {};
modelOptions = modelOptions || {};
debug('createTokens');
if (modelOptions.transacting) {
return _private.handleTokenCreation(data, modelOptions);
}
return models.Base.transaction(function (transaction) {
modelOptions.transacting = transaction;
return _private.handleTokenCreation(data, modelOptions);
});
};
module.exports.getBearerAutorizationToken = function (req) {
var parts,
scheme,
token;
if (req.headers && req.headers.authorization) {
parts = req.headers.authorization.split(' ');
scheme = parts[0];
if (/^Bearer$/i.test(scheme)) {
token = parts[1];
}
} else if (req.query && req.query.access_token) {
token = req.query.access_token;
}
return token;
};
module.exports.hasGrantType = function hasGrantType(req, type) {
return req.body && Object.prototype.hasOwnProperty.call(req.body, 'grant_type') && req.body.grant_type === type
|| req.query && Object.prototype.hasOwnProperty.call(req.query, 'grant_type') && req.query.grant_type === type;
};

View File

@ -1,226 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
Promise = require('bluebird'),
_ = require('lodash'),
authStrategies = require('../../../../server/services/auth/auth-strategies'),
Models = require('../../../../server/models'),
common = require('../../../../server/lib/common'),
security = require('../../../../server/lib/security'),
constants = require('../../../../server/lib/constants'),
fakeClient = {
slug: 'ghost-admin',
secret: 'not_available',
status: 'enabled'
},
fakeValidToken = {
user_id: 3,
token: 'valid-token',
client_id: 1,
expires: Date.now() + constants.ONE_DAY_MS
},
fakeInvalidToken = {
user_id: 3,
token: 'expired-token',
client_id: 1,
expires: Date.now() - constants.ONE_DAY_MS
};
describe('Auth Strategies', function () {
var next;
before(function () {
// Loads all the models
Models.init();
});
beforeEach(function () {
next = sinon.spy();
});
afterEach(function () {
sinon.restore();
});
describe('Client Password Strategy', function () {
var clientStub;
beforeEach(function () {
clientStub = sinon.stub(Models.Client, 'findOne');
clientStub.returns(new Promise.resolve());
clientStub.withArgs({slug: fakeClient.slug}).returns(new Promise.resolve({
toJSON: function () {
return fakeClient;
}
}));
});
it('should find client', function (done) {
var clientId = 'ghost-admin',
clientSecret = 'not_available';
authStrategies.clientPasswordStrategy(clientId, clientSecret, next).then(function () {
clientStub.calledOnce.should.be.true();
clientStub.calledWith({slug: clientId}).should.be.true();
next.called.should.be.true();
next.firstCall.args.length.should.eql(2);
should.equal(next.firstCall.args[0], null);
next.firstCall.args[1].slug.should.eql(clientId);
done();
}).catch(done);
});
it('shouldn\'t find client with invalid id', function (done) {
var clientId = 'invalid_id',
clientSecret = 'not_available';
authStrategies.clientPasswordStrategy(clientId, clientSecret, next).then(function () {
clientStub.calledOnce.should.be.true();
clientStub.calledWith({slug: clientId}).should.be.true();
next.called.should.be.true();
next.calledWith(null, false).should.be.true();
done();
}).catch(done);
});
it('shouldn\'t find client with invalid secret', function (done) {
var clientId = 'ghost-admin',
clientSecret = 'invalid_secret';
authStrategies.clientPasswordStrategy(clientId, clientSecret, next).then(function () {
clientStub.calledOnce.should.be.true();
clientStub.calledWith({slug: clientId}).should.be.true();
next.called.should.be.true();
next.calledWith(null, false).should.be.true();
done();
}).catch(done);
});
it('shouldn\'t auth client that is disabled', function (done) {
var clientId = 'ghost-admin',
clientSecret = 'not_available';
fakeClient.status = 'disabled';
authStrategies.clientPasswordStrategy(clientId, clientSecret, next).then(function () {
clientStub.calledOnce.should.be.true();
clientStub.calledWith({slug: clientId}).should.be.true();
next.called.should.be.true();
next.calledWith(null, false).should.be.true();
done();
}).catch(done);
});
});
describe('Bearer Strategy', function () {
var tokenStub, userStub, userIsActive;
beforeEach(function () {
tokenStub = sinon.stub(Models.Accesstoken, 'findOne');
tokenStub.returns(new Promise.resolve());
tokenStub.withArgs({token: fakeValidToken.token}).returns(new Promise.resolve({
toJSON: function () {
return fakeValidToken;
}
}));
tokenStub.withArgs({token: fakeInvalidToken.token}).returns(new Promise.resolve({
toJSON: function () {
return fakeInvalidToken;
}
}));
userStub = sinon.stub(Models.User, 'findOne');
userStub.returns(new Promise.resolve());
userStub.withArgs({id: 3}).returns(new Promise.resolve({
toJSON: function () {
return {id: 3};
},
isActive: function () {
return userIsActive;
}
}));
});
it('should find user with valid token', function (done) {
var accessToken = 'valid-token',
userId = 3;
userIsActive = true;
authStrategies.bearerStrategy(accessToken, next).then(function () {
tokenStub.calledOnce.should.be.true();
tokenStub.calledWith({token: accessToken}).should.be.true();
userStub.calledOnce.should.be.true();
userStub.calledWith({id: userId}).should.be.true();
next.calledOnce.should.be.true();
next.firstCall.args.length.should.eql(3);
next.calledWith(null, {id: userId}, {scope: '*'}).should.be.true();
done();
}).catch(done);
});
it('should find user with valid token, but user is suspended', function (done) {
var accessToken = 'valid-token',
userId = 3;
userIsActive = false;
authStrategies.bearerStrategy(accessToken, next).then(function () {
tokenStub.calledOnce.should.be.true();
tokenStub.calledWith({token: accessToken}).should.be.true();
userStub.calledOnce.should.be.true();
userStub.calledWith({id: userId}).should.be.true();
next.calledOnce.should.be.true();
next.firstCall.args.length.should.eql(1);
(next.firstCall.args[0] instanceof common.errors.NoPermissionError).should.eql(true);
next.firstCall.args[0].message.should.eql('Your account was suspended.');
done();
}).catch(done);
});
it('shouldn\'t find user with invalid token', function (done) {
var accessToken = 'invalid_token';
authStrategies.bearerStrategy(accessToken, next).then(function () {
tokenStub.calledOnce.should.be.true();
tokenStub.calledWith({token: accessToken}).should.be.true();
userStub.called.should.be.false();
next.called.should.be.true();
next.calledWith(null, false).should.be.true();
done();
}).catch(done);
});
it('should find user that doesn\'t exist', function (done) {
var accessToken = 'valid-token',
userId = 2;
// override user
fakeValidToken.user_id = userId;
authStrategies.bearerStrategy(accessToken, next).then(function () {
tokenStub.calledOnce.should.be.true();
tokenStub.calledWith({token: accessToken}).should.be.true();
userStub.calledOnce.should.be.true();
userStub.calledWith({id: userId}).should.be.true();
next.called.should.be.true();
next.calledWith(null, false).should.be.true();
done();
}).catch(done);
});
it('should find user with expired token', function (done) {
var accessToken = 'expired-token';
authStrategies.bearerStrategy(accessToken, next).then(function () {
tokenStub.calledOnce.should.be.true();
tokenStub.calledWith({token: accessToken}).should.be.true();
userStub.calledOnce.should.be.false();
next.called.should.be.true();
next.calledWith(null, false).should.be.true();
done();
}).catch(done);
});
});
});

View File

@ -1,366 +0,0 @@
const should = require('should');
const sinon = require('sinon');
const passport = require('passport');
const BearerStrategy = require('passport-http-bearer').Strategy;
const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy;
const auth = require('../../../../server/services/auth');
const common = require('../../../../server/lib/common');
const models = require('../../../../server/models');
const labs = require('../../../../server/services/labs');
const user = {id: 1};
const info = {scope: '*'};
const token = 'test_token';
const testClient = 'test_client';
const testSecret = 'not_available';
const client = {
id: 2,
type: 'ua'
};
function registerSuccessfulBearerStrategy() {
// register fake BearerStrategy which always authenticates
passport.use(new BearerStrategy(
function strategy(accessToken, done) {
accessToken.should.eql(token);
return done(null, user, info);
}
));
}
function registerUnsuccessfulBearerStrategy() {
// register fake BearerStrategy which always authenticates
passport.use(new BearerStrategy(
function strategy(accessToken, done) {
accessToken.should.eql(token);
return done(null, false);
}
));
}
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, loggingStub;
before(function () {
models.init();
});
beforeEach(function () {
req = {};
res = {};
next = sinon.spy();
loggingStub = sinon.stub(common.logging, 'error');
});
afterEach(function () {
sinon.restore();
});
it('should require authorized user (user exists)', function (done) {
req.user = {id: 1};
auth.authorize.requiresAuthorizedUser(req, res, next);
next.called.should.be.true();
next.calledWith().should.be.true();
done();
});
it('should require authorized user (user is missing)', function (done) {
req.user = false;
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(403);
(err instanceof common.errors.NoPermissionError).should.eql(true);
done();
};
auth.authorize.requiresAuthorizedUser(req, res, next);
});
describe('User Authentication', function () {
it('should authenticate user', function (done) {
req.headers = {};
req.headers.authorization = 'Bearer ' + token;
registerSuccessfulBearerStrategy();
auth.authenticate.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.authenticate.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 = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
registerUnsuccessfulBearerStrategy();
auth.authenticate.authenticateUser(req, res, next);
});
it('shouldn\'t authenticate without bearer token', function (done) {
req.headers = {};
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
registerUnsuccessfulBearerStrategy();
auth.authenticate.authenticateUser(req, res, next);
});
it('shouldn\'t authenticate with bearer token and client', function (done) {
req.headers = {};
req.headers.authorization = 'Bearer ' + token;
req.client = {id: 1};
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
registerUnsuccessfulBearerStrategy();
auth.authenticate.authenticateUser(req, res, next);
});
it('shouldn\'t authenticate when error', function (done) {
req.headers = {};
req.headers.authorization = 'Bearer ' + token;
registerFaultyBearerStrategy();
auth.authenticate.authenticateUser(req, res, next);
next.called.should.be.true();
next.calledWith('error').should.be.true();
done();
});
});
describe('Client Authentication', function () {
beforeEach(function () {
sinon.stub(labs, 'isSet').withArgs('publicAPI').returns(true);
});
it('shouldn\'t require authorized client with bearer token', function (done) {
req.headers = {};
req.headers.authorization = 'Bearer ' + token;
auth.authenticate.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 = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
auth.authenticate.authenticateClient(req, res, next);
});
it('shouldn\'t authenticate client without client_id/client_secret', function (done) {
req.body = {};
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
auth.authenticate.authenticateClient(req, res, next);
});
it('shouldn\'t authenticate client without client_id', function (done) {
req.body = {};
req.body.client_secret = testSecret;
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
auth.authenticate.authenticateClient(req, res, next);
});
it('shouldn\'t authenticate client without client_secret', function (done) {
req.body = {};
req.body.client_id = testClient;
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
auth.authenticate.authenticateClient(req, res, next);
});
it('shouldn\'t authenticate without full client credentials', function (done) {
req.body = {};
req.body.client_id = testClient;
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
registerUnsuccessfulClientPasswordStrategy();
auth.authenticate.authenticateClient(req, res, next);
});
it('shouldn\'t authenticate invalid/unknown client', function (done) {
req.body = {};
req.body.client_id = testClient;
req.body.client_secret = testSecret;
res.status = {};
var next = function next(err) {
err.statusCode.should.eql(401);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
};
registerUnsuccessfulClientPasswordStrategy();
auth.authenticate.authenticateClient(req, res, next);
});
it('should authenticate valid/known client', function (done) {
req.body = {};
req.body.client_id = testClient;
req.body.client_secret = testSecret;
req.headers = {};
registerSuccessfulClientPasswordStrategy();
auth.authenticate.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 = {};
registerSuccessfulClientPasswordStrategy();
auth.authenticate.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 = {};
registerSuccessfulClientPasswordStrategy();
auth.authenticate.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.authenticate.authenticateClient(req, res, next);
next.called.should.be.true();
next.calledWith('error').should.be.true();
done();
});
});
});

View File

@ -1,406 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
Promise = require('bluebird'),
passport = require('passport'),
testUtils = require('../../../utils'),
oAuth = require('../../../../server/services/auth/oauth'),
authUtils = require('../../../../server/services/auth/utils'),
spamPrevention = require('../../../../server/web/shared/middlewares/api/spam-prevention'),
common = require('../../../../server/lib/common'),
models = require('../../../../server/models');
describe('OAuth', function () {
var next, req, res;
before(function () {
models.init();
});
beforeEach(function () {
req = {};
res = {};
next = sinon.spy();
sinon.stub(spamPrevention.userLogin(), 'reset');
});
afterEach(function () {
sinon.restore();
});
describe('Generate Token from Password', function () {
beforeEach(function () {
sinon.stub(models.Accesstoken, 'destroyAllExpired')
.returns(new Promise.resolve());
sinon.stub(models.Refreshtoken, 'destroyAllExpired')
.returns(new Promise.resolve());
oAuth.init();
});
it('Successfully generate access token.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.connection = {remoteAddress: '127.0.0.1'};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'password';
req.body.username = 'username';
req.body.password = 'password';
req.client = {
id: 1
};
res.setHeader = function () {
};
res.end = function () {
};
sinon.stub(models.User, 'check')
.withArgs({email: 'username', password: 'password'}).returns(Promise.resolve({
id: 1
}));
sinon.stub(authUtils, 'createTokens')
.returns(Promise.resolve({
access_token: 'AT',
refresh_token: 'RT',
expires_in: Date.now() + 1000
}));
sinon.stub(res, 'setHeader').callsFake(function () {
});
sinon.stub(res, 'end').callsFake(function (json) {
try {
should.exist(json);
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);
spamPrevention.userLogin().reset.called.should.eql(true);
done();
} catch (err) {
done(err);
}
});
oAuth.generateAccessToken(req, res, next);
});
it('Can\'t generate access token without client.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'password';
req.body.username = 'username';
req.body.password = 'password';
res.setHeader = {};
res.end = {};
oAuth.generateAccessToken(req, res, function (err) {
err.errorType.should.eql('UnauthorizedError');
done();
});
});
it('Can\'t generate access token without username.', function (done) {
req.body = {};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'password';
req.body.password = 'password';
res.setHeader = {};
res.end = {};
oAuth.generateAccessToken(req, res, function (err) {
err.errorType.should.eql('BadRequestError');
done();
});
});
it('Can\'t generate access token without password.', function (done) {
req.body = {};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'password';
req.body.username = 'username';
res.setHeader = {};
res.end = {};
oAuth.generateAccessToken(req, res, function (err) {
err.errorType.should.eql('BadRequestError');
done();
});
});
it('Handles database error.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'password';
req.body.username = 'username';
req.body.password = 'password';
req.client = {
id: 1
};
res.setHeader = {};
res.end = {};
sinon.stub(models.User, 'check')
.withArgs({email: 'username', password: 'password'}).returns(new Promise.resolve({
id: 1
}));
sinon.stub(authUtils, 'createTokens')
.returns(new Promise.reject({
message: 'DB error'
}));
oAuth.generateAccessToken(req, res, function (err) {
err.message.should.eql('DB error');
done();
});
});
});
describe('Generate Token from Refreshtoken', function () {
beforeEach(function () {
sinon.stub(models.Accesstoken, 'destroyAllExpired')
.returns(new Promise.resolve());
sinon.stub(models.Refreshtoken, 'destroyAllExpired')
.returns(new Promise.resolve());
oAuth.init();
});
it('Successfully generate access token.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.authInfo = {ip: '127.0.0.1'};
req.connection = {remoteAddress: '127.0.0.1'};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = function () {
};
res.end = function () {
};
sinon.stub(models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve({
toJSON: function () {
return {
expires: Date.now() + 3600
};
}
}));
sinon.stub(authUtils, 'createTokens')
.returns(new Promise.resolve({
access_token: 'AT',
refresh_token: 'RT',
expires_in: Date.now() + 1000
}));
sinon.stub(res, 'setHeader').callsFake(function () {
});
sinon.stub(res, 'end').callsFake(function (json) {
try {
should.exist(json);
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.generateAccessToken(req, res, next);
});
it('Can\'t generate access token without valid refresh token.', function (done) {
req.body = {};
req.client = {
slug: 'test'
};
req.connection = {remoteAddress: '127.0.0.1'};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = {};
res.end = {};
sinon.stub(models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve());
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.connection = {remoteAddress: '127.0.0.1'};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = {};
res.end = {};
sinon.stub(models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve({
toJSON: function () {
return {
expires: Date.now() - 3600
};
}
}));
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.connection = {remoteAddress: '127.0.0.1'};
req.authInfo = {ip: '127.0.0.1'};
req.body.grant_type = 'refresh_token';
req.body.refresh_token = 'token';
res.setHeader = {};
res.end = {};
sinon.stub(models.Refreshtoken, 'findOne')
.withArgs({token: 'token'}).returns(new Promise.resolve({
toJSON: function () {
return {
expires: Date.now() + 3600
};
}
}));
sinon.stub(authUtils, 'createTokens').callsFake(function () {
return Promise.reject(new Error('DB error'));
});
oAuth.generateAccessToken(req, res, function (err) {
err.stack.should.containEql('DB error');
done();
});
});
});
describe('Generate Token from Authorization Code', function () {
beforeEach(function () {
sinon.stub(models.Accesstoken, 'destroyAllExpired')
.returns(new Promise.resolve());
sinon.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.authInfo = {ip: '127.0.0.1'};
req.connection = {remoteAddress: '127.0.0.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();
};
sinon.stub(authUtils, 'createTokens')
.returns(new Promise.resolve({
access_token: 'access-token',
refresh_token: 'refresh-token',
expires_in: 10
}));
sinon.stub(passport, 'authenticate').callsFake(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.authInfo = {ip: '127.0.0.1'};
req.connection = {remoteAddress: '127.0.0.1'};
req.body.grant_type = 'authorization_code';
req.body.authorizationCode = '1234';
sinon.stub(passport, 'authenticate').callsFake(function (name, options, onSuccess) {
return function () {
onSuccess(new common.errors.UnauthorizedError());
};
});
oAuth.generateAccessToken(req, res, function (err) {
should.exist(err);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
});
});
it('Error: no authorization_code provided', function (done) {
req.body = {};
req.query = {};
req.client = {
id: 1
};
req.connection = {remoteAddress: '127.0.0.1'};
req.body.grant_type = 'authorization_code';
oAuth.generateAccessToken(req, res, function (err) {
should.exist(err);
(err instanceof common.errors.UnauthorizedError).should.eql(true);
done();
});
});
});
});

View File

@ -1,22 +0,0 @@
var should = require('should'),
sinon = require('sinon'),
passport = require('passport'),
ghostPassport = require('../../../../server/services/auth/passport');
describe('Ghost Passport', function () {
beforeEach(function () {
sinon.spy(passport, 'use');
});
afterEach(function () {
sinon.restore();
});
describe('[default] local auth', function () {
it('initialise passport with passport auth type', function () {
var response = ghostPassport.init();
should.exist(response);
passport.use.callCount.should.eql(2);
});
});
});