mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
✨ Ghost OAuth (#7451)
issue #7452 Remote oauth2 authentication with Ghost.org. This PR supports: - oauth2 login or local login - authentication on blog setup - authentication on invite - normal authentication - does not contain many, many tests, but we'll improve in the next alpha weeks
This commit is contained in:
parent
3e727d01f7
commit
6473c9e858
@ -129,6 +129,37 @@ function setupTasks(setupData) {
|
||||
* **See:** [API Methods](index.js.html#api%20methods)
|
||||
*/
|
||||
authentication = {
|
||||
/**
|
||||
* Generate a pair of tokens
|
||||
*/
|
||||
createTokens: function createTokens(data, options) {
|
||||
var localAccessToken = globalUtils.uid(191),
|
||||
localRefreshToken = globalUtils.uid(191),
|
||||
accessExpires = Date.now() + globalUtils.ONE_HOUR_MS,
|
||||
refreshExpires = Date.now() + globalUtils.ONE_WEEK_MS,
|
||||
client = options.context.client_id,
|
||||
user = options.context.user;
|
||||
|
||||
return models.Accesstoken.add({
|
||||
token: localAccessToken,
|
||||
user_id: user,
|
||||
client_id: client,
|
||||
expires: accessExpires
|
||||
}).then(function () {
|
||||
return models.Refreshtoken.add({
|
||||
token: localRefreshToken,
|
||||
user_id: user,
|
||||
client_id: client,
|
||||
expires: refreshExpires
|
||||
});
|
||||
}).then(function () {
|
||||
return {
|
||||
access_token: localAccessToken,
|
||||
refresh_token: localRefreshToken,
|
||||
expires_in: accessExpires
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @description generate a reset token for a given email address
|
||||
@ -364,22 +395,23 @@ authentication = {
|
||||
|
||||
function checkInvitation(email) {
|
||||
return models.Invite
|
||||
.where({email: email, status: 'sent'})
|
||||
.count('id')
|
||||
.then(function then(count) {
|
||||
return !!count;
|
||||
});
|
||||
}
|
||||
.findOne({email: email, status: 'sent'}, options)
|
||||
.then(function fetchedInvite(invite) {
|
||||
if (!invite) {
|
||||
return {invitation: [{valid: false}]};
|
||||
}
|
||||
|
||||
function formatResponse(isInvited) {
|
||||
return {invitation: [{valid: isInvited}]};
|
||||
return models.User.findOne({id: invite.get('created_by')})
|
||||
.then(function fetchedUser(user) {
|
||||
return {invitation: [{valid: true, invitedBy: user.get('name')}]};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tasks = [
|
||||
processArgs,
|
||||
assertSetupCompleted(true),
|
||||
checkInvitation,
|
||||
formatResponse
|
||||
checkInvitation
|
||||
];
|
||||
|
||||
return pipeline(tasks, localOptions);
|
||||
|
@ -229,8 +229,10 @@ http = function http(apiMethod) {
|
||||
var object = req.body,
|
||||
options = _.extend({}, req.file, req.query, req.params, {
|
||||
context: {
|
||||
// @TODO: forward the client and user obj in 1.0 (options.context.user.id)
|
||||
user: ((req.user && req.user.id) || (req.user && req.user.id === 0)) ? req.user.id : null,
|
||||
client: (req.client && req.client.slug) ? req.client.slug : null
|
||||
client: (req.client && req.client.slug) ? req.client.slug : null,
|
||||
client_id: (req.client && req.client.id) ? req.client.id : null
|
||||
}
|
||||
});
|
||||
|
||||
|
149
core/server/auth/auth-strategies.js
Normal file
149
core/server/auth/auth-strategies.js
Normal file
@ -0,0 +1,149 @@
|
||||
var models = require('../models'),
|
||||
utils = require('../utils'),
|
||||
i18n = require('../i18n'),
|
||||
errors = require('../errors'),
|
||||
_ = require('lodash'),
|
||||
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({include: ['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) {
|
||||
var user = model.toJSON(),
|
||||
info = {scope: '*'};
|
||||
return done(null, {id: user.id}, info);
|
||||
}
|
||||
return done(null, false);
|
||||
});
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Ghost Strategy
|
||||
* patronusRefreshToken: will be null for now, because we don't need it right now
|
||||
*
|
||||
* CASES:
|
||||
* - via invite token
|
||||
* - via normal auth
|
||||
* - via setup
|
||||
*
|
||||
* @TODO: validate patronus profile?
|
||||
*/
|
||||
ghostStrategy: function ghostStrategy(req, patronusAccessToken, patronusRefreshToken, profile, done) {
|
||||
var inviteToken = req.body.inviteToken,
|
||||
options = {context: {internal: true}},
|
||||
handleInviteToken, handleSetup;
|
||||
|
||||
handleInviteToken = function handleInviteToken() {
|
||||
var user, invite;
|
||||
inviteToken = utils.decodeBase64URLsafe(inviteToken);
|
||||
|
||||
return models.Invite.findOne({token: inviteToken}, options)
|
||||
.then(function addInviteUser(_invite) {
|
||||
invite = _invite;
|
||||
|
||||
if (!invite) {
|
||||
throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound'));
|
||||
}
|
||||
|
||||
if (invite.get('expires') < Date.now()) {
|
||||
throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteExpired'));
|
||||
}
|
||||
|
||||
return models.User.add({
|
||||
email: profile.email_address,
|
||||
name: profile.email_address,
|
||||
password: utils.uid(50),
|
||||
roles: invite.toJSON().roles
|
||||
}, options);
|
||||
})
|
||||
.then(function destroyInvite(_user) {
|
||||
user = _user;
|
||||
return invite.destroy(options);
|
||||
})
|
||||
.then(function () {
|
||||
return user;
|
||||
});
|
||||
};
|
||||
|
||||
handleSetup = function handleSetup() {
|
||||
return models.User.findOne({slug: 'ghost-owner', status: 'all'}, options)
|
||||
.then(function fetchedOwner(owner) {
|
||||
if (!owner) {
|
||||
throw new errors.NotFoundError(i18n.t('errors.models.user.userNotFound'));
|
||||
}
|
||||
|
||||
return models.User.edit({
|
||||
email: profile.email_address,
|
||||
status: 'active'
|
||||
}, _.merge({id: owner.id}, options));
|
||||
});
|
||||
};
|
||||
|
||||
models.User.getByEmail(profile.email_address, options)
|
||||
.then(function fetchedUser(user) {
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
if (inviteToken) {
|
||||
return handleInviteToken();
|
||||
}
|
||||
|
||||
return handleSetup();
|
||||
})
|
||||
.then(function updatePatronusToken(user) {
|
||||
options.id = user.id;
|
||||
return models.User.edit({patronus_access_token: patronusAccessToken}, options);
|
||||
})
|
||||
.then(function returnResponse(user) {
|
||||
done(null, user, profile);
|
||||
})
|
||||
.catch(done);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = strategies;
|
@ -1,10 +1,8 @@
|
||||
var passport = require('passport'),
|
||||
errors = require('../errors'),
|
||||
events = require('../events'),
|
||||
labs = require('../utils/labs'),
|
||||
i18n = require('../i18n'),
|
||||
|
||||
auth;
|
||||
var passport = require('passport'),
|
||||
errors = require('../errors'),
|
||||
events = require('../events'),
|
||||
i18n = require('../i18n'),
|
||||
authenticate;
|
||||
|
||||
function isBearerAutorizationHeader(req) {
|
||||
var parts,
|
||||
@ -29,8 +27,7 @@ function isBearerAutorizationHeader(req) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auth = {
|
||||
|
||||
authenticate = {
|
||||
// ### Authenticate Client Middleware
|
||||
authenticateClient: function authenticateClient(req, res, next) {
|
||||
// skip client authentication if bearer token is present
|
||||
@ -108,28 +105,28 @@ auth = {
|
||||
)(req, res, next);
|
||||
},
|
||||
|
||||
// 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 errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
||||
}
|
||||
},
|
||||
// ### Authenticate Ghost.org User
|
||||
authenticateGhostUser: function authenticateGhostUser(req, res, next) {
|
||||
req.query.code = req.body.authorizationCode;
|
||||
|
||||
// ### 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 {
|
||||
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
||||
}
|
||||
if (!req.query.code) {
|
||||
return errors.handleAPIError(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied')), req, res, next);
|
||||
}
|
||||
|
||||
passport.authenticate('ghost', {session: false, failWithError: false}, function authenticate(err, user, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return errors.handleAPIError(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied')), req, res, next);
|
||||
}
|
||||
|
||||
req.authInfo = info;
|
||||
req.user = user;
|
||||
next();
|
||||
})(req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = auth;
|
||||
module.exports = authenticate;
|
31
core/server/auth/authorize.js
Normal file
31
core/server/auth/authorize.js
Normal file
@ -0,0 +1,31 @@
|
||||
var errors = require('../errors'),
|
||||
labs = require('../utils/labs'),
|
||||
i18n = require('../i18n'),
|
||||
authorize;
|
||||
|
||||
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 errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
||||
}
|
||||
},
|
||||
|
||||
// ### 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 {
|
||||
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = authorize;
|
17
core/server/auth/index.js
Normal file
17
core/server/auth/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
var passport = require('./passport'),
|
||||
authorize = require('./authorize'),
|
||||
authenticate = require('./authenticate'),
|
||||
oauth = require('./oauth');
|
||||
|
||||
exports.init = function (options) {
|
||||
oauth.init();
|
||||
|
||||
return passport.init(options)
|
||||
.then(function (response) {
|
||||
return {auth: response.passport};
|
||||
});
|
||||
};
|
||||
|
||||
exports.oauth = oauth;
|
||||
exports.authorize = authorize;
|
||||
exports.authenticate = authenticate;
|
97
core/server/auth/oauth.js
Normal file
97
core/server/auth/oauth.js
Normal file
@ -0,0 +1,97 @@
|
||||
var oauth2orize = require('oauth2orize'),
|
||||
models = require('../models'),
|
||||
utils = require('../utils'),
|
||||
errors = require('../errors'),
|
||||
authenticationAPI = require('../api/authentication'),
|
||||
spamPrevention = require('../middleware/spam-prevention'),
|
||||
i18n = require('../i18n'),
|
||||
oauthServer,
|
||||
oauth;
|
||||
|
||||
function exchangeRefreshToken(client, refreshToken, scope, done) {
|
||||
models.Refreshtoken.findOne({token: refreshToken})
|
||||
.then(function then(model) {
|
||||
if (!model) {
|
||||
return done(new errors.NoPermissionError(i18n.t('errors.middleware.oauth.invalidRefreshToken')), false);
|
||||
} else {
|
||||
var token = model.toJSON(),
|
||||
accessToken = utils.uid(191),
|
||||
accessExpires = Date.now() + utils.ONE_HOUR_MS,
|
||||
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
|
||||
|
||||
if (token.expires > Date.now()) {
|
||||
models.Accesstoken.add({
|
||||
token: accessToken,
|
||||
user_id: token.user_id,
|
||||
client_id: token.client_id,
|
||||
expires: accessExpires
|
||||
}).then(function then() {
|
||||
return models.Refreshtoken.edit({expires: refreshExpires}, {id: token.id});
|
||||
}).then(function then() {
|
||||
return done(null, accessToken, {expires_in: utils.ONE_HOUR_S});
|
||||
}).catch(function handleError(error) {
|
||||
return done(error, false);
|
||||
});
|
||||
} else {
|
||||
done(new errors.UnauthorizedError(i18n.t('errors.middleware.oauth.refreshTokenExpired')), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exchangePassword(client, username, password, scope, done) {
|
||||
// Validate the client
|
||||
models.Client.findOne({slug: client.slug})
|
||||
.then(function then(client) {
|
||||
if (!client) {
|
||||
return done(new errors.NoPermissionError(i18n.t('errors.middleware.oauth.invalidClient')), false);
|
||||
}
|
||||
|
||||
// Validate the user
|
||||
return models.User.check({email: username, password: password})
|
||||
.then(function then(user) {
|
||||
return authenticationAPI.createTokens({}, {context: {client_id: client.id, user: user.id}});
|
||||
})
|
||||
.then(function then(response) {
|
||||
spamPrevention.resetCounter(username);
|
||||
return done(null, response.access_token, response.refresh_token, {expires_in: response.expires_in});
|
||||
});
|
||||
})
|
||||
.catch(function handleError(error) {
|
||||
return done(error, false);
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
// ### Generate access token Middleware
|
||||
// register the oauth2orize middleware for password and refresh token grants
|
||||
generateAccessToken: function generateAccessToken(req, res, next) {
|
||||
return oauthServer.token()(req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = oauth;
|
103
core/server/auth/passport.js
Normal file
103
core/server/auth/passport.js
Normal file
@ -0,0 +1,103 @@
|
||||
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
GhostOAuth2Strategy = require('passport-ghost').Strategy,
|
||||
passport = require('passport'),
|
||||
Promise = require('bluebird'),
|
||||
authStrategies = require('./auth-strategies'),
|
||||
utils = require('../utils'),
|
||||
errors = require('../errors'),
|
||||
models = require('../models'),
|
||||
_private = {};
|
||||
|
||||
/**
|
||||
* Public client registration at Ghost.org
|
||||
*/
|
||||
_private.registerClient = function registerClient(options) {
|
||||
var ghostOAuth2Strategy = options.ghostOAuth2Strategy,
|
||||
url = options.url;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var retry = function retry(retryCount, done) {
|
||||
models.Client.findOne({name: 'Ghost Patronus'}, {context: {internal: true}})
|
||||
.then(function (client) {
|
||||
// CASE: patronus client is already registered
|
||||
if (client) {
|
||||
return done(null, {
|
||||
client_id: client.get('uuid'),
|
||||
client_secret: client.get('secret')
|
||||
});
|
||||
}
|
||||
|
||||
return ghostOAuth2Strategy.registerClient({clientName: url})
|
||||
.then(function addClient(credentials) {
|
||||
return models.Client.add({
|
||||
name: 'Ghost Patronus',
|
||||
slug: 'patronus',
|
||||
uuid: credentials.client_id,
|
||||
secret: credentials.client_secret
|
||||
}, {context: {internal: true}});
|
||||
})
|
||||
.then(function returnClient(client) {
|
||||
return done(null, {
|
||||
client_id: client.get('uuid'),
|
||||
client_secret: client.get('secret')
|
||||
});
|
||||
})
|
||||
.catch(function publicClientRegistrationError(err) {
|
||||
if (retryCount < 0) {
|
||||
return done(new errors.IncorrectUsage(
|
||||
'Public client registration failed: ' + err.code || err.message,
|
||||
'Please verify that the url is reachable: ' + ghostOAuth2Strategy.url
|
||||
));
|
||||
}
|
||||
|
||||
console.log('RETRY: Public Client Registration...');
|
||||
var timeout = setTimeout(function () {
|
||||
clearTimeout(timeout);
|
||||
retryCount = retryCount - 1;
|
||||
retry(retryCount, done);
|
||||
}, 3000);
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
};
|
||||
|
||||
retry(10, function retryPublicClientRegistration(err, client) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(client);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.init = function initPassport(options) {
|
||||
var type = options.type,
|
||||
url = options.url;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
passport.use(new ClientPasswordStrategy(authStrategies.clientPasswordStrategy));
|
||||
passport.use(new BearerStrategy(authStrategies.bearerStrategy));
|
||||
|
||||
if (type !== 'patronus') {
|
||||
return resolve({passport: passport.initialize()});
|
||||
}
|
||||
|
||||
var ghostOAuth2Strategy = new GhostOAuth2Strategy({
|
||||
callbackURL: utils.url.getBaseUrl() + '/ghost/',
|
||||
url: url,
|
||||
passReqToCallback: true
|
||||
}, authStrategies.ghostStrategy);
|
||||
|
||||
_private.registerClient({ghostOAuth2Strategy: ghostOAuth2Strategy, url: utils.url.getBaseUrl()})
|
||||
.then(function setClient(client) {
|
||||
console.log('SUCCESS: Public Client Registration');
|
||||
|
||||
ghostOAuth2Strategy.setClient(client);
|
||||
passport.use(ghostOAuth2Strategy);
|
||||
return resolve({passport: passport.initialize()});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
@ -24,5 +24,8 @@
|
||||
},
|
||||
"scheduling": {
|
||||
"active": "SchedulingDefault"
|
||||
},
|
||||
"auth": {
|
||||
"type": "password"
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,9 @@
|
||||
},
|
||||
"paths": {
|
||||
"contentPath": "content/"
|
||||
},
|
||||
"auth": {
|
||||
"type": "patronus",
|
||||
"url": "http://devauth.ghost.org:8080"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
api = require('../api'),
|
||||
config = require('../config'),
|
||||
errors = require('../errors'),
|
||||
updateCheck = require('../update-check'),
|
||||
i18n = require('../i18n'),
|
||||
@ -17,7 +18,12 @@ adminControllers = {
|
||||
var configuration,
|
||||
fetch = {
|
||||
configuration: api.configuration.read().then(function (res) { return res.configuration[0]; }),
|
||||
client: api.clients.read({slug: 'ghost-admin'}).then(function (res) { return res.clients[0]; })
|
||||
client: api.clients.read({slug: 'ghost-admin'}).then(function (res) { return res.clients[0]; }),
|
||||
patronus: api.clients.read({slug: 'patronus'})
|
||||
.then(function (res) { return res.clients[0]; })
|
||||
.catch(function () {
|
||||
return;
|
||||
})
|
||||
};
|
||||
|
||||
return Promise.props(fetch).then(function renderIndex(result) {
|
||||
@ -26,6 +32,10 @@ adminControllers = {
|
||||
configuration.clientId = {value: result.client.slug, type: 'string'};
|
||||
configuration.clientSecret = {value: result.client.secret, type: 'string'};
|
||||
|
||||
if (result.patronus && config.get('auth:type') === 'patronus') {
|
||||
configuration.ghostAuthId = {value: result.patronus.uuid, type: 'string'};
|
||||
}
|
||||
|
||||
res.render('default', {
|
||||
configuration: configuration
|
||||
});
|
||||
|
@ -29,6 +29,7 @@ module.exports = {
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {isUUID: true}},
|
||||
name: {type: 'string', maxlength: 150, nullable: false},
|
||||
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
|
||||
patronus_access_token: {type: 'string', nullable: true},
|
||||
password: {type: 'string', maxlength: 60, nullable: false},
|
||||
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
|
||||
image: {type: 'text', maxlength: 2000, nullable: true},
|
||||
|
@ -25,6 +25,7 @@ var express = require('express'),
|
||||
models = require('./models'),
|
||||
permissions = require('./permissions'),
|
||||
apps = require('./apps'),
|
||||
auth = require('./auth'),
|
||||
xmlrpc = require('./data/xml/xmlrpc'),
|
||||
slack = require('./data/slack'),
|
||||
GhostServer = require('./ghost-server'),
|
||||
@ -61,7 +62,7 @@ function initDbHashAndFirstRun() {
|
||||
function init(options) {
|
||||
options = options || {};
|
||||
|
||||
var ghostServer = null;
|
||||
var ghostServer, parentApp;
|
||||
|
||||
// ### Initialisation
|
||||
// The server and its dependencies require a populated config
|
||||
@ -101,7 +102,7 @@ function init(options) {
|
||||
);
|
||||
}).then(function () {
|
||||
// Get reference to an express app instance.
|
||||
var parentApp = express();
|
||||
parentApp = express();
|
||||
|
||||
// ## Middleware and Routing
|
||||
middleware(parentApp);
|
||||
@ -119,6 +120,11 @@ function init(options) {
|
||||
});
|
||||
});
|
||||
|
||||
return auth.init(config.get('auth'))
|
||||
.then(function (response) {
|
||||
parentApp.use(response.auth);
|
||||
});
|
||||
}).then(function () {
|
||||
return new GhostServer(parentApp);
|
||||
}).then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
|
@ -1,61 +0,0 @@
|
||||
var models = require('../models'),
|
||||
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({include: ['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) {
|
||||
var user = model.toJSON(),
|
||||
info = {scope: '*'};
|
||||
return done(null, {id: user.id}, info);
|
||||
}
|
||||
return done(null, false);
|
||||
});
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = strategies;
|
@ -10,17 +10,13 @@ var bodyParser = require('body-parser'),
|
||||
serveStatic = require('express').static,
|
||||
slashes = require('connect-slashes'),
|
||||
storage = require('../storage'),
|
||||
passport = require('passport'),
|
||||
utils = require('../utils'),
|
||||
sitemapHandler = require('../data/xml/sitemap/handler'),
|
||||
multer = require('multer'),
|
||||
tmpdir = require('os').tmpdir,
|
||||
authStrategies = require('./auth-strategies'),
|
||||
auth = require('./auth'),
|
||||
cacheControl = require('./cache-control'),
|
||||
checkSSL = require('./check-ssl'),
|
||||
decideIsAdmin = require('./decide-is-admin'),
|
||||
oauth = require('./oauth'),
|
||||
redirectToSetup = require('./redirect-to-setup'),
|
||||
serveSharedFile = require('./serve-shared-file'),
|
||||
spamPrevention = require('./spam-prevention'),
|
||||
@ -34,10 +30,6 @@ var bodyParser = require('body-parser'),
|
||||
netjet = require('netjet'),
|
||||
labs = require('./labs'),
|
||||
helpers = require('../helpers'),
|
||||
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
|
||||
middleware,
|
||||
setupMiddleware;
|
||||
|
||||
@ -46,12 +38,7 @@ middleware = {
|
||||
validation: validation,
|
||||
cacheControl: cacheControl,
|
||||
spamPrevention: spamPrevention,
|
||||
oauth: oauth,
|
||||
api: {
|
||||
authenticateClient: auth.authenticateClient,
|
||||
authenticateUser: auth.authenticateUser,
|
||||
requiresAuthorizedUser: auth.requiresAuthorizedUser,
|
||||
requiresAuthorizedUserPublicAPI: auth.requiresAuthorizedUserPublicAPI,
|
||||
errorHandler: errors.handleAPIError,
|
||||
cors: cors,
|
||||
labs: labs,
|
||||
@ -84,11 +71,6 @@ setupMiddleware = function setupMiddleware(blogApp) {
|
||||
// Load helpers
|
||||
helpers.loadCoreHelpers(adminHbs);
|
||||
|
||||
// Initialize Auth Handlers & OAuth middleware
|
||||
passport.use(new ClientPasswordStrategy(authStrategies.clientPasswordStrategy));
|
||||
passport.use(new BearerStrategy(authStrategies.bearerStrategy));
|
||||
oauth.init();
|
||||
|
||||
// Make sure 'req.secure' is valid for proxied requests
|
||||
// (X-Forwarded-Proto header will be checked, if present)
|
||||
blogApp.enable('trust proxy');
|
||||
@ -180,8 +162,6 @@ setupMiddleware = function setupMiddleware(blogApp) {
|
||||
blogApp.use(bodyParser.json({limit: '1mb'}));
|
||||
blogApp.use(bodyParser.urlencoded({extended: true, limit: '1mb'}));
|
||||
|
||||
blogApp.use(passport.initialize());
|
||||
|
||||
// ### Caching
|
||||
// Blog frontend is cacheable
|
||||
blogApp.use(cacheControl('public'));
|
||||
|
@ -1,103 +0,0 @@
|
||||
var oauth2orize = require('oauth2orize'),
|
||||
models = require('../models'),
|
||||
utils = require('../utils'),
|
||||
errors = require('../errors'),
|
||||
spamPrevention = require('./spam-prevention'),
|
||||
i18n = require('../i18n'),
|
||||
|
||||
oauthServer,
|
||||
oauth;
|
||||
|
||||
function exchangeRefreshToken(client, refreshToken, scope, done) {
|
||||
models.Refreshtoken.findOne({token: refreshToken}).then(function then(model) {
|
||||
if (!model) {
|
||||
return done(new errors.NoPermissionError(i18n.t('errors.middleware.oauth.invalidRefreshToken')), false);
|
||||
} else {
|
||||
var token = model.toJSON(),
|
||||
accessToken = utils.uid(191),
|
||||
accessExpires = Date.now() + utils.ONE_HOUR_MS,
|
||||
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
|
||||
|
||||
if (token.expires > Date.now()) {
|
||||
models.Accesstoken.add({
|
||||
token: accessToken,
|
||||
user_id: token.user_id,
|
||||
client_id: token.client_id,
|
||||
expires: accessExpires
|
||||
}).then(function then() {
|
||||
return models.Refreshtoken.edit({expires: refreshExpires}, {id: token.id});
|
||||
}).then(function then() {
|
||||
return done(null, accessToken, {expires_in: utils.ONE_HOUR_S});
|
||||
}).catch(function handleError(error) {
|
||||
return done(error, false);
|
||||
});
|
||||
} else {
|
||||
done(new errors.UnauthorizedError(i18n.t('errors.middleware.oauth.refreshTokenExpired')), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exchangePassword(client, username, password, scope, done) {
|
||||
// Validate the client
|
||||
models.Client.findOne({slug: client.slug}).then(function then(client) {
|
||||
if (!client) {
|
||||
return done(new errors.NoPermissionError(i18n.t('errors.middleware.oauth.invalidClient')), false);
|
||||
}
|
||||
// Validate the user
|
||||
return models.User.check({email: username, password: password}).then(function then(user) {
|
||||
// Everything validated, return the access- and refreshtoken
|
||||
var accessToken = utils.uid(191),
|
||||
refreshToken = utils.uid(191),
|
||||
accessExpires = Date.now() + utils.ONE_HOUR_MS,
|
||||
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
|
||||
|
||||
return models.Accesstoken.add(
|
||||
{token: accessToken, user_id: user.id, client_id: client.id, expires: accessExpires}
|
||||
).then(function then() {
|
||||
return models.Refreshtoken.add(
|
||||
{token: refreshToken, user_id: user.id, client_id: client.id, expires: refreshExpires}
|
||||
);
|
||||
}).then(function then() {
|
||||
spamPrevention.resetCounter(username);
|
||||
return done(null, accessToken, refreshToken, {expires_in: utils.ONE_HOUR_S});
|
||||
});
|
||||
}).catch(function handleError(error) {
|
||||
return done(error, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
oauth = {
|
||||
|
||||
init: function init() {
|
||||
oauthServer = 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));
|
||||
},
|
||||
|
||||
// ### Generate access token Middleware
|
||||
// register the oauth2orize middleware for password and refresh token grants
|
||||
generateAccessToken: function generateAccessToken(req, res, next) {
|
||||
return oauthServer.token()(req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = oauth;
|
@ -3,8 +3,11 @@ var api = require('../api'),
|
||||
|
||||
// Redirect to setup if no user exists
|
||||
function redirectToSetup(req, res, next) {
|
||||
var isSetupRequest = req.path.match(/\/setup\//),
|
||||
isOauthAuthorization = req.path.match(/\/$/) && req.query && (req.query.code || req.query.error);
|
||||
|
||||
api.authentication.isSetup().then(function then(exists) {
|
||||
if (!exists.setup[0].status && !req.path.match(/\/setup\//)) {
|
||||
if (!exists.setup[0].status && !isSetupRequest && !isOauthAuthorization) {
|
||||
return res.redirect(utils.url.getSubdir() + '/ghost/setup/');
|
||||
}
|
||||
next();
|
||||
|
@ -131,6 +131,7 @@ User = ghostBookshelf.Model.extend({
|
||||
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
||||
// remove password hash for security reasons
|
||||
delete attrs.password;
|
||||
delete attrs.patronus_access_token;
|
||||
|
||||
if (!options || !options.context || (!options.context.user && !options.context.internal)) {
|
||||
delete attrs.email;
|
||||
|
@ -1,22 +1,23 @@
|
||||
// # API routes
|
||||
var express = require('express'),
|
||||
api = require('../api'),
|
||||
var express = require('express'),
|
||||
api = require('../api'),
|
||||
auth = require('../auth'),
|
||||
apiRoutes;
|
||||
|
||||
apiRoutes = function apiRoutes(middleware) {
|
||||
var router = express.Router(),
|
||||
// Authentication for public endpoints
|
||||
authenticatePublic = [
|
||||
middleware.api.authenticateClient,
|
||||
middleware.api.authenticateUser,
|
||||
middleware.api.requiresAuthorizedUserPublicAPI,
|
||||
auth.authenticate.authenticateClient,
|
||||
auth.authenticate.authenticateUser,
|
||||
auth.authorize.requiresAuthorizedUserPublicAPI,
|
||||
middleware.api.cors
|
||||
],
|
||||
// Require user for private endpoints
|
||||
authenticatePrivate = [
|
||||
middleware.api.authenticateClient,
|
||||
middleware.api.authenticateUser,
|
||||
middleware.api.requiresAuthorizedUser,
|
||||
auth.authenticate.authenticateClient,
|
||||
auth.authenticate.authenticateUser,
|
||||
auth.authorize.requiresAuthorizedUser,
|
||||
middleware.api.cors
|
||||
];
|
||||
|
||||
@ -48,7 +49,10 @@ apiRoutes = function apiRoutes(middleware) {
|
||||
router.del('/posts/:id', authenticatePrivate, api.http(api.posts.destroy));
|
||||
|
||||
// ## Schedules
|
||||
router.put('/schedules/posts/:id', [middleware.api.authenticateClient, middleware.api.authenticateUser], api.http(api.schedules.publishPost));
|
||||
router.put('/schedules/posts/:id', [
|
||||
auth.authenticate.authenticateClient,
|
||||
auth.authenticate.authenticateUser
|
||||
], api.http(api.schedules.publishPost));
|
||||
|
||||
// ## Settings
|
||||
router.get('/settings', authenticatePrivate, api.http(api.settings.browse));
|
||||
@ -57,13 +61,15 @@ apiRoutes = function apiRoutes(middleware) {
|
||||
|
||||
// ## Users
|
||||
router.get('/users', authenticatePublic, api.http(api.users.browse));
|
||||
|
||||
router.get('/users/:id', authenticatePublic, api.http(api.users.read));
|
||||
router.get('/users/slug/:slug', authenticatePublic, api.http(api.users.read));
|
||||
router.get('/users/email/:email', authenticatePublic, api.http(api.users.read));
|
||||
|
||||
router.put('/users/password', authenticatePrivate, api.http(api.users.changePassword));
|
||||
router.put('/users/owner', authenticatePrivate, api.http(api.users.transferOwnership));
|
||||
router.put('/users/:id', authenticatePrivate, api.http(api.users.edit));
|
||||
|
||||
router.post('/users', authenticatePrivate, api.http(api.users.add));
|
||||
router.del('/users/:id', authenticatePrivate, api.http(api.users.destroy));
|
||||
|
||||
// ## Tags
|
||||
@ -87,7 +93,7 @@ apiRoutes = function apiRoutes(middleware) {
|
||||
router.get('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.read));
|
||||
router.post('/subscribers', middleware.api.labs.subscribers, authenticatePublic, api.http(api.subscribers.add));
|
||||
router.put('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.edit));
|
||||
router.del('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.destroy));
|
||||
router.del('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.destroy));
|
||||
|
||||
// ## Roles
|
||||
router.get('/roles/', authenticatePrivate, api.http(api.roles.browse));
|
||||
@ -151,9 +157,16 @@ apiRoutes = function apiRoutes(middleware) {
|
||||
router.get('/authentication/setup', api.http(api.authentication.isSetup));
|
||||
router.post('/authentication/token',
|
||||
middleware.spamPrevention.signin,
|
||||
middleware.api.authenticateClient,
|
||||
middleware.oauth.generateAccessToken
|
||||
auth.authenticate.authenticateClient,
|
||||
auth.oauth.generateAccessToken
|
||||
);
|
||||
|
||||
router.post('/authentication/ghost', [
|
||||
auth.authenticate.authenticateClient,
|
||||
auth.authenticate.authenticateGhostUser,
|
||||
api.http(api.authentication.createTokens)
|
||||
]);
|
||||
|
||||
router.post('/authentication/revoke', authenticatePrivate, api.http(api.authentication.revoke));
|
||||
|
||||
// ## Uploads
|
||||
|
@ -394,6 +394,7 @@ describe('Authentication API', function () {
|
||||
.then(function (response) {
|
||||
should.exist(response);
|
||||
response.invitation[0].valid.should.be.true();
|
||||
response.invitation[0].invitedBy.should.eql('Joe Bloggs');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
_ = require('lodash'),
|
||||
|
||||
authStrategies = require('../../../server/middleware/auth-strategies'),
|
||||
Models = require('../../../server/models'),
|
||||
globalUtils = require('../../../server/utils'),
|
||||
authStrategies = require('../../../server/auth/auth-strategies'),
|
||||
Models = require('../../../server/models'),
|
||||
errors = require('../../../server/errors'),
|
||||
globalUtils = require('../../../server/utils'),
|
||||
|
||||
sandbox = sinon.sandbox.create(),
|
||||
|
||||
fakeClient = {
|
||||
fakeClient = {
|
||||
slug: 'ghost-admin',
|
||||
secret: 'not_available',
|
||||
status: 'enabled'
|
||||
@ -50,7 +52,9 @@ describe('Auth Strategies', function () {
|
||||
clientStub = sandbox.stub(Models.Client, 'findOne');
|
||||
clientStub.returns(new Promise.resolve());
|
||||
clientStub.withArgs({slug: fakeClient.slug}).returns(new Promise.resolve({
|
||||
toJSON: function () { return fakeClient; }
|
||||
toJSON: function () {
|
||||
return fakeClient;
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
@ -116,16 +120,22 @@ describe('Auth Strategies', function () {
|
||||
tokenStub = sandbox.stub(Models.Accesstoken, 'findOne');
|
||||
tokenStub.returns(new Promise.resolve());
|
||||
tokenStub.withArgs({token: fakeValidToken.token}).returns(new Promise.resolve({
|
||||
toJSON: function () { return fakeValidToken; }
|
||||
toJSON: function () {
|
||||
return fakeValidToken;
|
||||
}
|
||||
}));
|
||||
tokenStub.withArgs({token: fakeInvalidToken.token}).returns(new Promise.resolve({
|
||||
toJSON: function () { return fakeInvalidToken; }
|
||||
toJSON: function () {
|
||||
return fakeInvalidToken;
|
||||
}
|
||||
}));
|
||||
|
||||
userStub = sandbox.stub(Models.User, 'findOne');
|
||||
userStub.returns(new Promise.resolve());
|
||||
userStub.withArgs({id: 3}).returns(new Promise.resolve({
|
||||
toJSON: function () { return {id: 3}; }
|
||||
toJSON: function () {
|
||||
return {id: 3};
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
@ -189,4 +199,141 @@ describe('Auth Strategies', function () {
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ghost Strategy', function () {
|
||||
var userByEmailStub, inviteStub, userAddStub, userEditStub, userFindOneStub;
|
||||
|
||||
beforeEach(function () {
|
||||
userByEmailStub = sandbox.stub(Models.User, 'getByEmail');
|
||||
userFindOneStub = sandbox.stub(Models.User, 'findOne');
|
||||
userAddStub = sandbox.stub(Models.User, 'add');
|
||||
userEditStub = sandbox.stub(Models.User, 'edit');
|
||||
inviteStub = sandbox.stub(Models.Invite, 'findOne');
|
||||
});
|
||||
|
||||
it('with invite, but with wrong invite token', function (done) {
|
||||
var patronusAccessToken = '12345',
|
||||
req = {body: {inviteToken: 'wrong'}},
|
||||
profile = {email_address: 'kate@ghost.org'};
|
||||
|
||||
userByEmailStub.returns(Promise.resolve(null));
|
||||
inviteStub.returns(Promise.reject(new errors.NotFoundError()));
|
||||
|
||||
authStrategies.ghostStrategy(req, patronusAccessToken, null, profile, function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NotFoundError).should.eql(true);
|
||||
userByEmailStub.calledOnce.should.be.true();
|
||||
inviteStub.calledOnce.should.be.true();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with correct invite token, but expired', function (done) {
|
||||
var patronusAccessToken = '12345',
|
||||
req = {body: {inviteToken: 'token'}},
|
||||
profile = {email_address: 'kate@ghost.org'};
|
||||
|
||||
userByEmailStub.returns(Promise.resolve(null));
|
||||
inviteStub.returns(Promise.resolve(Models.Invite.forge({
|
||||
id: 1,
|
||||
token: 'token',
|
||||
expires: Date.now() - 1000
|
||||
})));
|
||||
|
||||
authStrategies.ghostStrategy(req, patronusAccessToken, null, profile, function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NotFoundError).should.eql(true);
|
||||
userByEmailStub.calledOnce.should.be.true();
|
||||
inviteStub.calledOnce.should.be.true();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with correct invite token', function (done) {
|
||||
var patronusAccessToken = '12345',
|
||||
req = {body: {inviteToken: 'token'}},
|
||||
invitedProfile = {email_address: 'kate@ghost.org'},
|
||||
invitedUser = {id: 2},
|
||||
inviteModel = Models.Invite.forge({
|
||||
id: 1,
|
||||
token: 'token',
|
||||
expires: Date.now() + 1000
|
||||
});
|
||||
|
||||
userByEmailStub.returns(Promise.resolve(null));
|
||||
userAddStub.returns(Promise.resolve(invitedUser));
|
||||
userEditStub.returns(Promise.resolve(invitedUser));
|
||||
inviteStub.returns(Promise.resolve(inviteModel));
|
||||
sandbox.stub(inviteModel, 'destroy').returns(Promise.resolve());
|
||||
|
||||
authStrategies.ghostStrategy(req, patronusAccessToken, null, invitedProfile, function (err, user, profile) {
|
||||
should.not.exist(err);
|
||||
should.exist(user);
|
||||
should.exist(profile);
|
||||
user.should.eql(invitedUser);
|
||||
profile.should.eql(invitedProfile);
|
||||
|
||||
userByEmailStub.calledOnce.should.be.true();
|
||||
inviteStub.calledOnce.should.be.true();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setup', function (done) {
|
||||
var patronusAccessToken = '12345',
|
||||
req = {body: {}},
|
||||
ownerProfile = {email_address: 'kate@ghost.org'},
|
||||
owner = {id: 2};
|
||||
|
||||
userByEmailStub.returns(Promise.resolve(null));
|
||||
userFindOneStub.returns(Promise.resolve(_.merge({}, {status: 'inactive'}, owner)));
|
||||
userEditStub.withArgs({status: 'active', email: 'kate@ghost.org'}, {
|
||||
context: {internal: true},
|
||||
id: owner.id
|
||||
}).returns(Promise.resolve(owner));
|
||||
|
||||
userEditStub.withArgs({patronus_access_token: patronusAccessToken}, {
|
||||
context: {internal: true},
|
||||
id: owner.id
|
||||
}).returns(Promise.resolve(owner));
|
||||
|
||||
authStrategies.ghostStrategy(req, patronusAccessToken, null, ownerProfile, function (err, user, profile) {
|
||||
should.not.exist(err);
|
||||
userByEmailStub.calledOnce.should.be.true();
|
||||
inviteStub.calledOnce.should.be.false();
|
||||
|
||||
should.exist(user);
|
||||
should.exist(profile);
|
||||
user.should.eql(owner);
|
||||
profile.should.eql(ownerProfile);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('auth', function (done) {
|
||||
var patronusAccessToken = '12345',
|
||||
req = {body: {}},
|
||||
ownerProfile = {email_address: 'kate@ghost.org'},
|
||||
owner = {id: 2};
|
||||
|
||||
userByEmailStub.returns(Promise.resolve(owner));
|
||||
userEditStub.withArgs({patronus_access_token: patronusAccessToken}, {
|
||||
context: {internal: true},
|
||||
id: owner.id
|
||||
}).returns(Promise.resolve(owner));
|
||||
|
||||
authStrategies.ghostStrategy(req, patronusAccessToken, null, ownerProfile, function (err, user, profile) {
|
||||
should.not.exist(err);
|
||||
userByEmailStub.calledOnce.should.be.true();
|
||||
userEditStub.calledOnce.should.be.true();
|
||||
inviteStub.calledOnce.should.be.false();
|
||||
|
||||
should.exist(user);
|
||||
should.exist(profile);
|
||||
user.should.eql(owner);
|
||||
profile.should.eql(ownerProfile);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -3,7 +3,7 @@ var sinon = require('sinon'),
|
||||
passport = require('passport'),
|
||||
rewire = require('rewire'),
|
||||
errors = require('../../../server/errors'),
|
||||
auth = rewire('../../../server/middleware/auth'),
|
||||
auth = rewire('../../../server/auth'),
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
user = {id: 1},
|
||||
@ -100,7 +100,7 @@ describe('Auth', function () {
|
||||
it('should require authorized user (user exists)', function (done) {
|
||||
req.user = {id: 1};
|
||||
|
||||
auth.requiresAuthorizedUser(req, res, next);
|
||||
auth.authorize.requiresAuthorizedUser(req, res, next);
|
||||
next.called.should.be.true();
|
||||
next.calledWith().should.be.true();
|
||||
done();
|
||||
@ -119,7 +119,7 @@ describe('Auth', function () {
|
||||
};
|
||||
});
|
||||
|
||||
auth.requiresAuthorizedUser(req, res, next);
|
||||
auth.authorize.requiresAuthorizedUser(req, res, next);
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
@ -130,7 +130,7 @@ describe('Auth', function () {
|
||||
req.headers.authorization = 'Bearer ' + token;
|
||||
|
||||
registerSuccessfulBearerStrategy();
|
||||
auth.authenticateUser(req, res, next);
|
||||
auth.authenticate.authenticateUser(req, res, next);
|
||||
|
||||
next.called.should.be.true();
|
||||
next.calledWith(null, user, info).should.be.true();
|
||||
@ -142,7 +142,7 @@ describe('Auth', function () {
|
||||
req.client = {id: 1};
|
||||
res.status = {};
|
||||
|
||||
auth.authenticateUser(req, res, next);
|
||||
auth.authenticate.authenticateUser(req, res, next);
|
||||
|
||||
next.called.should.be.true();
|
||||
next.calledWith().should.be.true();
|
||||
@ -164,7 +164,7 @@ describe('Auth', function () {
|
||||
});
|
||||
|
||||
registerUnsuccessfulBearerStrategy();
|
||||
auth.authenticateUser(req, res, next);
|
||||
auth.authenticate.authenticateUser(req, res, next);
|
||||
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
@ -184,7 +184,7 @@ describe('Auth', function () {
|
||||
});
|
||||
|
||||
registerUnsuccessfulBearerStrategy();
|
||||
auth.authenticateUser(req, res, next);
|
||||
auth.authenticate.authenticateUser(req, res, next);
|
||||
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
@ -206,7 +206,7 @@ describe('Auth', function () {
|
||||
});
|
||||
|
||||
registerUnsuccessfulBearerStrategy();
|
||||
auth.authenticateUser(req, res, next);
|
||||
auth.authenticate.authenticateUser(req, res, next);
|
||||
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
@ -217,7 +217,7 @@ describe('Auth', function () {
|
||||
req.headers.authorization = 'Bearer ' + token;
|
||||
|
||||
registerFaultyBearerStrategy();
|
||||
auth.authenticateUser(req, res, next);
|
||||
auth.authenticate.authenticateUser(req, res, next);
|
||||
|
||||
next.called.should.be.true();
|
||||
next.calledWith('error').should.be.true();
|
||||
@ -230,7 +230,7 @@ describe('Auth', function () {
|
||||
req.headers = {};
|
||||
req.headers.authorization = 'Bearer ' + token;
|
||||
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
next.called.should.be.true();
|
||||
next.calledWith().should.be.true();
|
||||
done();
|
||||
@ -251,7 +251,7 @@ describe('Auth', function () {
|
||||
};
|
||||
});
|
||||
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
@ -269,7 +269,7 @@ describe('Auth', function () {
|
||||
};
|
||||
});
|
||||
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
@ -288,7 +288,7 @@ describe('Auth', function () {
|
||||
};
|
||||
});
|
||||
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
@ -307,7 +307,7 @@ describe('Auth', function () {
|
||||
};
|
||||
});
|
||||
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
next.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
@ -327,7 +327,7 @@ describe('Auth', function () {
|
||||
});
|
||||
|
||||
registerUnsuccessfulClientPasswordStrategy();
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
next.called.should.be.false();
|
||||
errorStub.calledTwice.should.be.true();
|
||||
errorStub.getCall(0).args[1].should.eql('Client credentials were not provided');
|
||||
@ -351,7 +351,7 @@ describe('Auth', function () {
|
||||
});
|
||||
|
||||
registerUnsuccessfulClientPasswordStrategy();
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
next.called.should.be.false();
|
||||
errorStub.calledTwice.should.be.true();
|
||||
errorStub.getCall(0).args[1].should.eql('Client credentials were not valid');
|
||||
@ -366,7 +366,7 @@ describe('Auth', function () {
|
||||
req.headers = {};
|
||||
|
||||
registerSuccessfulClientPasswordStrategy();
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
|
||||
next.called.should.be.true();
|
||||
next.calledWith(null, client).should.be.true();
|
||||
@ -381,7 +381,7 @@ describe('Auth', function () {
|
||||
req.headers = {};
|
||||
|
||||
registerSuccessfulClientPasswordStrategy();
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
|
||||
next.called.should.be.true();
|
||||
next.calledWith(null, client).should.be.true();
|
||||
@ -396,7 +396,7 @@ describe('Auth', function () {
|
||||
req.headers = {};
|
||||
|
||||
registerSuccessfulClientPasswordStrategy();
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
|
||||
next.called.should.be.true();
|
||||
next.calledWith(null, client).should.be.true();
|
||||
@ -410,7 +410,7 @@ describe('Auth', function () {
|
||||
res.status = {};
|
||||
|
||||
registerFaultyClientPasswordStrategy();
|
||||
auth.authenticateClient(req, res, next);
|
||||
auth.authenticate.authenticateClient(req, res, next);
|
||||
|
||||
next.called.should.be.true();
|
||||
next.calledWith('error').should.be.true();
|
@ -1,8 +1,7 @@
|
||||
var sinon = require('sinon'),
|
||||
should = require('should'),
|
||||
Promise = require('bluebird'),
|
||||
|
||||
oAuth = require('../../../server/middleware/oauth'),
|
||||
oAuth = require('../../../server/auth/oauth'),
|
||||
Models = require('../../../server/models');
|
||||
|
||||
describe('OAuth', function () {
|
@ -69,4 +69,40 @@ describe('redirectToSetup', function () {
|
||||
|
||||
redirectToSetup(req, res, next);
|
||||
});
|
||||
|
||||
it('should not redirect successful oauth authorization requests', function (done) {
|
||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
||||
return Promise.resolve({setup: [{status: false}]});
|
||||
});
|
||||
|
||||
res = {redirect: sinon.spy()};
|
||||
req.path = '/';
|
||||
req.query = {code: 'authCode'};
|
||||
|
||||
next = sinon.spy(function () {
|
||||
next.called.should.be.true();
|
||||
res.redirect.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
|
||||
redirectToSetup(req, res, next);
|
||||
});
|
||||
|
||||
it('should not redirect failed oauth authorization requests', function (done) {
|
||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
||||
return Promise.resolve({setup: [{status: false}]});
|
||||
});
|
||||
|
||||
res = {redirect: sinon.spy()};
|
||||
req.path = '/';
|
||||
req.query = {error: 'access_denied', state: 'randomstring'};
|
||||
|
||||
next = sinon.spy(function () {
|
||||
next.called.should.be.true();
|
||||
res.redirect.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
|
||||
redirectToSetup(req, res, next);
|
||||
});
|
||||
});
|
||||
|
@ -22,7 +22,7 @@ var _ = require('lodash'),
|
||||
// Post API swaps author_id to author, and always returns a computed 'url' property
|
||||
post: _(schema.posts).keys().without('author_id').concat('author', 'url').value(),
|
||||
// User API always removes the password field
|
||||
user: _(schema.users).keys().without('password').value(),
|
||||
user: _(schema.users).keys().without('password').without('patronus_access_token').value(),
|
||||
// Tag API swaps parent_id to parent
|
||||
tag: _(schema.tags).keys().without('parent_id').concat('parent').value(),
|
||||
setting: _.keys(schema.settings),
|
||||
|
@ -66,6 +66,7 @@
|
||||
"passport": "0.3.2",
|
||||
"passport-http-bearer": "1.0.1",
|
||||
"passport-oauth2-client-password": "0.1.2",
|
||||
"passport-ghost": "1.0.0",
|
||||
"path-match": "1.2.4",
|
||||
"rss": "1.2.1",
|
||||
"sanitize-html": "1.13.0",
|
||||
|
Loading…
Reference in New Issue
Block a user