fxa-auth-server/test/local/customs.js

506 строки
18 KiB
JavaScript

/* 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/. */
'use strict'
const ROOT_DIR = '../..'
const assert = require('insist')
const log = {
trace: () => {},
activityEvent: () => {},
flowEvent: () => {},
error() {}
}
const mocks = require('../mocks')
const error = require(`${ROOT_DIR}/lib/error.js`)
const P = require(`${ROOT_DIR}/lib/promise.js`)
var nock = require('nock')
const Customs = require(`${ROOT_DIR}/lib/customs.js`)(log, error)
var CUSTOMS_URL_REAL = 'http://localhost:7000'
var CUSTOMS_URL_MISSING = 'http://localhost:7001'
var customsNoUrl
var customsWithUrl
var customsInvalidUrl
var customsServer = nock(CUSTOMS_URL_REAL)
.defaultReplyHeaders({
'Content-Type': 'application/json'
})
describe('Customs', () => {
it(
"can create a customs object with url as 'none'",
() => {
customsNoUrl = new Customs('none')
assert.ok(customsNoUrl, 'got a customs object with a none url')
var request = newRequest()
var ip = request.app.clientAddress
var email = newEmail()
var action = newAction()
return customsNoUrl.check(request, email, action)
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds')
})
.then(function() {
return customsNoUrl.flag(ip, { email: email, uid: '12345' })
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
})
.then(function() {
return customsNoUrl.reset(email)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /passwordReset succeeds')
})
.then(() => {
return customsNoUrl.checkIpOnly(request, action)
})
.then(result => {
assert.equal(result, undefined, 'Nothing is returned when /checkIpOnly succeeds')
})
}
)
it(
'can create a customs object with a url',
() => {
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
assert.ok(customsWithUrl, 'got a customs object with a valid url')
var request = newRequest()
var ip = request.app.clientAddress
var email = newEmail()
var action = newAction()
// Mock a check that does not get blocked.
customsServer.post('/check', function (body) {
assert.deepEqual(body, {
ip: ip,
email: email,
action: action,
headers: request.headers,
query: request.query,
payload: request.payload,
}, 'first call to /check had expected request params')
return true
}).reply(200, {
block: false,
retryAfter: 0
})
return customsWithUrl.check(request, email, action)
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds')
})
.then(function() {
// Mock a report of a failed login attempt
customsServer.post('/failedLoginAttempt', function (body) {
assert.deepEqual(body, {
ip: ip,
email: email,
errno: error.ERRNO.UNEXPECTED_ERROR
}, 'first call to /failedLoginAttempt had expected request params')
return true
}).reply(200, {})
return customsWithUrl.flag(ip, { email: email, uid: '12345' })
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
})
.then(function() {
// Mock a report of a password reset.
customsServer.post('/passwordReset', function (body) {
assert.deepEqual(body, {
email: email,
}, 'first call to /passwordReset had expected request params')
return true
}).reply(200, {})
return customsWithUrl.reset(email)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /passwordReset succeeds')
})
.then(function() {
// Mock a check that does get blocked, with a retryAfter.
customsServer.post('/check', function (body) {
assert.deepEqual(body, {
ip: ip,
email: email,
action: action,
headers: request.headers,
query: request.query,
payload: request.payload,
}, 'second call to /check had expected request params')
return true
}).reply(200, {
block: true,
retryAfter: 10001
})
return customsWithUrl.check(request, email, action)
})
.then(function(result) {
assert(false, 'This should have failed the check since it should be blocked')
}, function(err) {
assert.equal(err.errno, error.ERRNO.THROTTLED, 'Error number is correct')
assert.equal(err.message, 'Client has sent too many requests', 'Error message is correct')
assert.ok(err.isBoom, 'The error causes a boom')
assert.equal(err.output.statusCode, 429, 'Status Code is correct')
assert.equal(err.output.payload.retryAfter, 10001, 'retryAfter is correct')
assert.equal(err.output.headers['retry-after'], 10001, 'retryAfter header is correct')
})
.then(function() {
// Mock a report of a failed login attempt that does trigger lockout.
customsServer.post('/failedLoginAttempt', function (body) {
assert.deepEqual(body, {
ip: ip,
email: email,
errno: error.ERRNO.INCORRECT_PASSWORD
}, 'second call to /failedLoginAttempt had expected request params')
return true
}).reply(200, { })
return customsWithUrl.flag(ip, {
email: email,
errno: error.ERRNO.INCORRECT_PASSWORD
})
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
})
.then(function() {
// Mock a check that does get blocked, with no retryAfter.
request.headers['user-agent'] = 'test passing through headers'
request.payload['foo'] = 'bar'
customsServer.post('/check', function (body) {
assert.deepEqual(body, {
ip: ip,
email: email,
action: action,
headers: request.headers,
query: request.query,
payload: request.payload,
}, 'third call to /check had expected request params')
return true
}).reply(200, {
block: true
})
return customsWithUrl.check(request, email, action)
})
.then(function(result) {
assert(false, 'This should have failed the check since it should be blocked')
}, function(err) {
assert.equal(err.errno, error.ERRNO.REQUEST_BLOCKED, 'Error number is correct')
assert.equal(err.message, 'The request was blocked for security reasons', 'Error message is correct')
assert.ok(err.isBoom, 'The error causes a boom')
assert.equal(err.output.statusCode, 400, 'Status Code is correct')
assert(! err.output.payload.retryAfter, 'retryAfter field is not present')
assert(! err.output.headers['retry-after'], 'retryAfter header is not present')
})
.then(() => {
customsServer.post('/checkIpOnly', function (body) {
assert.deepEqual(body, {
ip: ip,
action: action
}, 'first call to /check had expected request params')
return true
}).reply(200, {
block: false,
retryAfter: 0
})
return customsWithUrl.checkIpOnly(request, action)
})
.then(result => {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds')
})
}
)
it(
'failed closed when creating a customs object with non-existant customs service',
() => {
customsInvalidUrl = new Customs(CUSTOMS_URL_MISSING)
assert.ok(customsInvalidUrl, 'got a customs object with a non-existant service url')
var request = newRequest()
var ip = request.app.clientAddress
var email = newEmail()
var action = newAction()
return P.all([
customsInvalidUrl.check(request, email, action)
.then(assert.fail, err => {
assert.equal(err.errno, error.ERRNO.BACKEND_SERVICE_FAILURE, 'an error is returned from /check')
}),
customsInvalidUrl.flag(ip, { email: email, uid: '12345' })
.then(assert.fail, err => {
assert.equal(err.errno, error.ERRNO.BACKEND_SERVICE_FAILURE, 'an error is returned from /flag')
}),
customsInvalidUrl.reset(email)
.then(assert.fail, err => {
assert.equal(err.errno, error.ERRNO.BACKEND_SERVICE_FAILURE, 'an error is returned from /passwordReset')
})
])
}
)
it(
'can rate limit checkAccountStatus /check',
() => {
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
assert.ok(customsWithUrl, 'can rate limit checkAccountStatus /check')
var request = newRequest()
var ip = request.app.clientAddress
var email = newEmail()
var action = 'accountStatusCheck'
function checkRequestBody (body) {
assert.deepEqual(body, {
ip: ip,
email: email,
action: action,
headers: request.headers,
query: request.query,
payload: request.payload,
}, 'call to /check had expected request params')
return true
}
customsServer
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/check', checkRequestBody).reply(200, '{"block":true,"retryAfter":10001}')
return customsWithUrl.check(request, email, action)
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 1')
return customsWithUrl.check(request, email, action)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 2')
return customsWithUrl.check(request, email, action)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 3')
return customsWithUrl.check(request, email, action)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 4')
return customsWithUrl.check(request, email, action)
})
.then(function() {
// request is blocked
return customsWithUrl.check(request, email, action)
})
.then(function() {
assert(false, 'This should have failed the check since it should be blocked')
}, function(error) {
assert.equal(error.errno, 114, 'Error number is correct')
assert.equal(error.message, 'Client has sent too many requests', 'Error message is correct')
assert.ok(error.isBoom, 'The error causes a boom')
assert.equal(error.output.statusCode, 429, 'Status Code is correct')
assert.equal(error.output.payload.retryAfter, 10001, 'retryAfter is correct')
assert.equal(error.output.payload.retryAfterLocalized, 'in 3 hours', 'retryAfterLocalized is correct')
assert.equal(error.output.headers['retry-after'], 10001, 'retryAfter header is correct')
})
}
)
it(
'can rate limit devicesNotify /checkAuthenticated',
() => {
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
assert.ok(customsWithUrl, 'can rate limit /checkAuthenticated')
var request = newRequest()
var action = 'devicesNotify'
var ip = request.app.clientAddress
var uid = 'foo'
function checkRequestBody (body) {
assert.deepEqual(body, {
action: action,
ip: ip,
uid: uid,
}, 'call to /checkAuthenticated had expected request params')
return true
}
customsServer
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":true,"retryAfter":10001}')
return customsWithUrl.checkAuthenticated(action, ip, uid)
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 1')
return customsWithUrl.checkAuthenticated(action, ip, uid)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 2')
return customsWithUrl.checkAuthenticated(action, ip, uid)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 3')
return customsWithUrl.checkAuthenticated(action, ip, uid)
})
.then(function(result) {
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 4')
return customsWithUrl.checkAuthenticated(action, ip, uid)
})
.then(function() {
// request is blocked
return customsWithUrl.checkAuthenticated(action, ip, uid)
})
.then(function() {
assert(false, 'This should have failed the check since it should be blocked')
}, function(error) {
assert.equal(error.errno, 114, 'Error number is correct')
assert.equal(error.message, 'Client has sent too many requests', 'Error message is correct')
assert.ok(error.isBoom, 'The error causes a boom')
assert.equal(error.output.statusCode, 429, 'Status Code is correct')
assert.equal(error.output.payload.retryAfter, 10001, 'retryAfter is correct')
assert.equal(error.output.headers['retry-after'], 10001, 'retryAfter header is correct')
})
}
)
it('can rate limit verifyTotpCode /check', () => {
const request = newRequest()
const action = 'verifyTotpCode'
const email = 'test@email.com'
const ip = request.app.clientAddress
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
assert.ok(customsWithUrl, 'can rate limit ')
function checkRequestBody(body) {
assert.deepEqual(body, {
ip: ip,
email: email,
action: action,
headers: request.headers,
query: request.query,
payload: request.payload,
}, 'call to /check had expected request params')
return true
}
customsServer
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
.post('/check', checkRequestBody).reply(200, '{"block":true,"retryAfter":30}')
return customsWithUrl.check(request, email, action)
.then((result) => {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 1')
return customsWithUrl.check(request, email, action)
})
.then((result) => {
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 2')
return customsWithUrl.check(request, email, action)
})
.then(assert.fail, (error) => {
assert.equal(error.errno, 114, 'Error number is correct')
assert.equal(error.message, 'Client has sent too many requests', 'Error message is correct')
assert.ok(error.isBoom, 'The error causes a boom')
assert.equal(error.output.statusCode, 429, 'Status Code is correct')
assert.equal(error.output.payload.retryAfter, 30, 'retryAfter is correct')
assert.equal(error.output.headers['retry-after'], 30, 'retryAfter header is correct')
})
})
it(
'can scrub customs request object',
() => {
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
assert.ok(customsWithUrl, 'got a customs object with a valid url')
var request = newRequest()
request.payload.authPW = 'asdfasdfadsf'
request.payload.oldAuthPW = '012301230123'
request.payload.notThePW = 'plaintext'
var ip = request.app.clientAddress
var email = newEmail()
var action = newAction()
customsServer.post('/check', function (body) {
assert.deepEqual(body, {
ip: ip,
email: email,
action: action,
headers: request.headers,
query: request.query,
payload: {
notThePW: 'plaintext'
}
}, 'should not have password fields in payload')
return true
}).reply(200, {
block: false,
retryAfter: 0
})
return customsWithUrl.check(request, email, action)
.then(function (result) {
assert.equal(result, undefined, 'nothing is returned when /check succeeds - 1')
})
}
)
})
function newEmail() {
return Math.random().toString().substr(2) + '@example.com'
}
function newIp() {
return [
'' + Math.floor(Math.random() * 256),
'' + Math.floor(Math.random() * 256),
'' + Math.floor(Math.random() * 256),
'' + Math.floor(Math.random() * 256),
].join('.')
}
function newRequest() {
return mocks.mockRequest({
clientAddress: newIp(),
headers: {},
query: {},
payload: {}
})
}
function newAction() {
var EMAIL_ACTIONS = [
'accountCreate',
'recoveryEmailResendCode',
'passwordForgotSendCode',
'passwordForgotResendCode'
]
return EMAIL_ACTIONS[Math.floor(Math.random() * EMAIL_ACTIONS.length)]
}