From 23afa8a073929a46714c05225bf166596a24c438 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Mon, 13 Feb 2017 10:30:51 -0500 Subject: [PATCH] Allow digest to be cleared by passing null --- README.md | 2 ++ lib/secure-password.js | 36 ++++++++++++++++++++++----- test/secure-password.spec.js | 48 ++++++++++++++++++++++++++++++------ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 788f2e6..1eb795e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ function signIn (email, password) { * BCrypt requires that passwords are 72 characters maximum (it ignores characters after 72). * This library uses the bcrypt synchronous methods when setting a password. This is to ensure the raw password is never stored on the model (in memory, or otherwise). * This library enables the built-in `virtuals` plugin on Bookshelf. +* Passing a `null` value to the password will clear the `password_digest`. +* Passing `undefined` or a zero-length string to the password will leave the `password_digest` as-is ## Testing diff --git a/lib/secure-password.js b/lib/secure-password.js index c034eb2..189814d 100644 --- a/lib/secure-password.js +++ b/lib/secure-password.js @@ -8,6 +8,12 @@ module.exports = function (Bookshelf) { Bookshelf.plugin('virtuals') + /** + * Get the password field from the plugin configuration. defaults to `password_digest` + * + * @param {Model} model - the Bookshelf model + * @returns {String} - The database column name for the password digest + */ function passwordField (model) { if (typeof model.hasSecurePassword === 'string' || model.hasSecurePassword instanceof String) { return model.hasSecurePassword @@ -16,7 +22,21 @@ module.exports = function (Bookshelf) { return DEFAULT_PASSWORD_FIELD } - var Model = Bookshelf.Model.extend({ + /** + * Checks if a string is empty (null, undefined, or length of zero) + * + * @param {String} str - A string + * @returns {Boolean} - Whether or not the string is empty + */ + function isEmpty (str) { + if (str === undefined || str === null) { + return true + } + + return ('' + str).length === 0 + } + + const Model = Bookshelf.Model.extend({ hasSecurePassword: false, constructor: function () { @@ -27,11 +47,15 @@ module.exports = function (Bookshelf) { this.virtuals = this.virtuals || {} this.virtuals.password = { - get: function () {}, - set: function (value) { - let salt = bcrypt.genSaltSync(DEFAULT_SALT_ROUNDS) - let passwordDigest = bcrypt.hashSync(value, salt) - this.set(passwordDigestField, passwordDigest) + get: function getPassword () {}, + set: function setPassword (value) { + if (value === null) { + this.set(passwordDigestField, null) + } else if (!isEmpty(value)) { + let salt = bcrypt.genSaltSync(DEFAULT_SALT_ROUNDS) + let digest = bcrypt.hashSync(value, salt) + this.set(passwordDigestField, digest) + } } } } diff --git a/test/secure-password.spec.js b/test/secure-password.spec.js index 00bc33f..e574d40 100644 --- a/test/secure-password.spec.js +++ b/test/secure-password.spec.js @@ -11,17 +11,49 @@ describe('bookshelf-secure-password', function () { }) describe('#constructor', function () { - it('does not keep the raw password on the model', function () { - const Model = bookshelf.Model.extend({ - hasSecurePassword: true + describe('with the default column', function () { + let model + + before(function () { + const Model = bookshelf.Model.extend({ + hasSecurePassword: true + }) + + model = new Model({ password: 'testing' }) }) - let model = new Model({ password: 'testing' }) - expect(model.get('password')).to.be.undefined - expect(model.attributes.password).to.be.undefined + it('does not keep the raw password on the model', function () { + expect(model.get('password')).to.be.undefined + expect(model.attributes.password).to.be.undefined + + expect(model.get('password_digest')).to.be.a.string + expect(model.attributes.password_digest).to.be.a.string + }) + + it('sets the password digest field to null if given a `null` value', function () { + expect(model.get('password_digest')).to.be.a.string + model.set('password', null) + expect(model.get('password_digest')).to.be.null + }) - expect(model.get('password_digest')).to.be.a.string - expect(model.attributes.password_digest).to.be.a.string + it('does not change the password digest if given undefined', function () { + let originalString = model.get('password_digest') + model.set('password', undefined) + expect(model.get('password_digest')).to.equal(originalString) + }) + + it('does not change the password digest if given an empty string', function () { + let originalString = model.get('password_digest') + model.set('password', '') + expect(model.get('password_digest')).to.equal(originalString) + }) + + it('changes the password digest if given a blank (spaces-only) string', function () { + let originalString = model.get('password_digest') + model.set('password', ' ') + expect(model.get('password_digest')).to.be.a.string + expect(model.get('password_digest')).not.to.equal(originalString) + }) }) it('allows the default column to be overwritten', function () {