Refactor authentication API into pipeline format

Refs #5508
This commit is contained in:
Jason Williams 2016-03-06 12:18:33 -06:00
parent 244a444e38
commit fe13503470
6 changed files with 711 additions and 242 deletions

View File

@ -1,54 +1,125 @@
var _ = require('lodash'),
validator = require('validator'),
pipeline = require('../utils/pipeline'),
dataProvider = require('../models'),
settings = require('./settings'),
mail = require('./mail'),
globalUtils = require('../utils'),
utils = require('./utils'),
Promise = require('bluebird'),
errors = require('../errors'),
config = require('../config'),
i18n = require('../i18n'),
authentication;
function setupTasks(object) {
var setupUser,
internal = {context: {internal: true}};
return utils.checkObject(object, 'setup').then(function (checkedSetupData) {
setupUser = {
name: checkedSetupData.setup[0].name,
email: checkedSetupData.setup[0].email,
password: checkedSetupData.setup[0].password,
blogTitle: checkedSetupData.setup[0].blogTitle,
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.
if (!_.isEmpty(setupUser.blogTitle)) {
userSettings.push({key: 'title', value: setupUser.blogTitle});
userSettings.push({key: 'description', value: i18n.t('common.api.authentication.sampleBlogDescription')});
}
setupUser = user.toJSON(internal);
return settings.edit({settings: userSettings}, {context: {user: setupUser.id}});
}).then(function () {
return Promise.resolve(setupUser);
/**
* Returns setup status
*
* @return {Promise<Boolean>}
*/
function checkSetup() {
return authentication.isSetup().then(function then(result) {
return result.setup[0].status;
});
}
/**
* Allows an assertion to be made about setup status.
*
* @param {Boolean} status True: setup must be complete. False: setup must not be complete.
* @return {Function} returns a "task ready" function
*/
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'
};
});
}
function setupUser(userData) {
var context = {context: {internal: true}},
User = dataProvider.User;
return User.findOne({role: 'Owner', status: 'all'}).then(function then(owner) {
if (!owner) {
throw new errors.InternalServerError(
i18n.t('errors.api.authentication.setupUnableToRun')
);
}
return User.setup(userData, _.extend({id: owner.id}, context));
}).then(function then(user) {
return {
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
*
@ -57,266 +128,420 @@ function setupTasks(object) {
authentication = {
/**
* ## Generate Reset Token
* generate a reset token for a given email address
* @param {Object} object
* @returns {Promise(passwordreset)} message
* @description generate a reset token for a given email address
* @param {Object} resetRequest
* @returns {Promise<Object>} message
*/
generateResetToken: function generateResetToken(object) {
var expires = Date.now() + globalUtils.ONE_DAY_MS,
email;
generateResetToken: function generateResetToken(resetRequest) {
var tasks;
return authentication.isSetup().then(function (result) {
var setup = result.setup[0].status;
function validateRequest(resetRequest) {
return utils.checkObject(resetRequest, 'passwordreset').then(function then(data) {
var email = data.passwordreset[0].email;
if (!setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
}
if (typeof email !== 'string' || !validator.isEmail(email)) {
throw new errors.BadRequestError(
i18n.t('errors.api.authentication.noEmailProvided')
);
}
return utils.checkObject(object, 'passwordreset');
}).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 email;
});
}
return settings.read({context: {internal: true}, key: 'dbHash'})
.then(function (response) {
var dbHash = response.settings[0].value;
return dataProvider.User.generateResetToken(email, expires, dbHash);
}).then(function (resetToken) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + globalUtils.encodeBase64URLsafe(resetToken) + '/';
function generateToken(email) {
var settingsQuery = {context: {internal: true}, key: 'dbHash'};
return mail.generateContent({data: {resetUrl: resetUrl}, template: 'reset-password'});
}).then(function (emailContent) {
return settings.read(settingsQuery).then(function then(response) {
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,
resetUrl = baseUrl.replace(/\/$/, '') +
'/ghost/reset/' +
globalUtils.encodeBase64URLsafe(data.resetToken) + '/';
return mail.generateContent({
data: {
resetUrl: resetUrl
},
template: 'reset-password'
}).then(function then(content) {
var payload = {
mail: [{
message: {
to: email,
to: data.email,
subject: i18n.t('common.api.authentication.mail.resetPassword'),
html: emailContent.html,
text: emailContent.text
html: content.html,
text: content.text
},
options: {}
}]
};
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 if a valid token and password (2x) is passed
* @param {Object} object
* @returns {Promise(passwordreset)} message
* @param {Object} resetRequest
* @returns {Promise<Object>} message
*/
resetPassword: function resetPassword(object) {
var resetToken,
newPassword,
ne2Password;
resetPassword: function resetPassword(resetRequest) {
var tasks;
return authentication.isSetup().then(function (result) {
var setup = result.setup[0].status;
function validateRequest(resetRequest) {
return utils.checkObject(resetRequest, 'passwordreset');
}
if (!setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
}
function doReset(resetRequest) {
var settingsQuery = {context: {internal: true}, key: 'dbHash'},
data = resetRequest.passwordreset[0],
resetToken = data.token,
newPassword = data.newPassword,
ne2Password = data.ne2Password;
return utils.checkObject(object, 'passwordreset');
}).then(function (checkedPasswordReset) {
resetToken = checkedPasswordReset.passwordreset[0].token;
newPassword = checkedPasswordReset.passwordreset[0].newPassword;
ne2Password = checkedPasswordReset.passwordreset[0].ne2Password;
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
var dbHash = response.settings[0].value;
return settings.read(settingsQuery).then(function then(response) {
return dataProvider.User.resetPassword({
token: resetToken,
newPassword: newPassword,
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) {
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
* @param {User} object the user to create
* @returns {Promise(User}} Newly created user
* @param {Object} invitation an invitation object
* @returns {Promise<Object>}
*/
acceptInvitation: function acceptInvitation(object) {
var resetToken,
newPassword,
ne2Password,
name,
email;
acceptInvitation: function acceptInvitation(invitation) {
var tasks;
return authentication.isSetup().then(function (result) {
var setup = result.setup[0].status;
function validateInvitation(invitation) {
return utils.checkObject(invitation, 'invitation');
}
if (!setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
}
function processInvitation(invitation) {
var User = dataProvider.User,
settingsQuery = {context: {internal: true}, key: 'dbHash'},
data = invitation.invitation[0],
resetToken = data.token,
newPassword = data.password,
email = data.email,
name = data.name;
return utils.checkObject(object, 'invitation');
}).then(function (checkedInvitation) {
resetToken = checkedInvitation.invitation[0].token;
newPassword = checkedInvitation.invitation[0].password;
ne2Password = checkedInvitation.invitation[0].password;
email = checkedInvitation.invitation[0].email;
name = checkedInvitation.invitation[0].name;
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
var dbHash = response.settings[0].value;
return dataProvider.User.resetPassword({
return settings.read(settingsQuery).then(function then(response) {
return User.resetPassword({
token: resetToken,
newPassword: newPassword,
ne2Password: ne2Password,
dbHash: dbHash
ne2Password: newPassword,
dbHash: response.settings[0].value
});
}).then(function (user) {
// Setting the slug to '' has the model regenerate the slug from the user's name
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')}]});
}).then(function then(user) {
return User.edit({name: name, email: email, slug: ''}, {id: user.id});
}).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
* @param {Object} options
* @param {string} options.email The email to check for an invitation on
* @returns {Promise(Invitation}} An invitation status
* @returns {Promise<Object>} An invitation status
*/
isInvitation: function isInvitation(options) {
return authentication.isSetup().then(function (result) {
var setup = result.setup[0].status;
var tasks,
localOptions = _.cloneDeep(options || {});
if (!setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupMustBeCompleted')));
function processArgs(options) {
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 dataProvider.User.findOne({email: options.email, status: 'invited'}).then(function (response) {
if (response) {
return {invitation: [{valid: true}]};
} else {
return {invitation: [{valid: false}]};
}
return email;
}
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() {
return dataProvider.User.query(function (qb) {
qb.whereIn('status', ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked']);
}).fetch().then(function (users) {
if (users) {
return Promise.resolve({setup: [{status: true}]});
} else {
return Promise.resolve({setup: [{status: false}]});
}
});
var tasks,
validStatuses = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'];
function checkSetupStatus() {
return dataProvider.User
.where('status', 'in', validStatuses)
.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) {
var setup = result.setup[0].status;
if (setup) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.setupAlreadyCompleted')));
}
return setupTasks(object);
}).then(function (result) {
setupUser = result;
function doSetup(setupDetails) {
return setupTasks(setupDetails);
}
function sendNotification(setupUser) {
var data = {
ownerEmail: setupUser.email
};
return mail.generateContent({data: data, template: 'welcome'});
}).then(function (emailContent) {
var message = {
to: setupUser.email,
subject: i18n.t('common.api.authentication.mail.yourNewGhostBlog'),
html: emailContent.html,
text: emailContent.text
},
payload = {
mail: [{
message: message,
options: {}
}]
};
return mail.generateContent({data: data, template: 'welcome'})
.then(function then(content) {
var message = {
to: setupUser.email,
subject: i18n.t('common.api.authentication.mail.yourNewGhostBlog'),
html: content.html,
text: content.text
},
payload = {
mail: [{
message: message,
options: {}
}]
};
mail.send(payload, {context: {internal: true}}).catch(function (error) {
errors.logError(
error.message,
i18n.t('errors.api.authentication.unableToSendWelcomeEmail', {url: 'http://support.ghost.org/mail/'}),
i18n.t('errors.api.authentication.checkEmailConfigInstructions')
);
});
}).then(function () {
return Promise.resolve({users: [setupUser]});
});
},
updateSetup: function updateSetup(object, options) {
if (!options.context || !options.context.user) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.notLoggedIn')));
mail.send(payload, {context: {internal: true}}).catch(function (error) {
errors.logError(
error.message,
i18n.t(
'errors.api.authentication.unableToSendWelcomeEmail',
{url: 'http://support.ghost.org/mail/'}
),
i18n.t('errors.api.authentication.checkEmailConfigInstructions')
);
});
})
.return(setupUser);
}
return dataProvider.User.findOne({role: 'Owner', status: 'all'}).then(function (result) {
var user = result.toJSON();
function formatResponse(setupUser) {
return {users: [setupUser]};
}
if (user.id !== options.context.user) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner')));
tasks = [
assertSetupCompleted(false),
doSetup,
sendNotification,
formatResponse
];
return pipeline(tasks, setupDetails);
},
/**
* 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) {
throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner'));
}
return setupTasks(object);
}).then(function (result) {
return Promise.resolve({users: [result]});
});
},
revoke: function (object) {
var token;
if (object.token_type_hint && object.token_type_hint === 'access_token') {
token = dataProvider.Accesstoken;
} 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 _.assign({setupDetails: setupDetails}, options);
}
return token.destroyByToken({token: object.token}).then(function () {
return Promise.resolve({token: object.token});
}, function () {
// On error we still want a 200. See https://tools.ietf.org/html/rfc7009#page-5
return Promise.resolve({token: object.token, error: i18n.t('errors.api.authentication.invalidTokenProvided')});
});
function checkPermission(options) {
return dataProvider.User.findOne({role: 'Owner', status: 'all'})
.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);
},
/**
* 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 || {});
function processArgs(tokenDetails, options) {
return _.assign({}, tokenDetails, options);
}
function revokeToken(options) {
var providers = [
dataProvider.Refreshtoken,
dataProvider.Accesstoken
],
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);
}
};

View File

@ -17,6 +17,7 @@ var _ = require('lodash'),
EmailError = require('./email-error'),
DataImportError = require('./data-import-error'),
TooManyRequestsError = require('./too-many-requests-error'),
TokenRevocationError = require('./token-revocation-error'),
i18n = require('../i18n'),
config,
errors,
@ -431,3 +432,4 @@ module.exports.EmailError = EmailError;
module.exports.DataImportError = DataImportError;
module.exports.MethodNotAllowedError = MethodNotAllowedError;
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,17 +68,14 @@ Basetoken = ghostBookshelf.Model.extend({
var token = options.token;
options = this.filterOptions(options, 'destroyByUser');
options.require = true;
if (token) {
return ghostBookshelf.Collection.forge([], {model: this})
.query('where', 'token', '=', token)
.fetch(options)
.then(function then(collection) {
collection.invokeThen('destroy', options);
});
}
return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.base.token.tokenNotFound')));
return this.forge()
.query('where', 'token', '=', token)
.fetch(options)
.then(function then(model) {
return model.destroy(options);
});
}
});

View File

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

View File

@ -3,6 +3,10 @@ var testUtils = require('../../utils'),
should = require('should'),
sinon = require('sinon'),
Promise = require('bluebird'),
uid = require('../../../server/utils').uid,
Accesstoken,
Refreshtoken,
User,
// Stuff we are testing
@ -51,6 +55,37 @@ describe('Authentication API', function () {
should.exist(AuthAPI);
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 () {
// TODO: stub settings
beforeEach(testUtils.setup('roles', 'owner:pre', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
@ -89,6 +124,43 @@ describe('Authentication API', function () {
}).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) {
AuthAPI.acceptInvitation(testInvite).then(function () {
done(new Error('Invitation was allowed to be accepted'));
@ -99,7 +171,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403);
done();
});
}).catch(done);
});
it('should not generate a password reset token', function (done) {
@ -112,7 +184,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403);
done();
});
}).catch(done);
});
it('should not allow a password reset', function (done) {
@ -125,12 +197,18 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403);
done();
});
}).catch(done);
});
});
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) {
AuthAPI.isSetup().then(function (result) {
@ -158,7 +236,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403);
done();
});
}).catch(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.statusCode.should.equal(401);
err.message.should.equal('Invalid token structure');
done();
});
}).catch(done);
});
it('should generate a password reset token', function (done) {
@ -183,6 +262,23 @@ describe('Authentication API', function () {
}).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) {
AuthAPI.resetPassword(testReset).then(function () {
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.statusCode.should.equal(401);
err.message.should.equal('Invalid token structure');
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);
done();
});
}).catch(done);
});
});
@ -261,7 +490,7 @@ describe('Authentication API', function () {
err.statusCode.should.equal(403);
done();
});
}).catch(done);
});
});