Merge pull request #3595 from mozilla/signup-code-unverified-signin r=@vbudhram

feat(signin): Convert signin w/ unverified account to codes
This commit is contained in:
Shane Tomlinson 2019-12-12 10:30:27 +00:00 коммит произвёл GitHub
Родитель 8255380181 f384a7eed4
Коммит 3e99324488
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 116 добавлений и 105 удалений

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

@ -313,12 +313,14 @@ module.exports = (log, config, customs, db, mailer) => {
}
function sendVerifyAccountEmail() {
if (verificationMethod === 'email-otp') {
return sendVerifyLoginCodeEmail();
}
// If the session doesn't require verification,
// fall back to the account-level email code for the link.
const emailCode =
sessionToken.tokenVerificationId ||
accountRecord.primaryEmail.emailCode;
return mailer
.sendVerifyEmail([], accountRecord, {
code: emailCode,
@ -467,9 +469,19 @@ module.exports = (log, config, customs, db, mailer) => {
getSessionVerificationStatus(sessionToken, verificationMethod) {
if (!sessionToken.emailVerified) {
// for unverified accounts, only 'email', and 'email-otp' are valid.
// email-otp is the end goal, but a transition train is needed.
// Set the default to 'email' to handle train->train upgrades. If
// a user of content-server X loads their JS and then auth-server X+1
// is deployed, the content server of train X is not able to handle
// the 'email-otp' result and still send the user to the /confirm screen
// that expect verification links.
if (verificationMethod !== 'email-otp') {
verificationMethod = 'email';
}
return {
verified: false,
verificationMethod: 'email',
verificationMethod: verificationMethod,
verificationReason: 'signup',
};
}

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

@ -1366,7 +1366,7 @@ describe('getSessionVerificationStatus', () => {
});
});
it('does not echo custom verificationMethod param for signups', () => {
it('does not echo invalid custom verificationMethod param for signups', () => {
const sessionToken = {
emailVerified: false,
tokenVerified: false,
@ -1379,4 +1379,18 @@ describe('getSessionVerificationStatus', () => {
verificationReason: 'signup',
});
});
it('correctly echos valid custom verificationMethod param for signups', () => {
const sessionToken = {
emailVerified: false,
tokenVerified: false,
mustVerify: true,
};
const res = getSessionVerificationStatus(sessionToken, 'email-otp');
assert.deepEqual(res, {
verified: false,
verificationMethod: 'email-otp',
verificationReason: 'signup',
});
});
});

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

@ -243,6 +243,12 @@ var ERRORS = {
errno: 165,
message: t('Failed due to a conflicting request, please try again.'),
},
INSUFFICIENT_ACR_VALUES: {
errno: 170,
message: t(
'This request requires two step authentication enabled on your account.'
),
},
UNKNOWN_SUBSCRIPTION_CUSTOMER: {
errno: 176,
message: t('Unknown customer for subscription.'),

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

@ -12,6 +12,7 @@ enum VerificationMethods {
EMAIL = 'email',
EMAIL_2FA = 'email-2fa',
EMAIL_CAPTCHA = 'email-captcha',
EMAIL_OTP = 'email-otp',
TOTP_2FA = 'totp-2fa',
}

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

@ -15,6 +15,7 @@ import ProfileErrors from '../lib/profile-errors';
import ProfileImage from './profile-image';
import ResumeTokenMixin from './mixins/resume-token';
import SignInReasons from '../lib/sign-in-reasons';
import VerificationMethods from '../lib/verification-methods';
import vat from '../lib/vat';
// Account attributes that can be persisted
@ -585,6 +586,7 @@ const Account = Backbone.Model.extend(
// can be updated with the correct case.
skipCaseError: true,
unblockCode: options.unblockCode,
verificationMethod: VerificationMethods.EMAIL_OTP,
};
// `originalLoginEmail` is specified when the account's primary email has changed.
@ -595,10 +597,6 @@ const Account = Backbone.Model.extend(
signinOptions.originalLoginEmail = originalLoginEmail;
}
if (options.verificationMethod) {
signinOptions.verificationMethod = options.verificationMethod;
}
if (!sessionToken) {
// We need to do a completely fresh login.
return this._fxaClient.signIn(

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

@ -130,6 +130,7 @@ export default {
if (
AuthErrors.is(err, 'TOTP_REQUIRED') ||
AuthErrors.is(err, 'INSUFFICIENT_ACR_VALUES') ||
OAuthErrors.is(err, 'MISMATCH_ACR_VALUES')
) {
err.forceMessage = t(
@ -189,6 +190,13 @@ export default {
return this.navigate('signin_token_code', { account });
}
if (
verificationReason === VerificationReasons.SIGN_IN &&
verificationMethod === VerificationMethods.EMAIL_OTP
) {
return this.navigate('signin_token_code', { account });
}
if (
verificationReason === VerificationReasons.SIGN_IN &&
verificationMethod === VerificationMethods.TOTP_2FA
@ -196,6 +204,13 @@ export default {
return this.navigate('signin_totp_code', { account });
}
if (
verificationReason === VerificationReasons.SIGN_UP &&
verificationMethod === VerificationMethods.EMAIL_OTP
) {
return this.navigate('confirm_signup_code', { account });
}
return this.navigate('confirm', { account });
}

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

@ -440,14 +440,12 @@ describe('models/account', function() {
describe('with a password and no sessionToken', () => {
describe('unverified, reason === undefined', () => {
beforeEach(() => {
sinon.stub(fxaClient, 'signIn').callsFake(() => {
return Promise.resolve({
sessionToken: SESSION_TOKEN,
uid: UID,
verificationMethod: VerificationMethods.EMAIL,
verificationReason: VerificationReasons.SIGN_UP,
verified: false,
});
sinon.stub(fxaClient, 'signIn').resolves({
sessionToken: SESSION_TOKEN,
uid: UID,
verificationMethod: VerificationMethods.EMAIL_OTP,
verificationReason: VerificationReasons.SIGN_UP,
verified: false,
});
sinon.stub(fxaClient, 'signUpResend').callsFake(() => {
@ -471,6 +469,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
})
);
});
@ -496,13 +495,11 @@ describe('models/account', function() {
describe('verified account, unverified session', () => {
beforeEach(() => {
sinon.stub(fxaClient, 'signIn').callsFake(() => {
return Promise.resolve({
sessionToken: SESSION_TOKEN,
verificationMethod: VerificationMethods.EMAIL,
verificationReason: VerificationReasons.SIGN_IN,
verified: false,
});
sinon.stub(fxaClient, 'signIn').resolves({
sessionToken: SESSION_TOKEN,
verificationMethod: VerificationMethods.EMAIL_OTP,
verificationReason: VerificationReasons.SIGN_IN,
verified: false,
});
sinon.stub(fxaClient, 'signUpResend').callsFake(() => {
@ -523,7 +520,7 @@ describe('models/account', function() {
it('updates the account with the returned data', () => {
assert.equal(
account.get('verificationMethod'),
VerificationMethods.EMAIL
VerificationMethods.EMAIL_OTP
);
assert.equal(
account.get('verificationReason'),
@ -569,6 +566,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
})
);
});
@ -621,6 +619,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
};
const secondExpectedOptions = {
@ -633,6 +632,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
};
assert.equal(fxaClient.signIn.callCount, 2);
@ -744,13 +744,11 @@ describe('models/account', function() {
describe('unverified, reason === undefined', () => {
beforeEach(() => {
sinon.stub(fxaClient, 'sessionReauth').callsFake(() => {
return Promise.resolve({
uid: UID,
verificationMethod: VerificationMethods.EMAIL,
verificationReason: VerificationReasons.SIGN_UP,
verified: false,
});
sinon.stub(fxaClient, 'sessionReauth').resolves({
uid: UID,
verificationMethod: VerificationMethods.EMAIL_OTP,
verificationReason: VerificationReasons.SIGN_UP,
verified: false,
});
sinon.stub(fxaClient, 'signIn').callsFake(() => {
@ -779,6 +777,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
}
)
);
@ -805,12 +804,10 @@ describe('models/account', function() {
describe('verified account, unverified session', () => {
beforeEach(() => {
sinon.stub(fxaClient, 'sessionReauth').callsFake(() => {
return Promise.resolve({
verificationMethod: VerificationMethods.EMAIL,
verificationReason: VerificationReasons.SIGN_IN,
verified: false,
});
sinon.stub(fxaClient, 'sessionReauth').resolves({
verificationMethod: VerificationMethods.EMAIL_OTP,
verificationReason: VerificationReasons.SIGN_IN,
verified: false,
});
sinon.stub(fxaClient, 'signIn').callsFake(() => {
@ -838,7 +835,7 @@ describe('models/account', function() {
it('updates the account with the returned data', () => {
assert.equal(
account.get('verificationMethod'),
VerificationMethods.EMAIL
VerificationMethods.EMAIL_OTP
);
assert.equal(
account.get('verificationReason'),
@ -890,6 +887,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
}
)
);
@ -947,6 +945,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
};
const secondExpectedOptions = {
@ -959,6 +958,7 @@ describe('models/account', function() {
resume: 'resume token',
skipCaseError: true,
unblockCode: 'unblock code',
verificationMethod: VerificationMethods.EMAIL_OTP,
};
assert.equal(fxaClient.sessionReauth.callCount, 2);
@ -1085,12 +1085,10 @@ describe('models/account', function() {
beforeEach(function() {
account.set('sessionToken', SESSION_TOKEN);
sinon.stub(fxaClient, 'recoveryEmailStatus').callsFake(function() {
return Promise.resolve({
verificationMethod: VerificationMethods.EMAIL,
verificationReason: VerificationReasons.SIGN_IN,
verified: false,
});
sinon.stub(fxaClient, 'recoveryEmailStatus').resolves({
verificationMethod: VerificationMethods.EMAIL_OTP,
verificationReason: VerificationReasons.SIGN_IN,
verified: false,
});
sinon.stub(fxaClient, 'signUpResend').callsFake(function() {
@ -1119,7 +1117,7 @@ describe('models/account', function() {
);
assert.equal(
account.get('verificationMethod'),
VerificationMethods.EMAIL
VerificationMethods.EMAIL_OTP
);
});
});

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

@ -11,15 +11,12 @@ const selectors = require('./lib/selectors');
const {
clearBrowserState,
closeCurrentWindow,
createUser,
fillOutForceAuth,
fillOutSignInTokenCode,
fillOutSignInUnblock,
openForceAuth,
openVerificationLinkInNewTab,
respondToWebChannelMessage,
switchToWindow,
testElementExists,
testIsBrowserNotified,
thenify,
@ -35,7 +32,7 @@ const setupTest = thenify(function(options) {
? selectors.SIGNIN_UNBLOCK.HEADER
: options.preVerified
? selectors.SIGNIN_TOKEN_CODE.HEADER
: selectors.CONFIRM_SIGNUP.HEADER;
: selectors.CONFIRM_SIGNUP_CODE.HEADER;
return this.parent
.then(clearBrowserState())
@ -80,10 +77,7 @@ registerSuite('Fx Fennec Sync v1 force_auth', {
// email 0 - initial sign up email
// email 1 - sign in w/ unverified address email
// email 2 - "You have verified your Firefox Account"
.then(openVerificationLinkInNewTab(email, 1))
.then(switchToWindow(1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(closeCurrentWindow())
.then(fillOutSignInTokenCode(email, 1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(testIsBrowserNotified('fxaccounts:login'))

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

@ -19,7 +19,6 @@ const PASSWORD = '12345678';
const {
clearBrowserState,
click,
closeCurrentWindow,
createUser,
deleteAllSms,
disableInProd,
@ -28,9 +27,7 @@ const {
fillOutSignInUnblock,
getSmsSigninCode,
openPage,
openVerificationLinkInNewTab,
respondToWebChannelMessage,
switchToWindow,
testElementExists,
testElementTextEquals,
testElementTextInclude,
@ -73,16 +70,15 @@ registerSuite('Fx Fennec Sync v1 sign_in', {
return (
this.remote
.then(
setupTest(selectors.CONFIRM_SIGNUP.HEADER, { preVerified: false })
setupTest(selectors.CONFIRM_SIGNUP_CODE.HEADER, {
preVerified: false,
})
)
// email 0 - initial sign up email
// email 1 - sign in w/ unverified address email
// email 2 - "You have verified your Firefox Account"
.then(openVerificationLinkInNewTab(email, 1))
.then(switchToWindow(1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(closeCurrentWindow())
.then(fillOutSignInTokenCode(email, 1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(testIsBrowserNotified('fxaccounts:login'))
@ -118,7 +114,7 @@ registerSuite('Fx Fennec Sync v1 sign_in', {
// The phoneNumber is reused across tests, delete all
// if its SMS messages to ensure a clean slate.
.then(deleteAllSms(testPhoneNumber))
.then(setupTest(selectors.CONFIRM_SIGNUP.HEADER))
.then(setupTest(selectors.CONFIRM_SIGNUP_CODE.HEADER))
.then(openPage(SMS_PAGE_URL, selectors.SMS_SEND.HEADER))
.then(type(selectors.SMS_SEND.PHONE_NUMBER, testPhoneNumber))

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

@ -22,7 +22,6 @@ const PASSWORD = '12345678';
const {
clearBrowserState,
click,
closeCurrentWindow,
createUser,
deleteAllSms,
disableInProd,
@ -32,8 +31,6 @@ const {
getSmsSigninCode,
noPageTransition,
openPage,
openVerificationLinkInNewTab,
switchToWindow,
testElementExists,
testElementTextEquals,
testElementTextInclude,
@ -54,7 +51,7 @@ const setupTest = thenify(function(options = {}) {
? selectors.SIGNIN_UNBLOCK.HEADER
: options.preVerified
? selectors.SIGNIN_TOKEN_CODE.HEADER
: selectors.CONFIRM_SIGNUP.HEADER;
: selectors.CONFIRM_SIGNUP_CODE.HEADER;
return this.parent
.then(createUser(email, PASSWORD, { preVerified: options.preVerified }))
@ -150,10 +147,7 @@ registerSuite('FxiOS v1 signin', {
// email 0 - initial sign up email
// email 1 - sign in w/ unverified address email
// email 2 - "You have verified your Firefox Account"
.then(openVerificationLinkInNewTab(email, 1, { query }))
.then(switchToWindow(1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(closeCurrentWindow())
.then(fillOutSignInTokenCode(email, 1, { query }))
// In Fx for iOS >= 6.1, user should redirect to the signup-complete
// page after verification.

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

@ -29,13 +29,13 @@ const {
fillOutForceAuth,
fillOutEmailFirstSignIn,
fillOutEmailFirstSignUp,
fillOutSignInTokenCode,
fillOutSignUpCode,
noSuchElement,
openFxaFromRp: openFxaFromTrustedRp,
openFxaFromUntrustedRp,
openPage,
openSettingsInNewTab,
openVerificationLinkInSameTab,
switchToWindow,
testElementExists,
testUrlEquals,
@ -112,15 +112,13 @@ registerSuite('oauth permissions for untrusted reliers', {
.then(
click(
selectors.OAUTH_PERMISSIONS.SUBMIT,
// TODO - this should go to CONFIRM_SIGNUP_CODE
selectors.CONFIRM_SIGNUP.HEADER
selectors.CONFIRM_SIGNUP_CODE.HEADER
)
)
// get the second email, the first was sent on client.signUp w/
// preVerified: false above. The second email has the `service` and
// `resume` parameters.
.then(openVerificationLinkInSameTab(email, 1))
// preVerified: false above.
.then(fillOutSignInTokenCode(email, 1))
// user verifies in the same tab, so they are logged in to the RP.
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
);

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

@ -141,7 +141,7 @@ registerSuite('oauth prompt=none', {
.then(openPage(EMAIL_FIRST_URL, selectors.ENTER_EMAIL.HEADER))
.then(fillOutEmailFirstSignIn(email, PASSWORD))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(testElementExists(selectors.CONFIRM_SIGNUP_CODE.HEADER))
.then(openRP({ query: { login_hint: email, return_on_error: false } }))
.then(click(selectors['123DONE'].BUTTON_PROMPT_NONE))

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

@ -43,7 +43,6 @@ const {
generateTotpCode,
openFxaFromRp,
openPage,
openVerificationLinkInSameTab,
testElementExists,
testElementTextEquals,
testElementTextInclude,
@ -206,18 +205,12 @@ registerSuite('oauth signin', {
.then(fillOutEmailFirstSignIn(email, PASSWORD))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(
testElementTextInclude(
selectors.CONFIRM_SIGNUP.EMAIL_MESSAGE,
email
)
)
.then(testElementExists(selectors.CONFIRM_SIGNUP_CODE.HEADER))
// get the second email, the first was sent on client.signUp w/
// preVerified: false above. The second email has the `service` and
// `resume` parameters.
.then(openVerificationLinkInSameTab(email, 1))
.then(fillOutSignInTokenCode(email, 1))
// user verifies in the same tab, so they are logged in to the RP.
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
);

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

@ -60,9 +60,10 @@ function unverifiedAccountTest(suite, page) {
this.remote
.then(openPage(url, selectors.ENTER_EMAIL.HEADER))
.then(fillOutEmailFirstSignIn(email, PASSWORD))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(testElementExists(selectors.CONFIRM_SIGNUP_CODE.HEADER))
// Expect to get redirected to confirm since the account is unverified
// TODO - before merge, fix re-load with an unverified session.
.then(openPage(url, selectors.CONFIRM_SIGNUP.HEADER))
);
};

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

@ -20,11 +20,11 @@ const {
closeCurrentWindow,
createUser,
fillOutEmailFirstSignIn,
fillOutSignInTokenCode,
fillOutSignInUnblock,
getUnblockInfo,
openPage,
openTab,
openVerificationLinkInSameTab,
switchToWindow,
testErrorTextInclude,
testElementExists,
@ -331,7 +331,7 @@ registerSuite('signin blocked', {
);
},
'unverified user': function() {
unverified: function() {
email = TestHelpers.createEmail('blocked{id}');
return (
@ -346,12 +346,12 @@ registerSuite('signin blocked', {
// It's substandard UX, but we decided to punt on making
// users verified until v2. When submitting an unblock code
// verifies unverified users, they will not need to open
// the signup verification link, instead they'll go directly
// verifies unverified users, they will not need to enter
// the verification code, instead they'll go directly
// to the settings page.
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(testElementExists(selectors.CONFIRM_SIGNUP_CODE.HEADER))
.then(openVerificationLinkInSameTab(email, 2))
.then(fillOutSignInTokenCode(email, 2))
.then(testElementExists(selectors.SETTINGS.HEADER))
);
},

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

@ -146,8 +146,7 @@ registerSuite('signup here', {
.then(fillOutEmailFirstSignIn(emailWithoutSpace, PASSWORD))
// user is not confirmed, success is seeing the confirm screen.
// TODO - this should redirect to CONFIRM_SIGNUP_PASSWORD
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(testElementExists(selectors.CONFIRM_SIGNUP_CODE.HEADER))
);
},
@ -165,8 +164,7 @@ registerSuite('signup here', {
.then(fillOutEmailFirstSignIn(emailWithoutSpace, PASSWORD))
// user is not confirmed, success is seeing the confirm screen.
// TODO - this should redirect to CONFIRM_SIGNUP_PASSWORD
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(testElementExists(selectors.CONFIRM_SIGNUP_CODE.HEADER))
);
},

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

@ -19,16 +19,13 @@ const PASSWORD = '12345678';
const {
clearBrowserState,
click,
closeCurrentWindow,
createUser,
fillOutEmailFirstSignIn,
fillOutSignInTokenCode,
fillOutSignInUnblock,
noEmailExpected,
openPage,
openVerificationLinkInNewTab,
respondToWebChannelMessage,
switchToWindow,
testElementExists,
testEmailExpected,
testIsBrowserNotified,
@ -45,7 +42,7 @@ const setupTest = thenify(function(options = {}) {
? selectors.SIGNIN_UNBLOCK.HEADER
: options.preVerified
? selectors.SIGNIN_TOKEN_CODE.HEADER
: selectors.CONFIRM_SIGNUP.HEADER;
: selectors.CONFIRM_SIGNUP_CODE.HEADER;
const query = options.query || {
forceUA: uaStrings['desktop_firefox_58'],
@ -162,13 +159,9 @@ registerSuite('Firefox Desktop Sync v3 signin', {
// the verification reminder emails. 5 attempts occur in 5 seconds,
// the first verification reminder is set after 10 seconds.
.then(noEmailExpected(email, 2, { maxAttempts: 5 }))
.then(openVerificationLinkInNewTab(email, 1))
.then(fillOutSignInTokenCode(email, 1))
.then(testEmailExpected(email, 2))
.then(switchToWindow(1))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(closeCurrentWindow())
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(testIsBrowserNotified('fxaccounts:login'))
);