From 713001913e71256b1ea272274e6c64cb5b417c05 Mon Sep 17 00:00:00 2001 From: jakerella Date: Thu, 12 Mar 2015 10:56:43 -0400 Subject: [PATCH] Ability to pass in custom verification token generator This commit adds the ability for the developer to use a custom token generator function for the user.verify(...) method. By default, the system will still use the crypto.randomBytes() method if no option is provided. --- common/models/user.js | 59 +++++++++++++++++++++--------- test/user.test.js | 85 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 18 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index dcbd2813..94136a33 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -303,11 +303,12 @@ module.exports = function(User) { * * ```js * var options = { - * type: 'email', - * to: user.email, - * template: 'verify.ejs', - * redirect: '/' - * }; + * type: 'email', + * to: user.email, + * template: 'verify.ejs', + * redirect: '/', + * tokenGenerator: function (user, cb) { cb("random-token"); } + * }; * * user.verify(options, next); * ``` @@ -323,6 +324,11 @@ module.exports = function(User) { * page, for example, `'verify.ejs'. * @property {String} redirect Page to which user will be redirected after * they verify their email, for example `'/'` for root URI. + * @property {Function} generateVerificationToken A function to be used to + * generate the verification token. It must accept the user object and a + * callback function. This function should NOT add the token to the user + * object, instead simply execute the callback with the token! User saving + * and email sending will be handled in the `verify()` method. */ User.prototype.verify = function(options, fn) { @@ -360,19 +366,20 @@ module.exports = function(User) { // Email model var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); - crypto.randomBytes(64, function(err, buf) { - if (err) { - fn(err); - } else { - user.verificationToken = buf.toString('hex'); - user.save(function(err) { - if (err) { - fn(err); - } else { - sendEmail(user); - } - }); - } + // Set a default token generation function if one is not provided + var tokenGenerator = options.generateVerificationToken || User.generateVerificationToken; + + tokenGenerator(user, function(err, token) { + if (err) { return fn(err); } + + user.verificationToken = token; + user.save(function(err) { + if (err) { + fn(err); + } else { + sendEmail(user); + } + }); }); // TODO - support more verification types @@ -401,6 +408,22 @@ module.exports = function(User) { } }; + /** + * A default verification token generator which accepts the user the token is + * being generated for and a callback function to indicate completion. + * This one uses the crypto library and 64 random bytes (converted to hex) + * for the token. When used in combination with the user.verify() method this + * function will be called with the `user` object as it's context (`this`). + * + * @param {object} user The User this token is being generated for. + * @param {Function} cb The generator must pass back the new token with this function call + */ + User.generateVerificationToken = function(user, cb) { + crypto.randomBytes(64, function(err, buf) { + cb(err, buf && buf.toString('hex')); + }); + }; + /** * Confirm the user's identity. * diff --git a/test/user.test.js b/test/user.test.js index baf5d6cc..56aae9e9 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -842,6 +842,91 @@ describe('User', function() { }); }); + it('Verify a user\'s email address with custom token generator', function(done) { + User.afterRemote('create', function(ctx, user, next) { + assert(user, 'afterRemote should include result'); + + var options = { + type: 'email', + to: user.email, + from: 'noreply@myapp.org', + redirect: '/', + protocol: ctx.req.protocol, + host: ctx.req.get('host'), + generateVerificationToken: function(user, cb) { + assert(user); + assert.equal(user.email, 'bar@bat.com'); + assert(cb); + assert.equal(typeof cb, 'function'); + // let's ensure async execution works on this one + process.nextTick(function() { + cb(null, 'token-123456'); + }); + } + }; + + user.verify(options, function(err, result) { + assert(result.email); + assert(result.email.response); + assert(result.token); + assert.equal(result.token, 'token-123456'); + var msg = result.email.response.toString('utf-8'); + assert(~msg.indexOf('token-123456')); + done(); + }); + }); + + request(app) + .post('/users') + .expect('Content-Type', /json/) + .expect(200) + .send({email: 'bar@bat.com', password: 'bar'}) + .end(function(err, res) { + if (err) { + return done(err); + } + }); + }); + + it('Fails if custom token generator returns error', function(done) { + User.afterRemote('create', function(ctx, user, next) { + assert(user, 'afterRemote should include result'); + + var options = { + type: 'email', + to: user.email, + from: 'noreply@myapp.org', + redirect: '/', + protocol: ctx.req.protocol, + host: ctx.req.get('host'), + generateVerificationToken: function(user, cb) { + // let's ensure async execution works on this one + process.nextTick(function() { + cb(new Error('Fake error')); + }); + } + }; + + user.verify(options, function(err, result) { + assert(err); + assert.equal(err.message, 'Fake error'); + assert.equal(result, undefined); + done(); + }); + }); + + request(app) + .post('/users') + .expect('Content-Type', /json/) + .expect(200) + .send({email: 'bar@bat.com', password: 'bar'}) + .end(function(err, res) { + if (err) { + return done(err); + } + }); + }); + }); describe('User.confirm(options, fn)', function() {