diff --git a/core/server/index.js b/core/server/index.js index 69c214f45e..574373122c 100644 --- a/core/server/index.js +++ b/core/server/index.js @@ -25,7 +25,6 @@ function initialiseServices() { routing.bootstrap.start(); const permissions = require('./services/permissions'), - auth = require('./services/auth'), apps = require('./services/apps'), xmlrpc = require('./services/xmlrpc'), slack = require('./services/slack'), @@ -58,9 +57,6 @@ function initialiseServices() { require('./analytics-events').init(); } }).then(function () { - parentApp.use(auth.init()); - debug('Auth done'); - debug('...`initialiseServices` End'); }); } diff --git a/core/server/services/auth/auth-strategies.js b/core/server/services/auth/auth-strategies.js deleted file mode 100644 index 0ed2489908..0000000000 --- a/core/server/services/auth/auth-strategies.js +++ /dev/null @@ -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; diff --git a/core/server/services/auth/authenticate.js b/core/server/services/auth/authenticate.js index 0b12c6a7cc..ab5f211199 100644 --- a/core/server/services/auth/authenticate.js +++ b/core/server/services/auth/authenticate.js @@ -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 apiKeyAuth = require('./api-key'); const members = require('./members'); 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], authenticateContentApi: [apiKeyAuth.content.authenticateContentApiKey, members.authenticateMembersToken] diff --git a/core/server/services/auth/authorize.js b/core/server/services/auth/authorize.js index 6d05ecc171..a912d07ab2 100644 --- a/core/server/services/auth/authorize.js +++ b/core/server/services/auth/authorize.js @@ -2,55 +2,6 @@ const labs = require('../labs'); const common = require('../../lib/common'); 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) { const hasApiKey = req.api_key && req.api_key.id; const hasMember = req.member; diff --git a/core/server/services/auth/index.js b/core/server/services/auth/index.js index 43d1f11a2f..aa6e21747c 100644 --- a/core/server/services/auth/index.js +++ b/core/server/services/auth/index.js @@ -17,18 +17,5 @@ module.exports = { get 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'); } }; diff --git a/core/server/services/auth/oauth.js b/core/server/services/auth/oauth.js deleted file mode 100644 index 2786af7ecb..0000000000 --- a/core/server/services/auth/oauth.js +++ /dev/null @@ -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; diff --git a/core/server/services/auth/passport.js b/core/server/services/auth/passport.js deleted file mode 100644 index b20c70b393..0000000000 --- a/core/server/services/auth/passport.js +++ /dev/null @@ -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(); -}; diff --git a/core/server/services/auth/utils.js b/core/server/services/auth/utils.js deleted file mode 100644 index 7157778285..0000000000 --- a/core/server/services/auth/utils.js +++ /dev/null @@ -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; -}; diff --git a/core/test/unit/services/auth/auth-strategies_spec.js b/core/test/unit/services/auth/auth-strategies_spec.js deleted file mode 100644 index 5ea08fcedc..0000000000 --- a/core/test/unit/services/auth/auth-strategies_spec.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/core/test/unit/services/auth/authenticate_spec.js b/core/test/unit/services/auth/authenticate_spec.js deleted file mode 100644 index 5d67c0430d..0000000000 --- a/core/test/unit/services/auth/authenticate_spec.js +++ /dev/null @@ -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(); - }); - }); -}); diff --git a/core/test/unit/services/auth/oauth_spec.js b/core/test/unit/services/auth/oauth_spec.js deleted file mode 100644 index e147f1083e..0000000000 --- a/core/test/unit/services/auth/oauth_spec.js +++ /dev/null @@ -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(); - }); - }); - }); -}); diff --git a/core/test/unit/services/auth/passport_spec.js b/core/test/unit/services/auth/passport_spec.js deleted file mode 100644 index 769c334d46..0000000000 --- a/core/test/unit/services/auth/passport_spec.js +++ /dev/null @@ -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); - }); - }); -});