* `lockAccount` takes both `lockedAt` and `unlockCode`
* Expose the `unlockCode` endpoint to get the unlock code
* Add an `/account/lock` endpoint. Used for testing.
* Sending an account unlock email requires the account to be locked.
* Add API docs for /account/lock
* Point to the mozilla repo for fxa-auth-db-mem
* Add an `enableLockout` configuration option.
* Extract the route removal code into a module.
* Add a new error `accountNotLocked`. Error is only returned for `/account/unlock/resend_code`
* A new `locked` event is logged whenever an account is locked. Can be used to determine the number of locked accounts over a timespan.
This commit is contained in:
Shane Tomlinson 2015-01-23 14:50:55 +00:00
Родитель 087abb2d99
Коммит cbad29619c
20 изменённых файлов: 2705 добавлений и 132 удалений

Просмотреть файл

@ -56,7 +56,7 @@ function main() {
.done(
function (db) {
database = db
customs = new Customs(config.customsUrl, database)
customs = new Customs(config.customsUrl)
var routes = require('../routes')(
log,
error,

Просмотреть файл

@ -236,6 +236,11 @@ module.exports = function (fs, path, url, convict) {
certPath: {
doc: "path to SSL certificate in PEM format if serving over https",
default: path.resolve(__dirname, '../cert.pem')
},
lockoutEnabled: {
doc: 'Is account lockout enabled',
format: Boolean,
default: false
}
})

Просмотреть файл

@ -20,5 +20,6 @@
"region": "us-east-1"
},
"customsUrl": "none",
"trustedJKUs": ["http://127.0.0.1:8080/.well-known/public-keys"]
"trustedJKUs": ["http://127.0.0.1:8080/.well-known/public-keys"],
"lockoutEnabled": true
}

Просмотреть файл

@ -7,8 +7,7 @@
module.exports = function (log, error) {
function Customs(url, db) {
this.db = db
function Customs(url) {
if (url === 'none') {
this.pool = {
post: function () { return P({ block: false })},
@ -57,20 +56,16 @@
}
)
.then(
(function (result) {
if (result.lockout) {
return this.db.lockAccount(account)
}
}).bind(this)
)
.then(
function () {},
function (result) {
return { lockout: !!result.lockout }
},
function (err) {
log.error({ op: 'customs.flag.1', email: email, err: err })
// If this happens, either:
// - (1) the url in config doesn't point to a real customs server
// - (2) the customs server returned an internal server error
// Either way, allow the request through so we fail open.
return { lockout: false }
}
)
}

Просмотреть файл

@ -4,6 +4,7 @@
var Pool = require('../pool')
var crypto = require('crypto')
var butil = require('../crypto/butil')
var unbuffer = butil.unbuffer
var bufferize = butil.bufferize
@ -440,19 +441,41 @@ module.exports = function (
}
DB.prototype.lockAccount = function (account) {
log.trace({ op: 'DB.lockAccount', uid: account && account.uid })
var unlockCode = crypto.randomBytes(16).toString('hex');
log.trace({ op: 'DB.lockAccount', uid: account && account.uid, unlockCode: unlockCode })
return this.pool.post(
'/account/' + account.uid.toString('hex') + '/lock',
{ lockedAt: Date.now() }
{
lockedAt: Date.now(),
unlockCode: unlockCode
}
)
}
DB.prototype.unlockAccount = function (account) {
log.trace({ op: 'DB.unlockAccount', uid: account && account.uid })
return this.pool.post(
'/account/' + account.uid.toString('hex') + '/unlock',
{ }
)
'/account/' + account.uid.toString('hex') + '/unlock'
);
}
DB.prototype.unlockCode = function (account) {
log.trace({ op: 'DB.unlockCode', uid: account && account.uid })
return this.pool.get(
'/account/' + account.uid.toString('hex') + '/unlockCode'
)
.then(
function (body) {
return bufferize(body).unlockCode
},
function (err) {
if (err.statusCode === 404) {
err = error.accountNotLocked()
}
throw err
}
)
}
DB.prototype.verifyEmail = function (account) {

Просмотреть файл

@ -81,6 +81,7 @@ The currently-defined error responses are:
* status code 400, errno 119: incorrect API version for this account
* status code 400, errno 120: incorrect email case
* status code 400, errno 121: account is locked
* status code 400, errno 122: account is not locked
* status code 503, errno 201: service temporarily unavailable to due high load (see [backoff protocol](#backoff-protocol))
* any status code, errno 999: unknown error
@ -110,6 +111,7 @@ Since this is a HTTP-based protocol, clients should be prepared to gracefully ha
* [GET /v1/account/keys (:lock: keyFetchToken) (verf-required)](#get-v1accountkeys)
* [POST /v1/account/reset (:lock: accountResetToken)](#post-v1accountreset)
* [POST /v1/account/destroy](#post-v1accountdestroy)
* [POST /v1/account/lock](#post-v1accountlock)
* Authentication
* [POST /v1/account/login](#post-v1accountlogin)
@ -508,6 +510,47 @@ Failing requests may be due to the following errors:
* status code 400, errno 120: incorrect email case
* status code 400, errno 121: account is locked
## POST /v1/account/lock
HAWK-authenticated.
This locks an account and prevents the user from performing any action that requires a password until their email address is re-verified. This endpoint is for testing only and is disabled for production.
__Parameters__
* email - the email address for the account
* authPW - the PBKDF2/HKDF stretched password as a hex string
### Request
```sh
curl -v \
-X POST \
-H "Content-Type: application/json" \
https://api-accounts.dev.lcip.org/v1/account/lock \
-d '{
"email": "me@example.com",
"authPW": "996bc6b1aa63cd69856a2ec81cbf19d5c8a604713362df9ee15c2bf07128efab"
}'
```
### Response
Successful requests will produce a "200 OK" and a json body.
```json
{}
```
Failing requests may be due to the following errors:
* status code 400, errno 102: attempt to access an account that does not exist
* status code 400, errno 103: incorrect password
* status code 400, errno 106: request body was not valid json
* status code 400, errno 107: request body contains invalid parameters
* status code 400, errno 108: request body missing required parameters
* status code 411, errno 112: content-length header was not provided
* status code 413, errno 113: request body too large
## POST /v1/account/unlock/resend_code
@ -552,6 +595,7 @@ Failing requests may be due to the following errors:
* status code 400, errno 108: request body missing required parameters
* status code 411, errno 112: content-length header was not provided
* status code 413, errno 113: request body too large
* status code 400, errno 122: account is not locked
## POST /v1/account/unlock/verify_code
@ -591,7 +635,6 @@ Successful requests will produce a "200 OK" response with an empty JSON body:
Failing requests may be due to the following errors:
* status code 400, errno 102: attempt to access an account that does not exist
* status code 400, errno 105: invalid verification code
* status code 400, errno 106: request body was not valid json
* status code 400, errno 107: request body contains invalid parameters

Просмотреть файл

@ -340,4 +340,18 @@ AppError.lockedAccount = function () {
})
}
module.exports = AppError
AppError.accountNotLocked = function (email) {
return new AppError(
{
code: 400,
error: 'Bad Request',
errno: 122,
message: 'Account is not locked'
},
{
email: email
}
)
}
module.exports = AppError;

2125
npm-shrinkwrap.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -35,7 +35,7 @@
"bunyan": "1.2.0",
"compute-cluster": "0.0.9",
"convict": "0.6",
"fxa-auth-mailer": "git+https://github.com/mozilla/fxa-auth-mailer.git#rfk/unlock-email",
"fxa-auth-mailer": "git+https://github.com/mozilla/fxa-auth-mailer.git#a2d00cf6",
"hapi": "7.5.3",
"hapi-auth-hawk": "1.1.1",
"hkdf": "0.0.2",
@ -50,7 +50,7 @@
},
"devDependencies": {
"ass": "0.0.4",
"fxa-auth-db-mem": "0.18.2",
"fxa-auth-db-mem": "git+https://github.com/mozilla/fxa-auth-db-mem.git#49e33d4a",
"grunt": "0.4.5",
"grunt-cli": "0.1.13",
"grunt-contrib-jshint": "0.10.0",

Просмотреть файл

@ -24,7 +24,8 @@ module.exports = function (
domain,
resendBlackoutPeriod,
customs,
isPreVerified
isPreVerified,
checkPassword
) {
var routes = [
@ -228,61 +229,56 @@ module.exports = function (
if (emailRecord.lockedAt) {
throw error.lockedAccount()
}
var password = new Password(
authPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.matches(emailRecord.verifyHash)
.then(
function (match) {
if (!match) {
return customs.flag(request.app.clientAddress, emailRecord)
.then(
function () {
throw error.incorrectPassword(emailRecord.email, email)
}
)
}
return db.createSessionToken(
{
uid: emailRecord.uid,
email: emailRecord.email,
emailCode: emailRecord.emailCode,
emailVerified: emailRecord.emailVerified,
verifierSetAt: emailRecord.verifierSetAt
return checkPassword(emailRecord, authPW, request.app.clientAddress)
.then(
function (match) {
if (!match) {
throw error.incorrectPassword(emailRecord.email, email)
}
)
}
)
.then(
function (sessionToken) {
if (request.query.keys !== 'true') {
return P({
sessionToken: sessionToken
})
}
return password.unwrap(emailRecord.wrapWrapKb)
.then(
function (wrapKb) {
return db.createKeyFetchToken(
{
uid: emailRecord.uid,
kA: emailRecord.kA,
wrapKb: wrapKb,
emailVerified: emailRecord.emailVerified
}
)
}
)
.then(
function (keyFetchToken) {
return {
sessionToken: sessionToken,
keyFetchToken: keyFetchToken
return db.createSessionToken(
{
uid: emailRecord.uid,
email: emailRecord.email,
emailCode: emailRecord.emailCode,
emailVerified: emailRecord.emailVerified,
verifierSetAt: emailRecord.verifierSetAt
}
)
}
)
.then(
function (sessionToken) {
if (request.query.keys !== 'true') {
return P({
sessionToken: sessionToken
})
}
)
var password = new Password(
authPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.unwrap(emailRecord.wrapWrapKb)
.then(
function (wrapKb) {
return db.createKeyFetchToken(
{
uid: emailRecord.uid,
kA: emailRecord.kA,
wrapKb: wrapKb,
emailVerified: emailRecord.emailVerified
}
)
}
)
.then(
function (keyFetchToken) {
return {
sessionToken: sessionToken,
keyFetchToken: keyFetchToken
}
}
)
}
)
}
@ -530,6 +526,8 @@ module.exports = function (
handler: function (request, reply) {
log.begin('Account.UnlockCodeResend', request)
var email = request.payload.email
var emailRecord
customs.check(
request.app.clientAddress,
email,
@ -538,10 +536,20 @@ module.exports = function (
db.emailRecord.bind(db, email)
)
.then(
function (emailRecord) {
function (_emailRecord) {
if (! _emailRecord.lockedAt) {
throw error.accountNotLocked(email)
}
emailRecord = _emailRecord
return db.unlockCode(emailRecord)
}
)
.then(
function (unlockCode) {
return mailer.sendUnlockCode(
emailRecord,
emailRecord.emailCode,
unlockCode,
{
service: request.payload.service,
redirectTo: request.payload.redirectTo,
@ -579,14 +587,19 @@ module.exports = function (
function (account) {
// If the account isn't actually locked, they may be
// e.g. clicking a stale link. Silently succeed.
if (!account.lockedAt) {
return true
if (! account.lockedAt) {
return
}
if (!butil.buffersAreEqual(code, account.emailCode)) {
throw error.invalidVerificationCode()
}
log.event('unlocked', { email: account.email, uid: account.uid })
return db.unlockAccount(account)
return db.unlockCode(account)
.then(
function (expectedCode) {
if (!butil.buffersAreEqual(code, expectedCode)) {
throw error.invalidVerificationCode()
}
log.event('unlocked', { email: account.email, uid: account.uid })
return db.unlockAccount(account)
}
)
}
)
.done(
@ -670,21 +683,12 @@ module.exports = function (
if (emailRecord.lockedAt) {
throw error.lockedAccount()
}
var password = new Password(
authPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.matches(emailRecord.verifyHash)
return checkPassword(emailRecord, authPW, request.app.clientAddress)
.then(
function (match) {
if (!match) {
return customs.flag(request.app.clientAddress, emailRecord)
.then(
function () {
throw error.incorrectPassword(emailRecord.email, form.email)
}
)
throw error.incorrectPassword(emailRecord.email, form.email)
}
return db.deleteAccount(emailRecord)
}
@ -704,6 +708,60 @@ module.exports = function (
if (isProduction) {
delete routes[0].config.validate.payload.preVerified
} else {
// programmatic account lockout is only available in non-production mode.
routes.push({
method: 'POST',
path: '/account/lock',
config: {
validate: {
payload: {
email: validators.email().required(),
authPW: isA.string().min(64).max(64).regex(HEX_STRING).required()
}
}
},
handler: function (request, reply) {
log.begin('Account.lock', request)
var form = request.payload
var email = form.email
var authPW = Buffer(form.authPW, 'hex')
customs.check(
request.app.clientAddress,
email,
'accountLock')
.then(db.emailRecord.bind(db, email))
.then(
function (emailRecord) {
// The account is already locked, silently succeed.
if (emailRecord.lockedAt) {
return true
}
return checkPassword(emailRecord, authPW, request.app.clientAddress)
.then(
function (match) {
// a bit of a strange one, only lock the account if the
// password matches, otherwise let customs handle any account
// lock.
if (! match) {
throw error.incorrectPassword(emailRecord.email, email)
}
log.event('locked', { email: emailRecord.email, uid: emailRecord.uid })
return db.lockAccount(emailRecord)
}
)
}
)
.done(
function () {
reply({})
},
reply
)
}
});
}
return routes

Просмотреть файл

@ -25,6 +25,7 @@ module.exports = function (
var isPreVerified = require('../preverifier')(jwks, error, config)
var defaults = require('./defaults')(log, P, db, error)
var idp = require('./idp')(log, serverPublicKey)
var checkPassword = require('./utils/password_check')(log, config, Password, customs, db)
var account = require('./account')(
log,
crypto,
@ -41,7 +42,8 @@ module.exports = function (
config.domain,
config.smtp.resendBlackoutPeriod,
customs,
isPreVerified
isPreVerified,
checkPassword
)
var password = require('./password')(
log,
@ -52,7 +54,8 @@ module.exports = function (
config.smtp.redirectDomain,
mailer,
config.verifierVersion,
customs
customs,
checkPassword
)
var session = require('./session')(log, isA, error, db)
var sign = require('./sign')(log, isA, error, signer, db, config.domain)

Просмотреть файл

@ -17,7 +17,8 @@ module.exports = function (
redirectDomain,
mailer,
verifierVersion,
customs
customs,
checkPassword
) {
function failVerifyAttempt(passwordForgotToken) {
@ -49,22 +50,17 @@ module.exports = function (
if (emailRecord.lockedAt) {
throw error.lockedAccount()
}
var password = new Password(
oldAuthPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.matches(emailRecord.verifyHash)
return checkPassword(emailRecord, oldAuthPW, request.app.clientAddress)
.then(
function (match) {
if (!match) {
return customs.flag(request.app.clientAddress, emailRecord)
.then(
function () {
throw error.incorrectPassword(emailRecord.email, form.email)
}
)
throw error.incorrectPassword(emailRecord.email, form.email)
}
var password = new Password(
oldAuthPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.unwrap(emailRecord.wrapWrapKb)
}
)

Просмотреть файл

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Check if the password a user entered matches the one on
* file for the account. If it does not, flag the account with
* customs. If customs says the account should be locked,
* lock the acocunt. Higher levels will take care of
* returning an error to the user.
*/
module.exports = function (log, config, Password, customs, db) {
return function (emailRecord, authPW, clientAddress) {
var password = new Password(
authPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.matches(emailRecord.verifyHash)
.then(
function (match) {
if (match) {
return match
}
return customs.flag(clientAddress, emailRecord)
.then(
function (result) {
if (result.lockout && config.lockoutEnabled) {
log.event('locked', { email: emailRecord.email, uid: emailRecord.uid })
return db.lockAccount(emailRecord)
}
}
)
.then(
function () {
return match
}
)
}
)
}
}

Просмотреть файл

@ -369,6 +369,18 @@ ClientApi.prototype.passwordForgotStatus = function (passwordForgotTokenHex) {
)
}
ClientApi.prototype.accountLock = function (email, authPW) {
return this.doRequest(
'POST',
this.baseURL + '/account/lock',
null,
{
email: email,
authPW: authPW.toString('hex')
}
)
}
ClientApi.prototype.accountUnlockResendCode = function (email, options) {
options = options || {}
return this.doRequest(

Просмотреть файл

@ -311,6 +311,10 @@ Client.prototype.verifyPasswordResetCode = function (code) {
)
}
Client.prototype.lockAccount = function () {
return this.api.accountLock(this.email, this.authPW)
}
Client.prototype.resendAccountUnlockCode = function () {
return this.api.accountUnlockResendCode(this.email, this.options)
}

Просмотреть файл

@ -12,16 +12,6 @@ var nock = require('nock')
var Customs = require('../../customs.js')(log, error)
var MockDB = {
locked: {},
lockAccount: function(account) {
MockDB.locked[account.uid] = true;
},
unlockAccount: function(account) {
delete MockDB.locked[account.uid];
}
}
var CUSTOMS_URL_REAL = 'http://localhost:7000'
var CUSTOMS_URL_MISSING = 'http://localhost:7001'
@ -39,7 +29,7 @@ test(
function (t) {
t.plan(7)
customsNoUrl = new Customs('none', MockDB)
customsNoUrl = new Customs('none')
t.ok(customsNoUrl, 'got a customs object with a none url')
@ -58,7 +48,7 @@ test(
return customsNoUrl.flag(ip, { email: email, uid: "12345" })
})
.then(function(result) {
t.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
t.equal(result.lockout, false, 'lockout is false when /failedLoginAttempt returns `lockout: false`')
t.pass('Passed /failedLoginAttempt')
}, function(error) {
t.fail('We should have failed open for /failedLoginAttempt')
@ -78,9 +68,9 @@ test(
test(
'can create a customs object with a url',
function (t) {
t.plan(18)
t.plan(16)
customsWithUrl = new Customs(CUSTOMS_URL_REAL, MockDB)
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
t.ok(customsWithUrl, 'got a customs object with a valid url')
@ -106,8 +96,7 @@ test(
return customsWithUrl.flag(ip, { email: email, uid: "12345" })
})
.then(function(result) {
t.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
t.ok(!MockDB.locked["12345"], 'We ignore /failedLoginAttempt {lockout:false}')
t.equal(result.lockout, false, 'lockout is false when /failedLoginAttempt returns false')
t.pass('Passed /failedLoginAttempt')
}, function(error) {
t.fail('We should not have failed here for /failedLoginAttempt : err=' + error)
@ -122,6 +111,7 @@ test(
t.fail('We should not have failed here for /passwordReset : err=' + error)
})
.then(function() {
// request is blocked
return customsWithUrl.check(email, ip, action)
})
.then(function(result) {
@ -136,11 +126,11 @@ test(
t.equal(error.output.headers['retry-after'], 10001, 'retryAfter header is correct')
})
.then(function() {
// account is locked
return customsWithUrl.flag(ip, { email: email, uid: "12345" })
})
.then(function(result) {
t.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
t.ok(MockDB.locked["12345"], 'We lockout on /failedLoginAttempt {lockout:true}')
t.equal(result.lockout, true, 'lockout is true when /failedLoginAttempt returns `lockout: true`')
t.pass('Passed /failedLoginAttempt with lockout')
}, function(error) {
t.fail('We should not have failed here for /failedLoginAttempt : err=' + error)
@ -154,7 +144,7 @@ test(
function (t) {
t.plan(7)
customsInvalidUrl = new Customs(CUSTOMS_URL_MISSING, MockDB)
customsInvalidUrl = new Customs(CUSTOMS_URL_MISSING)
t.ok(customsInvalidUrl, 'got a customs object with a non-existant service url')
@ -173,7 +163,7 @@ test(
return customsInvalidUrl.flag(ip, { email: email, uid: "12345" })
})
.then(function(result) {
t.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
t.equal(result.lockout, false, 'lockout is false when /failedLoginAttempt hits an invalid endpoint')
t.pass('Passed /failedLoginAttempt')
}, function(error) {
t.fail('We should have failed open (no url provided) for /failedLoginAttempt')

Просмотреть файл

@ -349,6 +349,43 @@ DB.connect(config[config.db.backend])
}
)
test(
'account lockout',
function (t) {
return db.lockAccount(ACCOUNT)
.then(function() {
t.pass('lockAccount should succeed')
return db.emailRecord(ACCOUNT.email)
})
.then(function(emailRecord) {
t.ok(emailRecord.lockedAt, 'emailRecord should have a lockedAt date set')
return db.unlockCode(ACCOUNT)
})
.then(function(unlockCode) {
t.ok(unlockCode, 'unlockCode should be returned for a locked account')
return db.unlockAccount(ACCOUNT)
})
.then(function() {
t.pass('unlockAccount should succeed')
return db.emailRecord(ACCOUNT.email)
})
.then(function(emailRecord) {
t.equal(emailRecord.lockedAt, null, 'an unlocked account should have no lockedAt')
return db.unlockCode(ACCOUNT)
})
.then(function () {
t.fail('unlockCode on an unlocked account should fail')
}, function (err) {
t.equal(err.errno, 122, 'unlockCode on an unlocked account should fail with an account not locked error')
return db.unlockAccount(ACCOUNT)
})
.then(function () {
t.pass('unlockAccount on an unlocked account should succeed')
})
}
)
test(
'account deletion',
function (t) {

Просмотреть файл

@ -0,0 +1,127 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var P = require('../../promise')
var test = require('../ptaptest')
var MockLog = { event: function () { } }
var config = { lockoutEnabled: true }
var Password = require('../../crypto/password')(MockLog, config)
var triggersLockout = false;
var MockCustoms = {
flag: function (clientAddress, emailRecord) {
return P({ lockout: triggersLockout })
}
}
var MockDB = {
locked: {},
isLocked: function (uid) {
return !! this.locked[uid];
},
lockAccount: function(account) {
MockDB.locked[account.uid] = true;
}
}
var checkPassword = require('../../routes/utils/password_check')(MockLog, config, Password, MockCustoms, MockDB)
test(
'password check with correct password',
function (t) {
var authPW = new Buffer('aaaaaaaaaaaaaaaa')
var emailRecord = {
uid: 'correct_password',
verifyHash: null,
verifierVersion: 0,
authSalt: new Buffer('bbbbbbbbbbbbbbbb')
}
var password = new Password(
authPW, emailRecord.authSalt, emailRecord.verifierVersion);
return password.verifyHash()
.then(
function (hash) {
emailRecord.verifyHash = hash
return checkPassword(emailRecord, authPW, '10.0.0.1')
}
)
.then(
function (matches) {
t.ok(matches, 'password matches, checkPassword returns true')
}
)
}
)
test(
'password check with incorrect password that does not trigger lockout',
function (t) {
var authPW = new Buffer('aaaaaaaaaaaaaaaa')
var emailRecord = {
uid: 'not_locked',
verifyHash: null,
verifierVersion: 0,
authSalt: new Buffer('bbbbbbbbbbbbbbbb')
}
var password= new Password(
authPW, emailRecord.authSalt, emailRecord.verifierVersion);
return password.verifyHash()
.then(
function (hash) {
emailRecord.verifyHash = hash
var incorrectAuthPW = new Buffer('cccccccccccccccc')
triggersLockout = false;
return checkPassword(emailRecord, incorrectAuthPW, '10.0.0.1')
}
)
.then(
function (match) {
t.equal(!!match, false, 'password does not match, checkPassword returns false')
t.equal(MockDB.isLocked('not_locked'), false, 'account was not marked as locked');
}
)
}
)
test(
'password check with incorrect password that triggers lockout',
function (t) {
var authPW = new Buffer('aaaaaaaaaaaaaaaa')
var emailRecord = {
uid: 'locked',
verifyHash: null,
verifierVersion: 0,
authSalt: new Buffer('bbbbbbbbbbbbbbbb')
}
var password= new Password(
authPW, emailRecord.authSalt, emailRecord.verifierVersion);
return password.verifyHash()
.then(
function (hash) {
emailRecord.verifyHash = hash
var incorrectAuthPW = new Buffer('cccccccccccccccc')
triggersLockout = true;
return checkPassword(emailRecord, incorrectAuthPW, '10.0.0.1')
}
)
.then(
function (match) {
t.equal(!!match, false, 'password does not match, checkPassword returns false')
t.equal(MockDB.isLocked('locked'), true, 'account was not marked as locked');
}
)
}
)

Просмотреть файл

@ -24,6 +24,7 @@ require('simplesmtp').createSimpleServer(
var link = mail.headers['x-link']
var rc = mail.headers['x-recovery-code']
var vc = mail.headers['x-verify-code']
var uc = mail.headers['x-unlock-code']
var name = emailName(mail.headers.to)
if (vc) {
console.log('\x1B[32m', link, '\x1B[39m')
@ -31,6 +32,9 @@ require('simplesmtp').createSimpleServer(
else if (rc) {
console.log('\x1B[34m', link, '\x1B[39m')
}
else if (uc) {
console.log('\x1B[36m %s', link, '\x1B[39m')
}
else {
console.error('\x1B[31mNo verify code match\x1B[39m')
console.error(mail)

Просмотреть файл

@ -32,8 +32,11 @@ TestServer.start(config)
)
.then(
function () {
// There's no public API to force an account into the "locked" state,
// but this will re-send the email regardless of actual account state.
return client.lockAccount(email, password)
}
)
.then(
function () {
return client.resendAccountUnlockCode()
}
)
@ -71,6 +74,11 @@ TestServer.start(config)
return server.mailbox.waitForEmail(email)
}
)
.then(
function () {
return client.lockAccount(email, password)
}
)
.then(
function () {
return client.resendAccountUnlockCode()
@ -94,6 +102,89 @@ TestServer.start(config)
}
)
test(
'resend account unlock code for account that is not locked',
function (t) {
var email = server.uniqueEmail()
var password = 'allyourbasearebelongtous'
var client = null
return Client.createAndVerify(config.publicUrl, email, password, server.mailbox)
.then(
function () {
return Client.login(config.publicUrl, email, password)
}
)
.then(
function (x) {
client = x
}
)
.then(
function () {
return client.resendAccountUnlockCode()
}
)
.then(
function () {
t.fail('resendAccountUnlockCode is expected to fail')
},
function (err) {
t.equal(err.code, 400, '400 status code')
t.equal(err.errno, 122, 'account is not locked errno')
}
)
}
)
test(
're-verify an account unlock when an account is not locked',
function (t) {
var email = server.uniqueEmail()
var password = 'allyourbasearebelongtous'
var client = null
var code = null
return Client.createAndVerify(config.publicUrl, email, password, server.mailbox)
.then(
function () {
return Client.login(config.publicUrl, email, password)
}
)
.then(
function (x) {
client = x
}
)
.then(
function () {
return client.lockAccount(email, password)
}
)
.then(
function () {
return client.resendAccountUnlockCode()
}
)
.then(
function () {
return server.mailbox.waitForCode(email)
}
)
.then(
function (_code) {
code = _code
return client.verifyAccountUnlockCode(client.uid, code)
}
)
.then(
function () {
// the user may be re-verifying a stale link,
// silently succeed.
return client.verifyAccountUnlockCode(client.uid, code)
}
)
}
)
test(
'teardown',
function (t) {