feat(emails): Send `New Device Login` email on all new logins

This commit is contained in:
Vijay Budhram 2022-09-01 11:57:08 -04:00
Родитель 42811d1693
Коммит df857c569d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: EBAEC5D86596C9EE
15 изменённых файлов: 72 добавлений и 67 удалений

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

@ -17,7 +17,8 @@
"format": "yarn workspaces foreach run format",
"ports": "pm2 jlist | json -a -c 'this.pm2_env.env.PORT' pm2_env.env.PORT name",
"heroku-postbuild": "yarn workspaces foreach --verbose --include 123done install",
"mysql": "docker exec -it $(docker container ls | grep mysql | cut -d' ' -f1) mysql"
"mysql": "docker exec -it $(docker container ls | grep mysql | cut -d' ' -f1) mysql",
"firefox": "./packages/fxa-dev-launcher/bin/fxa-dev-launcher.mjs"
},
"homepage": "https://github.com/mozilla/fxa",
"bugs": {

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

@ -1011,50 +1011,48 @@ export class AccountHandler {
verificationMethod
);
// For new sync logins that don't send some other sort of email,
// For new logins that don't send some other sort of email,
// send an after-the-fact notification email so that the user
// is aware that something happened on their account.
if (accountRecord.primaryEmail.isVerified) {
if (sessionToken.tokenVerified || !sessionToken.mustVerify) {
if (requestHelper.wantsKeys(request)) {
const geoData = request.app.geo;
const service =
(request.payload as any).service || request.query.service;
const ip = request.app.clientAddress;
const { deviceId, flowId, flowBeginTime } = await request.app
.metricsContext;
const geoData = request.app.geo;
const service =
(request.payload as any).service || request.query.service;
const ip = request.app.clientAddress;
const { deviceId, flowId, flowBeginTime } = await request.app
.metricsContext;
try {
await this.mailer.sendNewDeviceLoginEmail(
accountRecord.emails,
accountRecord,
{
acceptLanguage: request.app.acceptLanguage,
deviceId,
flowId,
flowBeginTime,
ip,
location: geoData.location,
service,
timeZone: geoData.timeZone,
uaBrowser: request.app.ua.browser,
uaBrowserVersion: request.app.ua.browserVersion,
uaOS: request.app.ua.os,
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid,
}
);
} catch (err) {
// If we couldn't email them, no big deal. Log
// and pretend everything worked.
this.log.trace(
'Account.login.sendNewDeviceLoginNotification.error',
{
error: err,
}
);
}
try {
await this.mailer.sendNewDeviceLoginEmail(
accountRecord.emails,
accountRecord,
{
acceptLanguage: request.app.acceptLanguage,
deviceId,
flowId,
flowBeginTime,
ip,
location: geoData.location,
service,
timeZone: geoData.timeZone,
uaBrowser: request.app.ua.browser,
uaBrowserVersion: request.app.ua.browserVersion,
uaOS: request.app.ua.os,
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid,
},
);
} catch (err) {
// If we couldn't email them, no big deal. Log
// and pretend everything worked.
this.log.trace(
'Account.login.sendNewDeviceLoginNotification.error',
{
error: err,
},
);
}
}
}

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

@ -335,21 +335,17 @@ module.exports = (log, db, mailer, customs, config) => {
};
// Check to see if this token was just verified, if it is, then this means
// the user has enabled two step authentication, otherwise send new device
// the user has enabled two-step authentication, otherwise send new device
// login email.
if (isValidCode && !tokenVerified) {
return mailer.sendPostAddTwoStepAuthenticationEmail(
account.emails,
account,
emailOptions
);
}
if (isValidCode) {
if (!tokenVerified) {
return mailer.sendPostAddTwoStepAuthenticationEmail(
account.emails,
account,
emailOptions
);
}
// All accounts that have a TOTP token, force the session to be verified, therefore
// we can not check `session.mustVerify=true` to determine sending the new device
// login email. Instead, lets perform a basic check that the service is `sync`, otherwise
// don't send.
if (isValidCode && service === 'sync') {
return mailer.sendNewDeviceLoginEmail(
account.emails,
account,

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

@ -12,7 +12,7 @@ export default {
const createStory = storyWithProps(
'newDeviceLogin',
'Sent to notify the account that a new device has signed in.',
'Sent to notify the account that a new device or service has signed in.',
{
...MOCK_USER_INFO,
clientName: 'Firefox',

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

@ -2575,9 +2575,7 @@ describe('/account/login', () => {
tokenData.tokenVerificationId,
'sessionToken was created unverified'
);
// Note that *neither* email is sent in this case,
// since it can't have been a new device connection.
assert.equal(mockMailer.sendNewDeviceLoginEmail.callCount, 0);
assert.equal(mockMailer.sendNewDeviceLoginEmail.callCount, 1);
assert.equal(
mockMailer.sendVerifyLoginEmail.callCount,
0,

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

@ -380,7 +380,7 @@ describe('totp', () => {
);
// correct emails sent
assert.equal(mailer.sendNewDeviceLoginEmail.callCount, 0);
assert.equal(mailer.sendNewDeviceLoginEmail.callCount, 1);
assert.equal(mailer.sendPostAddTwoStepAuthenticationEmail.callCount, 0);
});
});

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

@ -331,6 +331,10 @@ describe('remote account signin verification', function () {
.then((c) => {
client2 = c;
})
.then(() => {
// Clears inbox of new signin email
return server.mailbox.waitForEmail(email);
})
.then(() => {
return client2.login(options);
})

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

@ -109,6 +109,10 @@ describe('remote recovery email resend code', function () {
.then((c) => {
client2 = c;
})
.then(() => {
// Clears inbox of new signin email
return server.mailbox.waitForEmail(email);
})
.then(() => {
return client2.login(options);
})

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

@ -447,6 +447,10 @@ describe('remote session', function () {
})
.then((x) => {
client = x;
// Clears inbox of new signin email
return server.mailbox.waitForEmail(email);
})
.then(() => {
return client.sessionStatus();
})
.then((status) => {

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

@ -303,7 +303,7 @@ registerSuite('oauth reset password with TOTP', {
tests: {
'reset password, verify same browser same tab': function () {
return this.remote
.then(openVerificationLinkInSameTab(email, 1))
.then(openVerificationLinkInSameTab(email, 2))
.then(fillOutCompleteResetPassword(PASSWORD, PASSWORD))
.then(testElementExists(selectors.TOTP_SIGNIN.HEADER))
@ -320,7 +320,7 @@ registerSuite('oauth reset password with TOTP', {
'reset password, verify same browser different tab': function () {
return this.remote
.then(openVerificationLinkInNewTab(email, 1))
.then(openVerificationLinkInNewTab(email, 2))
.then(switchToWindow(1))
.then(fillOutCompleteResetPassword(PASSWORD, PASSWORD))
@ -342,7 +342,7 @@ registerSuite('oauth reset password with TOTP', {
function () {
return (
this.remote
.then(openPasswordResetLinkInDifferentBrowser(email, PASSWORD, 1))
.then(openPasswordResetLinkInDifferentBrowser(email, PASSWORD, 2))
// user verified in a new browser, they have to enter
// their password in the original tab.
@ -373,7 +373,7 @@ registerSuite('oauth reset password with TOTP', {
this.remote
// clear all browser state, simulate opening in a new browser
.then(clearBrowserState({ forceAll: true }))
.then(openVerificationLinkInSameTab(email, 1))
.then(openVerificationLinkInSameTab(email, 2))
.then(fillOutCompleteResetPassword(PASSWORD, PASSWORD))
// this tab's success is seeing the reset password complete header.

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

@ -144,7 +144,7 @@ registerSuite('signin to Sync after OAuth', {
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT))
.then(testElementExists(selectors.SIGNIN_TOKEN_CODE.HEADER))
.then(fillOutSignInTokenCode(email, 0))
.then(fillOutSignInTokenCode(email, 1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER));
},
},

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

@ -304,7 +304,7 @@ registerSuite('Recovery key - unverified session', {
.then(click(selectors.RECOVERY_KEY.GENERATE_KEY_BUTTON))
// if the session is unverified, then the modal will be shown.
.then(testElementExists('[data-testid=modal-verify-session]'))
.then(fillOutVerificationCode(email, 0))
.then(fillOutVerificationCode(email, 1))
.then(
testElementExists(
selectors.SETTINGS.SECURITY.RECOVERY_KEY.PASSWORD_TEXTBOX_LABEL

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

@ -311,7 +311,7 @@ registerSuite('TOTP - unverified session', {
.then(testElementExists(selectors.TOTP.UNLOCK_SEND_VERIFY))
// send and open verification in same tab
.then(fillOutVerificationCode(email, 0))
.then(fillOutVerificationCode(email, 1))
// panel becomes verified and can be opened
.then(

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

@ -399,7 +399,7 @@ registerSuite('signin blocked', {
)
.then(testElementExists(selectors.SIGNIN_UNBLOCK.HEADER))
.then(fillOutSignInUnblock(email, 2))
.then(fillOutSignInUnblock(email, 3))
.then(testElementExists(selectors.SETTINGS.HEADER))
);

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

@ -388,7 +388,7 @@ registerSuite('cached signin', {
selectors.SIGNIN_TOKEN_CODE.HEADER
)
)
.then(fillOutSignInTokenCode(email, 0))
.then(fillOutSignInTokenCode(email, 1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(testIsBrowserNotified('fxaccounts:login'))