Skip to content

Commit

Permalink
Merge pull request #54 from butlerx/enchanment/bruteforce-protection
Browse files Browse the repository at this point in the history
Add brute force protection for login
  • Loading branch information
mihaidma committed Aug 12, 2016
2 parents 2e680c5 + 2c6e3c9 commit 67f3ad8
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 5 deletions.
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -120,6 +120,7 @@ contains an `ok` field that is either true or false, indicating the success or f
* `mustrepeat`: you must provide a `repeat` argument (a repeat of the password) when setting a password
* `resetperiod`: duration in millis that a password reset token is valid, default: 24 hours
* `pepper`: used in addition to password salts, a pepper is very similar but is stored in code instead of a database.
* `lockTry`: number of failed attempts before the account is locked, default 3, set to null to disable this feature

To set options, do so when you load the plugin:

Expand Down Expand Up @@ -430,6 +431,19 @@ Object with properties:

* `ok`: true if operation is OK

### role:user, cmd:set_user_lock

Sets the lock on users account

#### Argumants:

* `id`: the id of the user
* `failTry`: boolean to say if the login failed or not
* `why`: why the accouunts lock status is being changed

#### Provides:

* `ok`: true if the account was unloocked or false if the lock was incremented

## Logging

Expand Down
1 change: 1 addition & 0 deletions default-options.json
Expand Up @@ -7,6 +7,7 @@
"confirm": false,
"oldsha": true,
"pepper": "",
"lockTry": null,
"user": {
"fields": [
{
Expand Down
148 changes: 148 additions & 0 deletions test/lock-unlock.test.js
@@ -0,0 +1,148 @@
'use strict'

var Seneca = require('seneca')

var _ = require('lodash')

var Lab = require('lab')
var Code = require('code')
var lab = exports.lab = Lab.script()
var suite = lab.suite
var test = lab.test
var before = lab.before
var expect = Code.expect
var wrongPassword = 'fail1fail'
var resetPassword = 'reset1reset'

var si = Seneca()
if (si.version >= '2.0.0'){
si
.use(require('seneca-entity'))
}
si.use('../user', {lockTry: 3})

var user1Data = {
nick: 'nick1',
email: 'nick1@example.com',
password: 'test1test',
active: true,
}

var user2Data = {
nick: 'nick2',
email: 'nick2@example.com',
password: 'test2test',
active: true,
}

suite('seneca-user update suite tests ', function () {
before({}, function (done) {
si.ready(function (err) {
if (err) return process.exit(!console.error(err))
done()
})
})

test('user/register test', function (done) {
si.act(_.extend({role: 'user', cmd: 'register'}, user1Data), function (err, data) {
expect(err).to.not.exist()
expect(data.user.nick).to.equal(user1Data.nick)
done(err)
})
})

test('user/register test', function (done) {
si.act(_.extend({role: 'user', cmd: 'register'}, user2Data), function (err, data) {
expect(err).to.not.exist()
expect(data.user.nick).to.equal(user2Data.nick)
done(err)
})
})

var user2Id
test('user/login user test', function (done) {
si.act({role: 'user', cmd: 'login', nick: user2Data.nick, password: user2Data.password}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.true()
user2Id = data.user.id
done(err)
})
})

for(var i=0; i<3; i++) {
test('user/login user test', function (done) {
si.act({role: 'user', cmd: 'login', nick: user1Data.nick, password: wrongPassword}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.false()
done(err)
})
})

test('user/user lock test', function (done) {
si.act({role: 'user', cmd: 'set_user_lock', id: user2Id, failTry: true}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.false()
done(err)
})
})
}

test('user/login user test', function (done) {
si.act({role: 'user', cmd: 'login', nick: user1Data.nick, password: user1Data.password}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.false()
done(err)
})
})

test('user/login user test', function (done) {
si.act({role: 'user', cmd: 'login', nick: user2Data.nick, password: user2Data.password}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.false()
done(err)
})
})

test('user/user unlock test', function (done) {
si.act({role: 'user', cmd: 'set_user_lock', id: user2Id}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.true()
done(err)
})
})

var resetId
test('user/create_reset unknown user test', function (done) {
si.act({ role: 'user', cmd: 'create_reset', nick: user1Data.nick }, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.true()
expect(data.reset.id).to.exist()
resetId = data.reset.token
done(err)
})
})

