0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Merge pull request #6579 from jaswilli/auth-api

Refactor authentication API into pipeline format
This commit is contained in:
Sebastian Gierlinger 2016-03-08 15:54:34 +01:00
commit 411dd47002
6 changed files with 711 additions and 242 deletions

View file

@ -1,54 +1,125 @@
var _ = require('lodash'), var _ = require('lodash'),
validator = require('validator'),
pipeline = require('../utils/pipeline'),
dataProvider = require('../models'), dataProvider = require('../models'),
settings = require('./settings'), settings = require('./settings'),
mail = require('./mail'), mail = require('./mail'),
globalUtils = require('../utils'), globalUtils = require('../utils'),
utils = require('./utils'), utils = require('./utils'),
Promise = require('bluebird'),
errors = require('../errors'), errors = require('../errors'),
config = require('../config'), config = require('../config'),
i18n = require('../i18n'), i18n = require('../i18n'),
authentication; authentication;
function setupTasks(object) { /**
var setupUser, * Returns setup status
internal = {context: {internal: true}}; *
* @return {Promise<Boolean>}
*/
function checkSetup() {
return authentication.isSetup().then(function then(result) {
return result.setup[0].status;
});
}
return utils.checkObject(object, 'setup').then(function (checkedSetupData) { /**
setupUser = { * Allows an assertion to be made about setup status.
name: checkedSetupData.setup[0].name, *
email: checkedSetupData.setup[0].email, * @param {Boolean} status True: setup must be complete. False: setup must not be complete.
password: checkedSetupData.setup[0].password, * @return {Function} returns a "task ready" function
blogTitle: checkedSetupData.setup[0].blogTitle, */
function assertSetupCompleted(status) {
return function checkPermission(__) {
return checkSetup().then(function then(isSetup) {
if (isSetup === status) {
return __;
}
var completed = i18n.t('errors.api.authentication.setupAlreadyCompleted'),
notCompleted = i18n.t('errors.api.authentication.setupMustBeCompleted');
function throwReason(reason) {
throw new errors.NoPermissionError(reason);
}
if (isSetup) {
throwReason(completed);
} else {
throwReason(notCompleted);
}
});
};
}
function setupTasks(setupData) {
var tasks;
function validateData(setupData) {
return utils.checkObject(setupData, 'setup').then(function then(checked) {
var data = checked.setup[0];
return {
name: data.name,
email: data.email,
password: data.password,
blogTitle: data.blogTitle,
status: 'active' status: 'active'
}; };
return dataProvider.User.findOne({role: 'Owner', status: 'all'});
}).then(function (ownerUser) {
if (ownerUser) {
return dataProvider.User.setup(setupUser, _.extend({id: ownerUser.id}, internal));
} else {
return dataProvider.Role.findOne({name: 'Owner'}).then(function (ownerRole) {
setupUser.roles = [ownerRole.id];
return dataProvider.User.add(setupUser, internal);
}); });
} }
}).then(function (user) {
var userSettings = [];
// Handles the additional values set by the setup screen. function setupUser(userData) {
if (!_.isEmpty(setupUser.blogTitle)) { var context = {context: {internal: true}},
userSettings.push({key: 'title', value: setupUser.blogTitle}); User = dataProvider.User;
userSettings.push({key: 'description', value: i18n.t('common.api.authentication.sampleBlogDescription')});
return User.findOne({role: 'Owner', status: 'all'}).then(function then(owner) {
if (!owner) {
throw new errors.InternalServerError(
i18n.t('errors.api.authentication.setupUnableToRun')
);
} }
setupUser = user.toJSON(internal); return User.setup(userData, _.extend({id: owner.id}, context));
return settings.edit({settings: userSettings}, {context: {user: setupUser.id}}); }).then(function then(user) {
}).then(function () { return {
return Promise.resolve(setupUser); user: user,
userData: userData
};
}); });
} }
function doSettings(data) {
var user = data.user,
blogTitle = data.userData.blogTitle,
context = {context: {user: data.user.id}},
userSettings;
if (!blogTitle || typeof blogTitle !== 'string') {
return user;
}
userSettings = [
{key: 'title', value: blogTitle.trim()},
{key: 'description', value: i18n.t('common.api.authentication.sampleBlogDescription')}
];
return settings.edit({settings: userSettings}, context).return(user);
}
function formatResponse(user) {
return user.toJSON({context: {internal: true}});
}
tasks = [
validateData,
setupUser,
doSettings,
formatResponse
];
return pipeline(tasks, setupData);
}
/** /**
* ## Authentication API Methods * ## Authentication API Methods
* *
@ -57,211 +128,290 @@ function setupTasks(object) {
authentication = { authentication = {
/** /**
* ## Generate Reset Token * @description generate a reset token for a given email address
* generate a reset token for a given email address * @param {Object} resetRequest
* @param {Object} object * @returns {Promise<Object>} message
* @returns {Promise(passwordreset)} message
*/ */
generateResetToken: function generateResetToken(object) { generateResetToken: function generateResetToken(resetRequest) {
var expires = Date.now() + globalUtils.ONE_DAY_MS, var tasks;
email;
return authentication.isSetup().then(function (result) { function validateRequest(resetRequest) {
var setup = result.setup[0].status; return utils.checkObject(resetRequest, 'passwordreset').then(function then(data) {
var email = data.passwordreset[0].email;
if (!setup) { if (typeof email !== 'string' || !validator.isEmail(email)) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted'))); throw new errors.BadRequestError(
i18n.t('errors.api.authentication.noEmailProvided')
);
} }
return utils.checkObject(object, 'passwordreset'); return email;
}).then(function (checkedPasswordReset) { });
if (checkedPasswordReset.passwordreset[0].email) {
email = checkedPasswordReset.passwordreset[0].email;
} else {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.authentication.noEmailProvided')));
} }
return settings.read({context: {internal: true}, key: 'dbHash'}) function generateToken(email) {
.then(function (response) { var settingsQuery = {context: {internal: true}, key: 'dbHash'};
var dbHash = response.settings[0].value;
return dataProvider.User.generateResetToken(email, expires, dbHash); return settings.read(settingsQuery).then(function then(response) {
}).then(function (resetToken) { var dbHash = response.settings[0].value,
expiresAt = Date.now() + globalUtils.ONE_DAY_MS;
return dataProvider.User.generateResetToken(email, expiresAt, dbHash);
}).then(function then(resetToken) {
return {
email: email,
resetToken: resetToken
};
});
}
function sendResetNotification(data) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url, var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + globalUtils.encodeBase64URLsafe(resetToken) + '/'; resetUrl = baseUrl.replace(/\/$/, '') +
'/ghost/reset/' +
globalUtils.encodeBase64URLsafe(data.resetToken) + '/';
return mail.generateContent({data: {resetUrl: resetUrl}, template: 'reset-password'}); return mail.generateContent({
}).then(function (emailContent) { data: {
resetUrl: resetUrl
},
template: 'reset-password'
}).then(function then(content) {
var payload = { var payload = {
mail: [{ mail: [{
message: { message: {
to: email, to: data.email,
subject: i18n.t('common.api.authentication.mail.resetPassword'), subject: i18n.t('common.api.authentication.mail.resetPassword'),
html: emailContent.html, html: content.html,
text: emailContent.text text: content.text
}, },
options: {} options: {}
}] }]
}; };
return mail.send(payload, {context: {internal: true}}); return mail.send(payload, {context: {internal: true}});
}).then(function () {
return Promise.resolve({passwordreset: [{message: i18n.t('common.api.authentication.mail.checkEmailForInstructions')}]});
}).catch(function (error) {
return Promise.reject(error);
});
}); });
}
function formatResponse() {
return {
passwordreset: [
{message: i18n.t('common.api.authentication.mail.checkEmailForInstructions')}
]
};
}
tasks = [
assertSetupCompleted(true),
validateRequest,
generateToken,
sendResetNotification,
formatResponse
];
return pipeline(tasks, resetRequest);
}, },
/** /**
* ## Reset Password * ## Reset Password
* reset password if a valid token and password (2x) is passed * reset password if a valid token and password (2x) is passed
* @param {Object} object * @param {Object} resetRequest
* @returns {Promise(passwordreset)} message * @returns {Promise<Object>} message
*/ */
resetPassword: function resetPassword(object) { resetPassword: function resetPassword(resetRequest) {
var resetToken, var tasks;
newPassword,
ne2Password;
return authentication.isSetup().then(function (result) { function validateRequest(resetRequest) {
var setup = result.setup[0].status; return utils.checkObject(resetRequest, 'passwordreset');
if (!setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
} }
return utils.checkObject(object, 'passwordreset'); function doReset(resetRequest) {
}).then(function (checkedPasswordReset) { var settingsQuery = {context: {internal: true}, key: 'dbHash'},
resetToken = checkedPasswordReset.passwordreset[0].token; data = resetRequest.passwordreset[0],
newPassword = checkedPasswordReset.passwordreset[0].newPassword; resetToken = data.token,
ne2Password = checkedPasswordReset.passwordreset[0].ne2Password; newPassword = data.newPassword,
ne2Password = data.ne2Password;
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) { return settings.read(settingsQuery).then(function then(response) {
var dbHash = response.settings[0].value;
return dataProvider.User.resetPassword({ return dataProvider.User.resetPassword({
token: resetToken, token: resetToken,
newPassword: newPassword, newPassword: newPassword,
ne2Password: ne2Password, ne2Password: ne2Password,
dbHash: dbHash dbHash: response.settings[0].value
}); });
}).then(function () {
return Promise.resolve({passwordreset: [{message: i18n.t('common.api.authentication.mail.passwordChanged')}]});
}).catch(function (error) { }).catch(function (error) {
return Promise.reject(new errors.UnauthorizedError(error.message)); throw new errors.UnauthorizedError(error.message);
});
}); });
}
function formatResponse() {
return {
passwordreset: [
{message: i18n.t('common.api.authentication.mail.passwordChanged')}
]
};
}
tasks = [
assertSetupCompleted(true),
validateRequest,
doReset,
formatResponse
];
return pipeline(tasks, resetRequest);
}, },
/** /**
* ### Accept Invitation * ### Accept Invitation
* @param {User} object the user to create * @param {Object} invitation an invitation object
* @returns {Promise(User}} Newly created user * @returns {Promise<Object>}
*/ */
acceptInvitation: function acceptInvitation(object) { acceptInvitation: function acceptInvitation(invitation) {
var resetToken, var tasks;
newPassword,
ne2Password,
name,
email;
return authentication.isSetup().then(function (result) { function validateInvitation(invitation) {
var setup = result.setup[0].status; return utils.checkObject(invitation, 'invitation');
if (!setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
} }
return utils.checkObject(object, 'invitation'); function processInvitation(invitation) {
}).then(function (checkedInvitation) { var User = dataProvider.User,
resetToken = checkedInvitation.invitation[0].token; settingsQuery = {context: {internal: true}, key: 'dbHash'},
newPassword = checkedInvitation.invitation[0].password; data = invitation.invitation[0],
ne2Password = checkedInvitation.invitation[0].password; resetToken = data.token,
email = checkedInvitation.invitation[0].email; newPassword = data.password,
name = checkedInvitation.invitation[0].name; email = data.email,
name = data.name;
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) { return settings.read(settingsQuery).then(function then(response) {
var dbHash = response.settings[0].value; return User.resetPassword({
return dataProvider.User.resetPassword({
token: resetToken, token: resetToken,
newPassword: newPassword, newPassword: newPassword,
ne2Password: ne2Password, ne2Password: newPassword,
dbHash: dbHash dbHash: response.settings[0].value
}); });
}).then(function (user) { }).then(function then(user) {
// Setting the slug to '' has the model regenerate the slug from the user's name return User.edit({name: name, email: email, slug: ''}, {id: user.id});
return dataProvider.User.edit({name: name, email: email, slug: ''}, {id: user.id});
}).then(function () {
return Promise.resolve({invitation: [{message: i18n.t('common.api.authentication.mail.invitationAccepted')}]});
}).catch(function (error) { }).catch(function (error) {
return Promise.reject(new errors.UnauthorizedError(error.message)); throw new errors.UnauthorizedError(error.message);
});
}); });
}
function formatResponse() {
return {
invitation: [
{message: i18n.t('common.api.authentication.mail.invitationAccepted')}
]
};
}
tasks = [
assertSetupCompleted(true),
validateInvitation,
processInvitation,
formatResponse
];
return pipeline(tasks, invitation);
}, },
/** /**
* ### Check for invitation * ### Check for invitation
* @param {Object} options * @param {Object} options
* @param {string} options.email The email to check for an invitation on * @returns {Promise<Object>} An invitation status
* @returns {Promise(Invitation}} An invitation status
*/ */
isInvitation: function isInvitation(options) { isInvitation: function isInvitation(options) {
return authentication.isSetup().then(function (result) { var tasks,
var setup = result.setup[0].status; localOptions = _.cloneDeep(options || {});
if (!setup) { function processArgs(options) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted'))); var email = options.email;
if (typeof email !== 'string' || !validator.isEmail(email)) {
throw new errors.BadRequestError(
i18n.t('errors.api.authentication.invalidEmailReceived')
);
} }
if (options.email) { return email;
return dataProvider.User.findOne({email: options.email, status: 'invited'}).then(function (response) {
if (response) {
return {invitation: [{valid: true}]};
} else {
return {invitation: [{valid: false}]};
} }
function checkInvitation(email) {
return dataProvider.User
.where({email: email, status: 'invited'})
.count('id')
.then(function then(count) {
return !!count;
}); });
} else {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.authentication.invalidEmailReceived')));
} }
});
function formatResponse(isInvited) {
return {invitation: [{valid: isInvited}]};
}
tasks = [
processArgs,
assertSetupCompleted(true),
checkInvitation,
formatResponse
];
return pipeline(tasks, localOptions);
}, },
/**
* Checks the setup status
* @return {Promise}
*/
isSetup: function isSetup() { isSetup: function isSetup() {
return dataProvider.User.query(function (qb) { var tasks,
qb.whereIn('status', ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked']); validStatuses = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'];
}).fetch().then(function (users) {
if (users) { function checkSetupStatus() {
return Promise.resolve({setup: [{status: true}]}); return dataProvider.User
} else { .where('status', 'in', validStatuses)
return Promise.resolve({setup: [{status: false}]}); .count('id')
} .then(function (count) {
return !!count;
}); });
}
function formatResponse(isSetup) {
return {setup: [{status: isSetup}]};
}
tasks = [
checkSetupStatus,
formatResponse
];
return pipeline(tasks);
}, },
setup: function setup(object) { /**
var setupUser; * Executes the setup tasks and sends an email to the owner
* @param {Object} setupDetails
* @return {Promise<Object>} a user api payload
*/
setup: function setup(setupDetails) {
var tasks;
return authentication.isSetup().then(function (result) { function doSetup(setupDetails) {
var setup = result.setup[0].status; return setupTasks(setupDetails);
if (setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupAlreadyCompleted')));
} }
return setupTasks(object); function sendNotification(setupUser) {
}).then(function (result) {
setupUser = result;
var data = { var data = {
ownerEmail: setupUser.email ownerEmail: setupUser.email
}; };
return mail.generateContent({data: data, template: 'welcome'}); return mail.generateContent({data: data, template: 'welcome'})
}).then(function (emailContent) { .then(function then(content) {
var message = { var message = {
to: setupUser.email, to: setupUser.email,
subject: i18n.t('common.api.authentication.mail.yourNewGhostBlog'), subject: i18n.t('common.api.authentication.mail.yourNewGhostBlog'),
html: emailContent.html, html: content.html,
text: emailContent.text text: content.text
}, },
payload = { payload = {
mail: [{ mail: [{
@ -273,51 +423,126 @@ authentication = {
mail.send(payload, {context: {internal: true}}).catch(function (error) { mail.send(payload, {context: {internal: true}}).catch(function (error) {
errors.logError( errors.logError(
error.message, error.message,
i18n.t('errors.api.authentication.unableToSendWelcomeEmail', {url: 'http://support.ghost.org/mail/'}), i18n.t(
'errors.api.authentication.unableToSendWelcomeEmail',
{url: 'http://support.ghost.org/mail/'}
),
i18n.t('errors.api.authentication.checkEmailConfigInstructions') i18n.t('errors.api.authentication.checkEmailConfigInstructions')
); );
}); });
}).then(function () { })
return Promise.resolve({users: [setupUser]}); .return(setupUser);
}); }
function formatResponse(setupUser) {
return {users: [setupUser]};
}
tasks = [
assertSetupCompleted(false),
doSetup,
sendNotification,
formatResponse
];
return pipeline(tasks, setupDetails);
}, },
updateSetup: function updateSetup(object, options) { /**
* Updates the blog setup
* @param {Object} setupDetails request payload with setup details
* @param {Object} options
* @return {Promise<Object>} a User API response payload
*/
updateSetup: function updateSetup(setupDetails, options) {
var tasks,
localOptions = _.cloneDeep(options || {});
function processArgs(setupDetails, options) {
if (!options.context || !options.context.user) { if (!options.context || !options.context.user) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.notLoggedIn'))); throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner'));
} }
return dataProvider.User.findOne({role: 'Owner', status: 'all'}).then(function (result) { return _.assign({setupDetails: setupDetails}, options);
var user = result.toJSON();
if (user.id !== options.context.user) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner')));
} }
return setupTasks(object); function checkPermission(options) {
}).then(function (result) { return dataProvider.User.findOne({role: 'Owner', status: 'all'})
return Promise.resolve({users: [result]}); .then(function (owner) {
if (owner.id !== options.context.user) {
throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner'));
}
return options.setupDetails;
}); });
}
function formatResponse(user) {
return {users: [user]};
}
tasks = [
processArgs,
assertSetupCompleted(true),
checkPermission,
setupTasks,
formatResponse
];
return pipeline(tasks, setupDetails, localOptions);
}, },
revoke: function (object) { /**
var token; * Revokes a bearer token.
* @param {Object} tokenDetails
* @param {Object} options
* @return {Promise<Object>} an object containing the revoked token.
*/
revoke: function revokeToken(tokenDetails, options) {
var tasks,
localOptions = _.cloneDeep(options || {});
if (object.token_type_hint && object.token_type_hint === 'access_token') { function processArgs(tokenDetails, options) {
token = dataProvider.Accesstoken; return _.assign({}, tokenDetails, options);
} else if (object.token_type_hint && object.token_type_hint === 'refresh_token') {
token = dataProvider.Refreshtoken;
} else {
return errors.BadRequestError(i18n.t('errors.api.authentication.invalidTokenTypeHint'));
} }
return token.destroyByToken({token: object.token}).then(function () { function revokeToken(options) {
return Promise.resolve({token: object.token}); var providers = [
}, function () { dataProvider.Refreshtoken,
// On error we still want a 200. See https://tools.ietf.org/html/rfc7009#page-5 dataProvider.Accesstoken
return Promise.resolve({token: object.token, error: i18n.t('errors.api.authentication.invalidTokenProvided')}); ],
response = {token: options.token};
function destroyToken(provider, options, providers) {
return provider.destroyByToken(options)
.return(response)
.catch(provider.NotFoundError, function () {
if (!providers.length) {
return {
token: tokenDetails.token,
error: i18n.t('errors.api.authentication.invalidTokenProvided')
};
}
return destroyToken(providers.pop(), options, providers);
})
.catch(function () {
throw new errors.TokenRevocationError(
i18n.t('errors.api.authentication.tokenRevocationFailed')
);
}); });
} }
return destroyToken(providers.pop(), options, providers);
}
tasks = [
processArgs,
revokeToken
];
return pipeline(tasks, tokenDetails, localOptions);
}
}; };
module.exports = authentication; module.exports = authentication;

View file

@ -17,6 +17,7 @@ var _ = require('lodash'),
EmailError = require('./email-error'), EmailError = require('./email-error'),
DataImportError = require('./data-import-error'), DataImportError = require('./data-import-error'),
TooManyRequestsError = require('./too-many-requests-error'), TooManyRequestsError = require('./too-many-requests-error'),
TokenRevocationError = require('./token-revocation-error'),
i18n = require('../i18n'), i18n = require('../i18n'),
config, config,
errors, errors,
@ -431,3 +432,4 @@ module.exports.EmailError = EmailError;
module.exports.DataImportError = DataImportError; module.exports.DataImportError = DataImportError;
module.exports.MethodNotAllowedError = MethodNotAllowedError; module.exports.MethodNotAllowedError = MethodNotAllowedError;
module.exports.TooManyRequestsError = TooManyRequestsError; module.exports.TooManyRequestsError = TooManyRequestsError;
module.exports.TokenRevocationError = TokenRevocationError;

View file

@ -0,0 +1,14 @@
// # Token Revocation ERror
// Custom error class with status code and type prefilled.
function TokenRevocationError(message) {
this.message = message;
this.stack = new Error().stack;
this.statusCode = 503;
this.errorType = this.name;
}
TokenRevocationError.prototype = Object.create(Error.prototype);
TokenRevocationError.prototype.name = 'TokenRevocationError';
module.exports = TokenRevocationError;

View file

@ -68,18 +68,15 @@ Basetoken = ghostBookshelf.Model.extend({
var token = options.token; var token = options.token;
options = this.filterOptions(options, 'destroyByUser'); options = this.filterOptions(options, 'destroyByUser');
options.require = true;
if (token) { return this.forge()
return ghostBookshelf.Collection.forge([], {model: this})
.query('where', 'token', '=', token) .query('where', 'token', '=', token)
.fetch(options) .fetch(options)
.then(function then(collection) { .then(function then(model) {
collection.invokeThen('destroy', options); return model.destroy(options);
}); });
} }
return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.base.token.tokenNotFound')));
}
}); });
module.exports = Basetoken; module.exports = Basetoken;

