feat(signin): Remove feature flag from sign-in confirmation. (#1530); r=vbudhram

It's now always enabled for all users, and we no longer have
any backwards-compatibility paths that might fall back to it
being disabled.
This commit is contained in:
Ryan Kelly 2016-11-24 07:27:09 +11:00 коммит произвёл GitHub
Родитель b23a531db7
Коммит 5f0f3ba550
17 изменённых файлов: 365 добавлений и 759 удалений

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

@ -4,8 +4,6 @@ LOCKOUT_ENABLED=true
LOG_FORMAT=pretty
LOG_LEVEL=info
RESEND_BLACKOUT_PERIOD=0
SIGNIN_CONFIRMATION_ENABLED=true
SIGNIN_CONFIRMATION_RATE=1
SMTP_HOST=127.0.0.1
SMTP_PORT=9999
SMTP_SECURE=false

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

@ -437,36 +437,8 @@ var conf = convict({
default: 3
},
signinConfirmation: {
enabled: {
doc: 'enable signin confirmation',
format: Boolean,
default: false,
env: 'SIGNIN_CONFIRMATION_ENABLED'
},
sample_rate: {
doc: 'signin confirmation sample rate, between 0.0 and 1.0',
format: Number,
default: 1.0,
env: 'SIGNIN_CONFIRMATION_RATE'
},
supportedClients: {
doc: 'support sign-in confirmation for only these clients',
format: Array,
default: [
'iframe',
'fx_firstrun_v1',
'fx_firstrun_v2',
'fx_desktop_v1',
'fx_desktop_v2',
'fx_desktop_v3',
'fx_ios_v1',
'fx_ios_v2',
'fx_fennec_v1'
],
env: 'SIGNIN_CONFIRMATION_SUPPORTED_CLIENTS'
},
forcedEmailAddresses: {
doc: 'If feature enabled, force sign-in confirmation for email addresses matching this regex.',
doc: 'Force sign-in confirmation for email addresses matching this regex.',
format: RegExp,
default: /.+@mozilla\.com$/,
env: 'SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX'

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

@ -8,7 +8,6 @@ const crypto = require('crypto')
module.exports = config => {
const lastAccessTimeUpdates = config.lastAccessTimeUpdates
const signinConfirmation = config.signinConfirmation
const signinUnblock = config.signinUnblock
const securityHistory = config.securityHistory
@ -28,49 +27,10 @@ module.exports = config => {
},
/**
* Predicate that indicates whether sign-in confirmation is enabled
* for a given user, based on their uid and email address.
* Returns whether or not to use signin unblock feature on a request.
*
* @param uid Buffer or String
* @param email String
*/
isSigninConfirmationEnabledForUser (uid, email, request) {
if (! signinConfirmation.enabled) {
return false
}
// Always create unverified tokens if customs-server
// has said the request is suspicious.
if (request.app.isSuspiciousRequest) {
return true
}
// Or if the email address matches the regex.
if (signinConfirmation.forcedEmailAddresses.test(email)) {
return true
}
// While we're testing this feature, there may be some funky
// edge-cases in device login flows that haven't been fully tested.
// Temporarily avoid them for regular users by checking the `context` flag,
// and create pre-verified sessions for unsupported clients.
const context = request.payload &&
request.payload.metricsContext &&
request.payload.metricsContext.context
if (signinConfirmation.supportedClients.indexOf(context) === -1) {
return false
}
// Check to see if user in roll-out cohort.
return isSampledUser(signinConfirmation.sample_rate, uid, 'signinConfirmation')
},
/**
* Returns whether or not to use signin unblock feature on a request.
*
* @param account
* @param config
* @param request
* @returns {boolean}
*/
@ -103,24 +63,28 @@ module.exports = config => {
},
/**
* Return whether or not this request should bypass sign-in confirmation. Currently,
* just checks if user has had a verified security event in the past day.
* Return whether tracking of security history events is enabled.
*
* @param verified
* @param recency
* @returns {boolean}
*/
canBypassSiginConfirmation(email, verified, recency) {
// If sign-in confirmation is forced for an email, it can't be bypassed.
if (signinConfirmation.enabled && signinConfirmation.forcedEmailAddresses.test(email)) {
isSecurityHistoryTrackingEnabled() {
return securityHistory.enabled
},
/**
* Return whether or not we can bypass sign-in confirmation based
* on previously seen security event history.
*
* @returns {boolean}
*/
isSecurityHistoryProfilingEnabled() {
if (! securityHistory.enabled) {
return false
}
// IP Profiling returns true if this user has verified a session
// within the past day from this ip address.
let ipProfilingEnabled = securityHistory.enabled && securityHistory.ipProfiling &&
securityHistory.ipProfiling.enabled
return ipProfilingEnabled && verified && recency === 'day'
if (! securityHistory.ipProfiling || ! securityHistory.ipProfiling.enabled) {
return false
}
return true
},
/**

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

@ -60,7 +60,6 @@ module.exports = function (
})
const features = require('../features')(config)
const securityHistoryEnabled = config.securityHistory && config.securityHistory.enabled
const unblockCodeLifetime = config.signinUnblock && config.signinUnblock.codeLifetime || 0
const unblockCodeLen = config.signinUnblock && config.signinUnblock.codeLength || 0
@ -239,12 +238,8 @@ module.exports = function (
}
function createSessionToken () {
const enableTokenVerification =
features.isSigninConfirmationEnabledForUser(account.uid, account.email, request)
// Verified sessions should only be created for preverified tokens
// and when sign-in confirmation is disabled or not needed.
if (preVerified || ! enableTokenVerification) {
// Verified sessions should only be created for preverified accounts.
if (preVerified) {
tokenVerificationId = undefined
}
@ -255,7 +250,7 @@ module.exports = function (
emailVerified: account.emailVerified,
verifierSetAt: account.verifierSetAt,
createdAt: parseInt(query._createdAt),
mustVerify: enableTokenVerification && requestHelper.wantsKeys(request),
mustVerify: requestHelper.wantsKeys(request),
tokenVerificationId: tokenVerificationId
}, userAgentString)
.then(
@ -341,7 +336,7 @@ module.exports = function (
}
function recordSecurityEvent() {
if (securityHistoryEnabled) {
if (features.isSecurityHistoryTrackingEnabled()) {
// don't block response recording db event
db.securityEvent({
name: 'account.create',
@ -522,7 +517,7 @@ module.exports = function (
}
function checkSecurityHistory () {
if (!securityHistoryEnabled) {
if (!features.isSecurityHistoryTrackingEnabled()) {
return
}
return db.securityEvents({
@ -581,40 +576,35 @@ module.exports = function (
}
function checkEmailAndPassword () {
// Session token verification is only enabled for certain users during phased rollout.
//
// If the user went through the sigin-unblock flow, they have already verified their email.
// No need to also require confirmation afterwards.
//
// Even when it is enabled, we only do the email challenge if:
// * the request wants keys, since unverified sessions are fine to use for e.g. oauth login.
// * the email is verified, since content-server triggers a resend of the verification
// email on unverified accounts, which doubles as sign-in confirmation.
// * the login is flagged that it can be bypassed
// All sessions are considered unverified by default.
needsVerificationId = true
// Check to see if this login can bypass sign-in confirmation. Current scenarios include
// * User has already logged in from this ip address and verified the sign-in
let bypassSiginConfirmation = features.canBypassSiginConfirmation(emailRecord.email, securityEventVerified, securityEventRecency)
if (bypassSiginConfirmation) {
log.info({
op: 'Account.ipprofiling.seenAddress',
uid: emailRecord.uid.toString('hex')
})
}
if (didSigninUnblock || !features.isSigninConfirmationEnabledForUser(emailRecord.uid, emailRecord.email, request)
|| bypassSiginConfirmation) {
// However! To help simplify the login flow, we can use some heuristics to
// decide whether to consider the session pre-verified. Some accounts
// get excluded from this process, e.g. testing accounts where we want
// to know for sure what flow they're going to see.
if (! forceTokenVerification(request, emailRecord)) {
if (skipTokenVerification(request, emailRecord)) {
needsVerificationId = false
mustVerifySession = false
doSigninConfirmation = false
} else {
// The user doesn't *have* to verify their session if they're not requesting keys,
// but we still create it with a non-null tokenVerificationId, so it will still
// be considered unverified. This prevents the session from being used for sync
// unless the user explicitly requests us to resend the confirmation email, and completes it.
mustVerifySession = requestHelper.wantsKeys(request)
doSigninConfirmation = mustVerifySession && emailRecord.emailVerified
}
}
// If they just went through the sigin-unblock flow, they have already verified their email.
// We don't need to force them to do that again, just make a verified session.
if (didSigninUnblock) {
needsVerificationId = false
}
// If the request wants keys, the user *must* confirm their login session before they
// can actually use it. If they dont want keys, they don't *have* to verify their
// their session, but we still create it with a non-null tokenVerificationId, so it will
// still be considered unverified. This prevents the session from being used for sync
// unless the user explicitly requests us to resend the confirmation email, and completes it.
mustVerifySession = needsVerificationId && requestHelper.wantsKeys(request)
// If the email itself is unverified, we'll re-send the "verify your account email" and
// that will suffice to confirm the sign-in. No need for a separate confirmation email.
doSigninConfirmation = mustVerifySession && emailRecord.emailVerified
let flowCompleteSignal
if (service === 'sync') {
@ -648,6 +638,40 @@ module.exports = function (
)
}
function forceTokenVerification (request, account) {
// If there was anything suspicious about the request,
// we should force token verification.
if (request.app.isSuspiciousRequest) {
return true
}
// If it's an email address used for testing etc,
// we should force token verification.
if (config.signinConfirmation) {
if (config.signinConfirmation.forcedEmailAddresses) {
if (config.signinConfirmation.forcedEmailAddresses.test(account.email)) {
return true
}
}
}
return false
}
function skipTokenVerification (request, account) {
// If they're logging in from an IP address on which they recently did
// another, successfully-verified login, then we can consider this one
// verified as well without going through the loop again.
if (features.isSecurityHistoryProfilingEnabled()) {
if (securityEventVerified && securityEventRecency === 'day') {
log.info({
op: 'Account.ipprofiling.seenAddress',
uid: account.uid.toString('hex')
})
return true
}
}
return false
}
function checkNumberOfActiveSessions () {
return db.sessions(emailRecord.uid)
.then(
@ -836,7 +860,7 @@ module.exports = function (
}
function recordSecurityEvent() {
if (securityHistoryEnabled) {
if (features.isSecurityHistoryTrackingEnabled()) {
// don't block response recording db event
db.securityEvent({
name: 'account.login',
@ -1920,7 +1944,7 @@ module.exports = function (
}
function recordSecurityEvent() {
if (securityHistoryEnabled) {
if (features.isSecurityHistoryTrackingEnabled()) {
// don't block response recording db event
db.securityEvent({
name: 'account.reset',

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

@ -168,9 +168,8 @@ module.exports = function (
}
)
} else {
// To keep backwards compatibility, default to creating a verified
// session if no sessionToken is passed
verifiedStatus = true
// Don't create a verified session unless they already had one.
verifiedStatus = false
return P.resolve()
}
}

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

@ -83,9 +83,7 @@ function runTest (route, request, assertions) {
}
describe('/recovery_email/status', function () {
var config = {
signinConfirmation: {}
}
var config = {}
var mockDB = mocks.mockDB()
var pushCalled
var mockLog = mocks.mockLog({
@ -102,8 +100,12 @@ describe('/recovery_email/status', function () {
})
var route = getRoute(accountRoutes, '/recovery_email/status')
describe('sign-in confirmation disabled', function () {
config.signinConfirmation.enabled = false
var mockRequest = mocks.mockRequest({
credentials: {
uid: uuid.v4('binary').toString('hex'),
email: TEST_EMAIL
}
})
describe('invalid email', function () {
var mockRequest = mocks.mockRequest({
@ -142,6 +144,7 @@ describe('/recovery_email/status', function () {
})
})
it('valid email, verified account', function () {
pushCalled = false
var mockRequest = mocks.mockRequest({
@ -167,17 +170,6 @@ describe('/recovery_email/status', function () {
})
})
})
})
describe('sign-in confirmation enabled', function () {
config.signinConfirmation.enabled = true
config.signinConfirmation.sample_rate = 1
var mockRequest = mocks.mockRequest({
credentials: {
uid: uuid.v4('binary').toString('hex'),
email: TEST_EMAIL
}
})
it('verified account, verified session', function () {
mockRequest.auth.credentials.emailVerified = true
@ -223,12 +215,9 @@ describe('/recovery_email/status', function () {
})
})
})
})
describe('/recovery_email/resend_code', () => {
const config = {
signinConfirmation: {}
}
const config = {}
const mockDB = mocks.mockDB()
const mockLog = mocks.mockLog()
mockLog.flowEvent = sinon.spy(() => {
@ -887,7 +876,7 @@ describe('/account/login', function () {
query: {},
payload: {
authPW: crypto.randomBytes(32).toString('hex'),
email: 'test@mozilla.com',
email: TEST_EMAIL,
unblockCode: 'ABCD1234',
service: 'dcdb5ae7add825d2',
reason: 'signin',
@ -929,32 +918,27 @@ describe('/account/login', function () {
const defaultEmailRecord = mockDB.emailRecord
beforeEach(() => {
afterEach(() => {
mockLog.activityEvent.reset()
mockLog.flowEvent.reset()
mockLog.stdout.write.reset()
mockMailer.sendNewDeviceLoginNotification.reset()
mockMailer.sendVerifyLoginEmail.reset()
mockMailer.sendVerifyCode.reset()
mockDB.createSessionToken.reset()
mockDB.sessions.reset()
mockMetricsContext.stash.reset()
mockMetricsContext.validate.reset()
mockMetricsContext.setFlowCompleteSignal.reset()
})
describe('sign-in confirmation disabled', function () {
beforeEach(() => {
mockDB.emailRecord = defaultEmailRecord
mockDB.emailRecord.reset()
mockRequest.payload.email = TEST_EMAIL
})
it('sign-in does not require verification', function () {
it('emits the correct series of calls and events', function () {
return runTest(route, mockRequest, function (response) {
assert.equal(mockDB.emailRecord.callCount, 1, 'db.emailRecord was called')
assert.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
assert.ok(!tokenData.mustVerify, 'sessionToken was created verified')
assert.ok(!tokenData.tokenVerificationId, 'sessionToken was created verified')
assert.equal(mockDB.sessions.callCount, 1, 'db.sessions was called')
assert.equal(mockLog.stdout.write.callCount, 1, 'an sqs event was logged')
var eventData = JSON.parse(mockLog.stdout.write.getCall(0).args[0])
@ -970,16 +954,18 @@ describe('/account/login', function () {
assert.equal(args[1], mockRequest, 'second argument was request object')
assert.deepEqual(args[2], {uid: uid.toString('hex')}, 'third argument contained uid')
assert.equal(mockLog.flowEvent.callCount, 1, 'log.flowEvent was called once')
assert.equal(mockLog.flowEvent.callCount, 2, 'log.flowEvent was called twice')
args = mockLog.flowEvent.args[0]
assert.equal(args.length, 2, 'log.flowEvent was passed two arguments')
assert.equal(args.length, 2, 'first log.flowEvent was passed two arguments')
assert.equal(args[0], 'account.login', 'first argument was event name')
assert.equal(args[1], mockRequest, 'second argument was request object')
args = mockLog.flowEvent.args[1]
assert.equal(args[0], 'email.confirmation.sent', 'second log.flowEvent was passed correct event name')
assert.equal(mockMetricsContext.validate.callCount, 1, 'metricsContext.validate was called')
assert.equal(mockMetricsContext.validate.args[0].length, 0, 'validate was called without arguments')
assert.equal(mockMetricsContext.stash.callCount, 2, 'metricsContext.stash was called twice')
assert.equal(mockMetricsContext.stash.callCount, 3, 'metricsContext.stash was called three times')
args = mockMetricsContext.stash.args[0]
assert.equal(args.length, 1, 'metricsContext.stash was passed one argument first time')
@ -989,6 +975,12 @@ describe('/account/login', function () {
args = mockMetricsContext.stash.args[1]
assert.equal(args.length, 1, 'metricsContext.stash was passed one argument second time')
assert.ok(/^[0-9a-f]{32}$/.test(args[0].id), 'argument was synthesized token verification id')
assert.deepEqual(args[0].uid, uid, 'tokenVerificationId uid was correct')
assert.equal(mockMetricsContext.stash.thisValues[1], mockRequest, 'this was request')
args = mockMetricsContext.stash.args[2]
assert.equal(args.length, 1, 'metricsContext.stash was passed one argument third time')
assert.deepEqual(args[0].tokenId, keyFetchTokenId, 'argument was key fetch token')
assert.deepEqual(args[0].uid, uid, 'keyFetchToken.uid was correct')
assert.equal(mockMetricsContext.stash.thisValues[1], mockRequest, 'this was request')
@ -998,21 +990,14 @@ describe('/account/login', function () {
assert.equal(args.length, 1, 'metricsContext.setFlowCompleteSignal was passed one argument')
assert.deepEqual(args[0], 'account.signed', 'argument was event name')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 1, 'mailer.sendNewDeviceLoginNotification was called')
assert.equal(mockMailer.sendNewDeviceLoginNotification.getCall(0).args[1].location.city, 'Mountain View')
assert.equal(mockMailer.sendNewDeviceLoginNotification.getCall(0).args[1].location.country, 'United States')
assert.equal(mockMailer.sendNewDeviceLoginNotification.getCall(0).args[1].timeZone, 'America/Los_Angeles')
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 0, 'mailer.sendVerifyLoginEmail was not called')
assert.ok(response.verified, 'response indicates account is verified')
assert.ok(!response.verificationMethod, 'verificationMethod doesn\'t exist')
assert.ok(!response.verificationReason, 'verificationReason doesn\'t exist')
}).then(function () {
mockLog.activityEvent.reset()
mockLog.flowEvent.reset()
mockMailer.sendNewDeviceLoginNotification.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.stash.reset()
mockMetricsContext.setFlowCompleteSignal.reset()
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
assert.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.city, 'Mountain View')
assert.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.country, 'United States')
assert.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].timeZone, 'America/Los_Angeles')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
assert.ok(!response.verified, 'response indicates account is not verified')
assert.equal(response.verificationMethod, 'email', 'verificationMethod is email')
assert.equal(response.verificationReason, 'login', 'verificationReason is login')
})
})
@ -1040,7 +1025,7 @@ describe('/account/login', function () {
// Verify that the email code was sent
var verifyCallArgs = mockMailer.sendVerifyCode.getCall(0).args
assert.equal(verifyCallArgs[1], emailCode, 'mailer.sendVerifyCode was called with emailCode')
assert.notEqual(verifyCallArgs[1], emailCode, 'mailer.sendVerifyCode was called with a fresh verification code')
assert.equal(mockLog.flowEvent.callCount, 2, 'log.flowEvent was called twice')
assert.equal(mockLog.flowEvent.args[0][0], 'account.login', 'first event was login')
assert.equal(mockLog.flowEvent.args[1][0], 'email.verification.sent', 'second event was sent')
@ -1050,20 +1035,12 @@ describe('/account/login', function () {
assert.equal(response.verificationMethod, 'email', 'verificationMethod is email')
assert.equal(response.verificationReason, 'signup', 'verificationReason is signup')
assert.equal(response.emailSent, true, 'email sent')
}).then(function () {
mockLog.flowEvent.reset()
mockMailer.sendVerifyCode.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.stash.reset()
})
})
})
})
describe('sign-in confirmation enabled', function () {
describe('sign-in confirmation', function () {
before(() => {
config.signinConfirmation.enabled = true
config.signinConfirmation.supportedClients = [ 'fx_desktop_v3' ]
config.signinConfirmation.forcedEmailAddresses = /.+@mozilla\.com$/
mockDB.emailRecord = function () {
@ -1082,9 +1059,7 @@ describe('/account/login', function () {
}
})
it('always on', function () {
config.signinConfirmation.sample_rate = 1
it('is enabled by default', function () {
return runTest(route, mockRequest, function (response) {
assert.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
@ -1096,64 +1071,14 @@ describe('/account/login', function () {
assert.equal(response.verificationMethod, 'email', 'verificationMethod is email')
assert.equal(response.verificationReason, 'login', 'verificationReason is login')
assert.equal(mockLog.flowEvent.callCount, 2, 'log.flowEvent was called twice')
assert.equal(mockLog.flowEvent.args[0][0], 'account.login', 'first event was login')
assert.equal(mockLog.flowEvent.args[1][0], 'email.confirmation.sent', 'second event was sent')
assert.equal(mockMetricsContext.stash.callCount, 3, 'metricsContext.stash was called three times')
var args = mockMetricsContext.stash.args[1]
assert.equal(args.length, 1, 'metricsContext.stash was passed one argument second time')
assert.ok(/^[0-9a-f]{32}$/.test(args[0].id), 'argument was synthesized token')
assert.deepEqual(args[0].uid, uid, 'token.uid was correct')
assert.equal(mockMetricsContext.stash.thisValues[1], mockRequest, 'this was request')
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
assert.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.city, 'Mountain View')
assert.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].location.country, 'United States')
assert.equal(mockMailer.sendVerifyLoginEmail.getCall(0).args[2].timeZone, 'America/Los_Angeles')
}).then(function () {
mockLog.flowEvent.reset()
mockMailer.sendVerifyLoginEmail.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.stash.reset()
})
})
it('on for email regex match, keys requested', function () {
mockRequest.payload.email = 'test@mozilla.com'
mockDB.emailRecord = function () {
return P.resolve({
authSalt: crypto.randomBytes(32),
data: crypto.randomBytes(32),
email: 'test@mozilla.com',
emailVerified: true,
kA: crypto.randomBytes(32),
lastAuthAt: function () {
return Date.now()
},
uid: uid,
wrapWrapKb: crypto.randomBytes(32)
})
}
return runTest(route, mockRequest, function (response) {
assert.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
assert.ok(tokenData.mustVerify, 'sessionToken must be verified before use')
assert.ok(tokenData.tokenVerificationId, 'sessionToken was created unverified')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
assert.ok(!response.verified, 'response indicates account is not verified')
assert.equal(response.verificationMethod, 'email', 'verificationMethod is email')
assert.equal(response.verificationReason, 'login', 'verificationReason is login')
}).then(function () {
mockMailer.sendVerifyLoginEmail.reset()
mockDB.createSessionToken.reset()
mockMetricsContext.setFlowCompleteSignal.reset()
})
})
it('off for email regex match, keys not requested', function () {
it('does not require verification when keys are not requested', function () {
mockDB.emailRecord = function () {
return P.resolve({
authSalt: crypto.randomBytes(32),
@ -1185,101 +1110,10 @@ describe('/account/login', function () {
assert.ok(response.verified, 'response indicates account is verified')
assert.ok(!response.verificationMethod, 'verificationMethod doesn\'t exist')
assert.ok(!response.verificationReason, 'verificationReason doesn\'t exist')
}).then(function () {
mockDB.createSessionToken.reset()
mockMetricsContext.setFlowCompleteSignal.reset()
})
})
it('off for email regex mismatch', function () {
config.signinConfirmation.sample_rate = 0
mockRequest.payload.email = 'moz@fire.fox'
mockDB.emailRecord = function () {
return P.resolve({
authSalt: crypto.randomBytes(32),
data: crypto.randomBytes(32),
email: 'moz@fire.fox',
emailVerified: true,
kA: crypto.randomBytes(32),
lastAuthAt: function () {
return Date.now()
},
uid: uid,
wrapWrapKb: crypto.randomBytes(32)
})
}
return runTest(route, mockRequest, function (response) {
assert.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
assert.ok(!tokenData.mustVerify, 'sessionToken was created verified')
assert.ok(!tokenData.tokenVerificationId, 'sessionToken was created verified')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 1, 'mailer.sendNewDeviceLoginNotification was called')
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 0, 'mailer.sendVerifyLoginEmail was not called')
assert.ok(response.verified, 'response indicates account is verified')
assert.ok(!response.verificationMethod, 'verificationMethod doesn\'t exist')
assert.ok(!response.verificationReason, 'verificationReason doesn\'t exist')
}).then(function () {
mockMailer.sendNewDeviceLoginNotification.reset()
mockDB.createSessionToken.reset()
})
})
it('off for unsupported client', function () {
config.signinConfirmation.supportedClients = [ 'fx_desktop_v999' ]
return runTest(route, mockRequest, function (response) {
assert.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
assert.ok(!tokenData.mustVerify, 'sessionToken was created verified')
assert.ok(!tokenData.tokenVerificationId, 'sessionToken was created verified')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 1, 'mailer.sendNewDeviceLoginNotification was called')
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 0, 'mailer.sendVerifyLoginEmail was not called')
assert.ok(response.verified, 'response indicates account is verified')
assert.ok(!response.verificationMethod, 'verificationMethod doesn\'t exist')
assert.ok(!response.verificationReason, 'verificationReason doesn\'t exist')
}).then(function () {
mockMailer.sendNewDeviceLoginNotification.reset()
mockDB.createSessionToken.reset()
})
})
it('on for suspicious requests', function () {
mockRequest.payload.email = 'dodgy@mcdodgeface.com'
mockRequest.app.isSuspiciousRequest = true
mockDB.emailRecord = function () {
return P.resolve({
authSalt: crypto.randomBytes(32),
data: crypto.randomBytes(32),
email: 'dodgy@mcdodgeface.com',
emailVerified: true,
kA: crypto.randomBytes(32),
lastAuthAt: function () {
return Date.now()
},
uid: uid,
wrapWrapKb: crypto.randomBytes(32)
})
}
return runTest(route, mockRequest, function (response) {
assert.equal(mockDB.createSessionToken.callCount, 1, 'db.createSessionToken was called')
var tokenData = mockDB.createSessionToken.getCall(0).args[0]
assert.ok(tokenData.mustVerify, 'sessionToken must be verified before use')
assert.ok(tokenData.tokenVerificationId, 'sessionToken was created unverified')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
assert.ok(!response.verified, 'response indicates account is not verified')
assert.equal(response.verificationMethod, 'email', 'verificationMethod is email')
assert.equal(response.verificationReason, 'login', 'verificationReason is login')
}).then(function () {
mockMailer.sendVerifyLoginEmail.reset()
mockDB.createSessionToken.reset()
delete mockRequest.app.isSuspiciousRequest
})
})
it('unverified account gets account confirmation email', function () {
config.signinConfirmation.supportedClients = [ 'fx_desktop_v3' ]
mockRequest.payload.email = 'test@mozilla.com'
mockDB.emailRecord = function () {
return P.resolve({
@ -1307,49 +1141,12 @@ describe('/account/login', function () {
assert.ok(!response.verified, 'response indicates account is not verified')
assert.equal(response.verificationMethod, 'email', 'verificationMethod is email')
assert.equal(response.verificationReason, 'signup', 'verificationReason is signup')
}).then(function () {
mockMailer.sendVerifyCode.reset()
mockDB.createSessionToken.reset()
})
})
describe('sign-in with unverified account', function () {
before(() => {
mockDB.emailRecord = function () {
return P.resolve({
authSalt: crypto.randomBytes(32),
data: crypto.randomBytes(32),
email: 'test@mozilla.com',
emailVerified: false,
kA: crypto.randomBytes(32),
lastAuthAt: function () {
return Date.now()
},
uid: uid,
wrapWrapKb: crypto.randomBytes(32)
})
}
})
it('sends verify account email', function () {
return runTest(route, mockRequest, function (response) {
assert.equal(mockMailer.sendVerifyCode.callCount, 1, 'mailer.sendVerifyCode was called')
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 0, 'mailer.sendVerifyLoginEmail was not called')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
assert.equal(response.verified, false, 'response indicates account is unverified')
assert.equal(response.verificationMethod, 'email', 'verificationMethod is email')
assert.equal(response.verificationReason, 'signup', 'verificationReason is signup')
assert.equal(response.emailSent, true, 'email not sent')
}).then(function () {
mockMailer.sendVerifyCode.reset()
})
assert.equal(response.emailSent, true, 'response indicates an email was sent')
})
})
})
it('creating too many sessions causes an error to be logged', function () {
mockDB.emailRecord = defaultEmailRecord
mockDB.emailRecord.reset()
const oldSessions = mockDB.sessions
mockDB.sessions = sinon.spy(function () {
return P.resolve(new Array(200))
@ -1519,16 +1316,6 @@ describe('/account/login', function () {
})
describe('with unblock code', () => {
mockLog.flowEvent.reset()
let previousEmailRecord
before(() => {
previousEmailRecord = mockDB.emailRecord
})
afterEach(() => {
mockDB.emailRecord = previousEmailRecord
})
it('invalid code', () => {
mockDB.consumeUnblockCode = () => P.reject(error.invalidUnblockCode())
@ -1567,7 +1354,7 @@ describe('/account/login', function () {
it('valid code', () => {
mockDB.consumeUnblockCode = () => P.resolve({ createdAt: Date.now() })
return runTest(route, mockRequestWithUnblockCode, (res) => {
assert.equal(mockLog.flowEvent.callCount, 4)
assert.equal(mockLog.flowEvent.callCount, 3)
assert.equal(mockLog.flowEvent.args[0][0], 'account.login.blocked', 'first event was account.login.blocked')
assert.equal(mockLog.flowEvent.args[1][0], 'account.login.confirmedUnblockCode', 'second event was account.login.confirmedUnblockCode')
assert.equal(mockLog.flowEvent.args[2][0], 'account.login', 'third event was account.login')

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

@ -36,9 +36,9 @@ describe('features', () => {
assert.equal(Object.keys(features).length, 5, 'object should have four properties')
assert.equal(typeof features.isSampledUser, 'function', 'isSampledUser should be function')
assert.equal(typeof features.isLastAccessTimeEnabledForUser, 'function', 'isLastAccessTimeEnabledForUser should be function')
assert.equal(typeof features.isSigninConfirmationEnabledForUser, 'function', 'isSigninConfirmationEnabledForUser should be function')
assert.equal(typeof features.isSigninUnblockEnabledForUser, 'function', 'isSigninUnblockEnabledForUser should be function')
assert.equal(typeof features.canBypassSiginConfirmation, 'function', 'canBypassSiginConfirmation should be function')
assert.equal(typeof features.isSecurityHistoryTrackingEnabled, 'function', 'isSecurityHistoryTrackingEnabled should be function')
assert.equal(typeof features.isSecurityHistoryProfilingEnabled, 'function', 'isSecurityHistoryProfilingEnabled should be function')
assert.equal(crypto.createHash.callCount, 1, 'crypto.createHash should have been called once on require')
let args = crypto.createHash.args[0]
@ -182,50 +182,6 @@ describe('features', () => {
}
)
it(
'isSigninConfirmationEnabledForUser',
() => {
const uid = 'wibble'
const email = 'blee@mozilla.com'
const request = {
app: {
isSuspiciousRequest: true
},
payload: {
metricsContext: {
context: 'iframe'
}
}
}
// First 27 characters are ignored, last 13 are 0.02 * 0xfffffffffffff
hashResult = '000000000000000000000000000051eb851eb852'
config.signinConfirmation.enabled = true
config.signinConfirmation.sample_rate = 0.03
config.signinConfirmation.forcedEmailAddresses = /.+@mozilla\.com$/
config.signinConfirmation.supportedClients = [ 'wibble', 'iframe' ]
assert.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), true, 'should return true when request is suspicious')
config.signinConfirmation.sample_rate = 0.02
request.app.isSuspiciousRequest = false
assert.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), true, 'should return true when email address matches')
config.signinConfirmation.forcedEmailAddresses = /.+@mozilla\.org$/
request.payload.metricsContext.context = 'iframe'
assert.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), false, 'should return false when email address and sample rate do not match')
config.signinConfirmation.sample_rate = 0.03
assert.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), true, 'should return true when sample rate and context match')
request.payload.metricsContext.context = ''
assert.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), false, 'should return false when context does not match')
config.signinConfirmation.enabled = false
request.payload.metricsContext.context = 'iframe'
assert.equal(features.isSigninConfirmationEnabledForUser(uid, email, request), false, 'should return false when feature is disabled')
}
)
it(
'isSigninUnblockEnabledForUser',
() => {
@ -272,39 +228,36 @@ describe('features', () => {
)
it(
'canBypassSiginConfirmation',
'isSecurityHistoryTrackingEnabled',
() => {
const request = {}
const securityEvents = []
const forceEmail = 'test@force.com'
const email = 'test@notforce.com'
config.securityHistory.enabled = true
assert.equal(features.isSecurityHistoryTrackingEnabled(), true, 'should return true when enabled in config')
config.securityHistory.enabled = false
assert.equal(features.isSecurityHistoryTrackingEnabled(), false, 'should return false when disabled in config')
}
)
it(
'isSecurityHistoryProfilingEnabled',
() => {
config.securityHistory.enabled = true
config.securityHistory.ipProfiling = {
enabled: true
}
assert.equal(features.canBypassSiginConfirmation(email, true, 'day', securityEvents, request), true, 'should return true if verified and recency within day')
assert.equal(features.isSecurityHistoryProfilingEnabled(), true, 'should return true when everything is enabled in config')
config.securityHistory.enabled = true
config.securityHistory.ipProfiling = {
enabled: false
}
assert.equal(features.canBypassSiginConfirmation(email, true, 'day', securityEvents, request), false, 'should return false if profiling disabled')
assert.equal(features.isSecurityHistoryProfilingEnabled(), false, 'should return false when profiling is disabled in config')
config.securityHistory.enabled = true
config.securityHistory.enabled = false
config.securityHistory.ipProfiling = {
enabled: true
}
assert.equal(features.canBypassSiginConfirmation(email, true, 'week', securityEvents, request), false, 'should return false if verified but not within day')
config.securityHistory.enabled = false
assert.equal(features.canBypassSiginConfirmation(email, true, 'day', securityEvents, request), false, 'should return false if security events disabled')
config.signinConfirmation.enabled = true
config.signinConfirmation.sample_rate = 1
config.signinConfirmation.forcedEmailAddresses = /.+@force\.com$/
config.securityHistory.enabled = true
assert.equal(features.canBypassSiginConfirmation(forceEmail, true, 'day', securityEvents, request), false, 'should return false if sign-in confirmation forced email')
assert.equal(features.isSecurityHistoryProfilingEnabled(), false, 'should return false when tracking is disabled in config')
}
)
})

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

@ -92,10 +92,7 @@ var config = {
}
},
signinConfirmation: {
forcedEmailAddresses: /.+@mozilla\.com$/,
enabled: true,
sample_rate: 1,
supportedClients: ['fx_desktop_v3']
forcedEmailAddresses: /.+@mozilla\.com$/
},
signinUnblock: {
enabled: false
@ -347,6 +344,75 @@ describe('IP Profiling', () => {
})
})
it(
'previously verified session with suspicious request',
() => {
mockRequest.payload.email = TEST_EMAIL
var mockDB = mocks.mockDB({
email: TEST_EMAIL,
emailVerified: true,
keyFetchTokenId: keyFetchTokenId,
sessionTokenId: sessionTokenId,
uid: uid
})
mockDB.emailRecord = function () {
return P.resolve({
authSalt: crypto.randomBytes(32),
data: crypto.randomBytes(32),
email: TEST_EMAIL,
emailVerified: true,
kA: crypto.randomBytes(32),
lastAuthAt: function () {
return Date.now()
},
uid: uid,
wrapWrapKb: crypto.randomBytes(32)
})
}
mockDB.securityEvents = function () {
return P.resolve([
{
name: 'account.login',
createdAt: Date.now(),
verified: true
}
])
}
var accountRoutes = makeRoutes({
checkPassword: function () {
return P.resolve(true)
},
config: config,
customs: mockCustoms,
db: mockDB,
log: mockLog,
mailer: mockMailer,
push: mockPush
})
mockRequest.app = {
isSuspiciousRequest: true
}
route = getRoute(accountRoutes, '/account/login')
return runTest(route, mockRequest, function (response) {
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 1, 'mailer.sendVerifyLoginEmail was called')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
assert.equal(response.verified, false, 'session verified')
return runTest(route, mockRequest)
})
.then(function (response) {
assert.equal(mockMailer.sendVerifyLoginEmail.callCount, 2, 'mailer.sendVerifyLoginEmail was called')
assert.equal(mockMailer.sendNewDeviceLoginNotification.callCount, 0, 'mailer.sendNewDeviceLoginNotification was not called')
assert.equal(response.verified, false, 'session verified')
})
})
afterEach(() => {
mockMailer.sendVerifyLoginEmail.reset()
mockMailer.sendNewDeviceLoginNotification.reset()

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

@ -29,7 +29,6 @@ describe('remote account preverified token', function() {
let server
before(() => {
process.env.TRUSTED_JKUS = 'http://127.0.0.1:9000/.well-known/public-keys'
process.env.SIGNIN_CONFIRMATION_ENABLED = false
return TestServer.start(config)
.then(s => {

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

@ -15,7 +15,6 @@ describe('remote account reset', function() {
this.timeout(15000)
let server
before(() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = false
return TestServer.start(config)
.then(s => {
server = s
@ -227,7 +226,6 @@ describe('remote account reset', function() {
)
after(() => {
delete process.env.SIGNIN_CONFIRMATION_ENABLE
return TestServer.stop(server)
})

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

@ -1,138 +0,0 @@
/* 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 assert = require('insist')
var TestServer = require('../test_server')
const Client = require('../client')()
describe('remote account signin verification enable', function() {
this.timeout(30000)
it(
'signin confirmation can be disabled',
() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = false
var config = require('../../config').getProperties()
var server, email, client
var password = 'allyourbasearebelongtous'
return TestServer.start(config)
.then(function main(serverObj) {
server = serverObj
email = server.uniqueEmail()
})
.then(function() {
return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys:true})
})
.then(
function (x) {
client = x
assert.ok(client.authAt, 'authAt was set')
}
)
.then(
function () {
return client.emailStatus()
}
)
.then(
function (status) {
assert.equal(status.verified, true, 'account is verified')
}
)
.then(
function () {
return client.login({keys:true})
}
)
.then(
function (response) {
assert.notEqual(response.verificationMethod, 'email', 'verification method not set')
assert.notEqual(response.verificationReason, 'login', 'verification reason not set')
}
)
.then(
function () {
return client.emailStatus()
}
)
.then(
function (status) {
assert.equal(status.verified, true, 'account is verified')
}
)
.then(function() {
return TestServer.stop(server)
})
}
)
it(
'signin confirmation can be enabled',
() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = true
process.env.SIGNIN_CONFIRMATION_RATE = 1.0
var config = require('../../config').getProperties()
var server, email, client
var password = 'allyourbasearebelongtous'
TestServer.start(config)
.then(function main(serverObj) {
server = serverObj
email = server.uniqueEmail()
})
.then(function() {
return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys:true})
})
.then(
function (x) {
client = x
assert.ok(client.authAt, 'authAt was set')
}
)
.then(
function () {
return client.emailStatus()
}
)
.then(
function (status) {
assert.equal(status.verified, true, 'account is verified')
}
)
.then(
function () {
return client.login({keys:true})
}
)
.then(
function (response) {
assert.equal(response.verificationMethod, 'email', 'verification method set')
assert.equal(response.verificationReason, 'login', 'verification reason set')
}
)
.then(
function () {
return client.emailStatus()
}
)
.then(
function (status) {
assert.equal(status.verified, false, 'account is not verified')
assert.equal(status.emailVerified, true, 'email is verified')
assert.equal(status.sessionVerified, false, 'session is not verified')
}
)
.then(function() {
return TestServer.stop(server)
})
}
)
after(() => {
delete process.env.SIGNIN_CONFIRMATION_ENABLED
TestServer.stop()
})
})

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

@ -22,8 +22,6 @@ describe('remote account signin verification', function() {
this.timeout(30000)
let server
before(() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = true
process.env.SIGNIN_CONFIRMATION_RATE = 1.0
process.env.IP_PROFILING_ENABLED = false
return TestServer.start(config)

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

@ -18,7 +18,6 @@ describe('remote flow', function() {
let server
let email1
before(() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = false
return TestServer.start(config)
.then(s => {
server = s
@ -81,7 +80,7 @@ describe('remote flow', function() {
'e': '65537'
}
var duration = 1000 * 60 * 60 * 24 // 24 hours
return Client.login(config.publicUrl, email, password, server.mailbox, {keys:true})
return Client.loginAndVerify(config.publicUrl, email, password, server.mailbox, {keys:true})
.then(
function (x) {
client = x
@ -112,7 +111,6 @@ describe('remote flow', function() {
)
after(() => {
delete process.env.SIGNIN_CONFIRMATION_ENABLED
return TestServer.stop(server)
})
})

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

@ -24,8 +24,6 @@ describe('remote password change', function() {
this.timeout(15000)
let server
before(() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = true
process.env.SIGNIN_CONFIRMATION_RATE = 1.0
process.env.IP_PROFILING_ENABLED = false
return TestServer.start(config)
@ -395,8 +393,6 @@ describe('remote password change', function() {
)
after(() => {
delete process.env.SIGNIN_CONFIRMATION_ENABLED
delete process.env.SIGNIN_CONFIRMATION_RATE
delete process.env.IP_PROFILING_ENABLED
return TestServer.stop(server)
})

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

@ -17,7 +17,6 @@ describe('remote password forgot', function() {
this.timeout(15000)
let server
before(() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = false
return TestServer.start(config)
.then(s => {
server = s
@ -423,7 +422,6 @@ describe('remote password forgot', function() {
)
after(() => {
delete process.env.SIGNIN_CONFIRMATION_ENABLED
return TestServer.stop(server)
})

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

@ -14,8 +14,6 @@ describe('remote recovery email resend code', function() {
this.timeout(15000)
let server
before(() => {
process.env.SIGNIN_CONFIRMATION_ENABLED = true
process.env.SIGNIN_CONFIRMATION_RATE = 1.0
process.env.IP_PROFILING_ENABLED = false
return TestServer.start(config)
@ -149,8 +147,6 @@ describe('remote recovery email resend code', function() {
)
after(() => {
delete process.env.SIGNIN_CONFIRMATION_ENABLED
delete process.env.SIGNIN_CONFIRMATION_RATE
delete process.env.IP_PROFILING_ENABLED
return TestServer.stop(server)
})

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

@ -29,7 +29,6 @@ describe('remote verifier upgrade', function() {
before(() => {
process.env.VERIFIER_VERSION = '0'
process.env.SIGNIN_CONFIRMATION_ENABLED = false
})
it(
@ -84,7 +83,7 @@ describe('remote verifier upgrade', function() {
.then(
function (server) {
var client
return Client.login(config.publicUrl, email, password, server.mailbox)
return Client.loginAndVerify(config.publicUrl, email, password, server.mailbox)
.then(
function (x) {
client = x
@ -139,6 +138,5 @@ describe('remote verifier upgrade', function() {
after(() => {
delete process.env.VERIFIER_VERSION
delete process.env.SIGNIN_CONFIRMATION_ENABLED
})
})