test('user/create_reset unknown user test', function (done) {
si.act({ role: 'user', cmd: 'execute_reset', token: resetId, password: resetPassword, repeat: resetPassword }, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.true()
done(err)
})
})

test('user/login user test', function (done) {
si.act({role: 'user', cmd: 'login', nick: user1Data.nick, password: resetPassword}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.true()
done(err)
})
})

test('user/login user test', function (done) {
si.act({role: 'user', cmd: 'login', nick: user2Data.nick, password: user2Data.password}, function (err, data) {
expect(err).to.not.exist()
expect(data.ok).to.be.true()
done(err)
})
})
})
59 changes: 54 additions & 5 deletions user.js
Expand Up @@ -219,6 +219,12 @@ module.exports = function user (options) {
cmd_update)


// ### Set user lock
// Pattern: _**role**:user, **cmd**:set_user_lock_
seneca.add({role: role, cmd: 'set_user_lock'},
cmd_set_user_lock)


// ### Enable User - DEPRECATED
// Replaced with **activate** command
seneca.add({role: role, cmd: 'enable'}, // keep this for backward compatibility
Expand Down Expand Up @@ -485,6 +491,7 @@ module.exports = function user (options) {
return done(null, out)
}

user.lockTry = 0
user.salt = out.salt
user.pass = out.pass
user.save$(function (err, user) {
Expand Down Expand Up @@ -526,6 +533,7 @@ module.exports = function user (options) {
user.name = args.name || ''
user.active = void 0 === args.active ? true : args.active
user.when = new Date().toISOString()
user.lockTry = 0;

if (options.confirm) {
user.confirmed = args.confirmed || false
Expand Down Expand Up @@ -612,7 +620,7 @@ module.exports = function user (options) {
// - password: password text, alias: pass
// Provides:
// - success: {ok:true,user:,login:}
// - failure: {ok:false,why:,nick:}
// - failure: {ok:false,why:,user:}
function cmd_login (args, done) {
var seneca = this
var user = args.user
Expand All @@ -625,6 +633,10 @@ module.exports = function user (options) {
return done(null, {ok: false, why: why, user: user})
}

if (user.lockTry >= options.lockTry && !_.isNull(options.lockTry)) {
seneca.log.debug('login/fail', why = 'locked-out', user)
return done(null, {ok: false, why: why, user: user})
}
if (args.auto) {
return make_login(user, 'auto')
}
Expand All @@ -633,7 +645,10 @@ module.exports = function user (options) {
if (err) return done(err)
if (!out.ok) {
seneca.log.debug('login/fail', why = 'invalid-password', user)
return done(null, {ok: false, why: why})
seneca.act({role: role, cmd: 'set_user_lock', id: user.id, failTry: true, why: why},function (err, out) {
if (err) return done(err)
done(null, {ok: false, why: why, user:user})
})
}
else return make_login(user, 'password')
})
Expand All @@ -660,13 +675,47 @@ module.exports = function user (options) {

login.save$(function (err, login) {
if (err) return done(err)

seneca.log.debug('login/ok', why, user, login)
done(null, {ok: true, user: user, login: login, why: why})
seneca.act({role: role, cmd: 'set_user_lock', id: user.id}, function (err, out) {
if (err) return done(err)
seneca.log.debug('login/ok', why, user, login)
done(null, {ok: true, user: user, login: login, why: why})
})
})
}
}

// Unlocks accounts that a user has been locked out of due to failled password attempts
// Or let the lock if the user failed to login to their account
// - id, user id to unlock
// - failTry, boolean to say login failed
// - why, why account lock was changed
// Provides:
// - success: {ok:true, why:}
// - failure: {ok:false, why:}
function cmd_set_user_lock (args, done) {
var seneca = this
var why
var userent = seneca.make(user_canon)
var ok

userent.load$({id: args.id}, function (err, user) {
if (err) return done(err, {ok: false, why: err})
if(!_.isUndefined(args.failTry) && args.failTry) {
user.lockTry = user.lockTry + 1
ok = false
why = args.why
} else {
user.lockTry = 0
ok = true
why = 'account-unlocked'
}
user.save$(function (err, user) {
if (err) return done(err, {ok: false, why: err})
seneca.log.debug('user/lock', why, user)
done(null, {ok: ok, why: why})
})
})
}

// Confirm an existing user - using confirm code sent to user
// - nick, email: to resolve user
Expand Down

0 comments on commit 67f3ad8

Please sign in to comment.