View file

@ -278,6 +278,7 @@
}, },
"api": { "api": {
"authentication": { "authentication": {
"setupUnableToRun": "Database missing fixture data. Please reset database and try again.",
"setupMustBeCompleted": "Setup must be completed before making this request.", "setupMustBeCompleted": "Setup must be completed before making this request.",
"noEmailProvided": "No email provided.", "noEmailProvided": "No email provided.",
"invalidEmailReceived": "The server did not receive a valid email", "invalidEmailReceived": "The server did not receive a valid email",
@ -287,7 +288,8 @@
"notLoggedIn": "You are not logged in.", "notLoggedIn": "You are not logged in.",
"notTheBlogOwner": "You are not the blog owner.", "notTheBlogOwner": "You are not the blog owner.",
"invalidTokenTypeHint": "Invalid token_type_hint given.", "invalidTokenTypeHint": "Invalid token_type_hint given.",
"invalidTokenProvided": "Invalid token provided" "invalidTokenProvided": "Invalid token provided",
"tokenRevocationFailed": "Token revocation failed"
}, },
"clients": { "clients": {
"clientNotFound": "Client not found." "clientNotFound": "Client not found."

View file

@ -3,6 +3,10 @@ var testUtils = require('../../utils'),
should = require('should'), should = require('should'),
sinon = require('sinon'), sinon = require('sinon'),
Promise = require('bluebird'), Promise = require('bluebird'),
uid = require('../../../server/utils').uid,
Accesstoken,
Refreshtoken,
User,
// Stuff we are testing // Stuff we are testing
@ -51,6 +55,37 @@ describe('Authentication API', function () {
should.exist(AuthAPI); should.exist(AuthAPI);
describe('Setup', function () { describe('Setup', function () {
describe('Cannot run', function () {
before(function () {
User = require('../../../server/models/user').User;
});
beforeEach(testUtils.setup('roles', 'owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
describe('Invalid database state', function () {
it('should not allow setup to be run if owner missing from database', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'areallygoodpassword',
blogTitle: 'a test blog'
};
User.fetchAll().call('invokeThen', 'destroy').then(function () {
AuthAPI.setup({setup: [setupData]}).then(function () {
done(new Error('Setup ran when it should not have.'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('InternalServerError');
err.statusCode.should.equal(500);
done();
}).catch(done);
});
});
});
});
describe('Not completed', function () { describe('Not completed', function () {
// TODO: stub settings // TODO: stub settings
beforeEach(testUtils.setup('roles', 'owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init')); beforeEach(testUtils.setup('roles', 'owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
@ -89,6 +124,43 @@ describe('Authentication API', function () {
}).catch(done); }).catch(done);
}); });
it('should allow setup to be completed without a blog title', function (done) {
var setupData = {
name: 'test user',
email: 'test@example.com',
password: 'areallygoodpassword'
};
AuthAPI.setup({setup: [setupData]}).then(function (result) {
should.exist(result);
should.exist(result.users);
should.not.exist(result.meta);
result.users.should.have.length(1);
testUtils.API.checkResponse(result.users[0], 'user');
var newUser = result.users[0];
newUser.id.should.equal(1);
newUser.name.should.equal(setupData.name);
newUser.email.should.equal(setupData.email);
done();
}).catch(done);
});
it('should return an error for an invitation check', function (done) {
AuthAPI.isInvitation({email: 'a@example.com'}).then(function () {
done(new Error('Did not receive an error response'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('NoPermissionError');
err.statusCode.should.equal(403);
done();
}).catch(done);
});
it('should not allow an invitation to be accepted', function (done) { it('should not allow an invitation to be accepted', function (done) {
AuthAPI.acceptInvitation(testInvite).then(function () { AuthAPI.acceptInvitation(testInvite).then(function () {
done(new Error('Invitation was allowed to be accepted')); done(new Error('Invitation was allowed to be accepted'));
@ -99,7 +171,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403); err.statusCode.should.equal(403);
done(); done();
}); }).catch(done);
}); });
it('should not generate a password reset token', function (done) { it('should not generate a password reset token', function (done) {
@ -112,7 +184,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403); err.statusCode.should.equal(403);
done(); done();
}); }).catch(done);
}); });
it('should not allow a password reset', function (done) { it('should not allow a password reset', function (done) {
@ -125,12 +197,18 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403); err.statusCode.should.equal(403);
done(); done();
}); }).catch(done);
}); });
}); });
describe('Completed', function () { describe('Completed', function () {
beforeEach(testUtils.setup('roles', 'owner', 'settings', 'perms:setting', 'perms:mail', 'perms:init')); before(function () {
Accesstoken = require('../../../server/models/accesstoken').Accesstoken;
Refreshtoken = require('../../../server/models/refreshtoken').Refreshtoken;
User = require('../../../server/models/user').User;
});
beforeEach(testUtils.setup('roles', 'owner', 'clients', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
it('should report that setup has been completed', function (done) { it('should report that setup has been completed', function (done) {
AuthAPI.isSetup().then(function (result) { AuthAPI.isSetup().then(function (result) {
@ -158,7 +236,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403); err.statusCode.should.equal(403);
done(); done();
}); }).catch(done);
}); });
it('should allow an invitation to be accepted, but fail on token validation', function (done) { it('should allow an invitation to be accepted, but fail on token validation', function (done) {
@ -170,8 +248,9 @@ describe('Authentication API', function () {
err.name.should.equal('UnauthorizedError'); err.name.should.equal('UnauthorizedError');
err.statusCode.should.equal(401); err.statusCode.should.equal(401);
err.message.should.equal('Invalid token structure'); err.message.should.equal('Invalid token structure');
done(); done();
}); }).catch(done);
}); });
it('should generate a password reset token', function (done) { it('should generate a password reset token', function (done) {
@ -183,6 +262,23 @@ describe('Authentication API', function () {
}).catch(done); }).catch(done);
}); });
it('should not generate a password reset token for an invalid email address', function (done) {
var badResetRequest = {
passwordreset: [{email: ''}]
};
AuthAPI.generateResetToken(badResetRequest).then(function () {
done(new Error('reset token was generated for invalid email address'));
}).catch(function (err) {
should.exist(err);
err.name.should.equal('BadRequestError');
err.statusCode.should.equal(400);
done();
}).catch(done);
});
it('should allow a password reset', function (done) { it('should allow a password reset', function (done) {
AuthAPI.resetPassword(testReset).then(function () { AuthAPI.resetPassword(testReset).then(function () {
done(new Error('password reset did not fail on token validation')); done(new Error('password reset did not fail on token validation'));
@ -192,8 +288,141 @@ describe('Authentication API', function () {
err.name.should.equal('UnauthorizedError'); err.name.should.equal('UnauthorizedError');
err.statusCode.should.equal(401); err.statusCode.should.equal(401);
err.message.should.equal('Invalid token structure'); err.message.should.equal('Invalid token structure');
done(); done();
}).catch(done);
}); });
it('should allow an access token to be revoked', function (done) {
var id = uid(256);
Accesstoken.add({
token: id,
expires: Date.now() + 8640000,
user_id: 1,
client_id: 1
}).then(function (token) {
should.exist(token);
token.get('token').should.equal(id);
return AuthAPI.revoke({
token: token.get('token'),
token_type_hint: 'access_token'
});
}).then(function (response) {
should.exist(response);
response.token.should.equal(id);
return Accesstoken.findOne({token: id});
}).then(function (token) {
should.not.exist(token);
done();
}).catch(done);
});
it('should know an email address has an active invitation', function (done) {
var user = {
name: 'test user',
email: 'invited@example.com',
password: '12345678',
status: 'invited'
},
options = {
context: {internal: true}
};
User.add(user, options).then(function (user) {
return AuthAPI.isInvitation({email: user.get('email')});
}).then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.true();
done();
}).catch(done);
});
it('should know an email address does not have an active invitation', function (done) {
var user = {
name: 'uninvited user',
email: 'notinvited@example.com',
password: '12345678',
status: 'active'
},
options = {
context: {internal: true}
};
User.add(user, options).then(function (user) {
return AuthAPI.isInvitation({email: user.get('email')});
}).then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.false();
done();
}).catch(done);
});
it('should know an unknown email address is not an active invitation', function (done) {
AuthAPI.isInvitation({email: 'unknown@example.com'}).then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.false();
done();
}).catch(done);
});
it('should allow a refresh token to be revoked', function (done) {
var id = uid(256);
Refreshtoken.add({
token: id,
expires: Date.now() + 8640000,
user_id: 1,
client_id: 1
}).then(function (token) {
should.exist(token);
token.get('token').should.equal(id);
return AuthAPI.revoke({
token: token.get('token'),
token_type_hint: 'refresh_token'
});
}).then(function (response) {
should.exist(response);
response.token.should.equal(id);
return Refreshtoken.findOne({token: id});
}).then(function (token) {
should.not.exist(token);
done();
}).catch(done);
});
it('should return success when attempting to revoke an invalid token', function (done) {
var id = uid(256);
Accesstoken.add({
token: id,
expires: Date.now() + 8640000,
user_id: 1,
client_id: 1
}).then(function (token) {
should.exist(token);
token.get('token').should.equal(id);
return AuthAPI.revoke({
token: 'notavalidtoken',
token_type_hint: 'access_token'
});
}).then(function (response) {
should.exist(response);
response.token.should.equal('notavalidtoken');
response.error.should.equal('Invalid token provided');
done();
}).catch(done);
}); });
}); });
}); });
@ -228,7 +457,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403); err.statusCode.should.equal(403);
done(); done();
}); }).catch(done);
}); });
}); });
@ -261,7 +490,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403); err.statusCode.should.equal(403);
done(); done();
}); }).catch(done);
}); });
}); });