Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
fix(codes): redirect user to replace recovery codes when they are low
Browse files Browse the repository at this point in the history
  • Loading branch information
vbudhram committed Aug 24, 2018
1 parent e882f50 commit c3cf28e
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 10 deletions.
4 changes: 4 additions & 0 deletions app/scripts/views/mixins/modal-settings-panel-mixin.js
Expand Up @@ -57,6 +57,10 @@ define(function (require, exports, module) {
this._returnToClients();
break;
case 'settings/two_step_authentication/recovery_codes':
if (this.model.get('previousViewName') === 'sign_in_recovery_code') {
const account = this.getSignedInAccount();
return this.invokeBrokerMethod('afterCompleteSignInWithCode', account);
}
this._returnToTwoFactorAuthentication();
break;
case 'settings/account_recovery/recovery_key' :
Expand Down
7 changes: 5 additions & 2 deletions app/scripts/views/sign_in_recovery_code.js
Expand Up @@ -13,6 +13,7 @@ const CODE_INPUT_SELECTOR = 'input.recovery-code';
const View = FormView.extend({
className: 'sign-in-recovery-code',
template: Template,
viewName: 'sign_in_recovery_code',

beforeRender() {
const account = this.getSignedInAccount();
Expand All @@ -27,8 +28,10 @@ const View = FormView.extend({

return account.consumeRecoveryCode(code)
.then((result) => {
if (result.remaining < 1) {
// TODO Lets handle automatically generating recovery codes separately
if (result.remaining < 2) {
return this.navigate('/settings/two_step_authentication/recovery_codes', {
previousViewName: this.viewName
});
}

this.logViewEvent('success');
Expand Down
16 changes: 16 additions & 0 deletions app/tests/spec/views/sign_in_recovery_code.js
Expand Up @@ -158,6 +158,22 @@ describe('views/sign_in_recovery_code', () => {
});
});

describe('success with no recovery codes left', () => {
beforeEach(() => {
sinon.stub(account, 'consumeRecoveryCode').callsFake(() => Promise.resolve({remaining: 0}));
sinon.spy(view, 'navigate');
view.$('.recovery-code').val(RECOVERY_CODE);
return view.submit();
});

it('navigates to recovery code modal', () => {
assert.isTrue(account.consumeRecoveryCode.calledWith(RECOVERY_CODE), 'verify with correct code');
const args = view.navigate.args[0];
assert.equal(args[0], '/settings/two_step_authentication/recovery_codes', 'correct viewname');
assert.equal(args[1].previousViewName, 'sign_in_recovery_code', 'correct previous name');
});
});

describe('invalid recovery code', () => {
beforeEach(() => {
sinon.stub(account, 'consumeRecoveryCode').callsFake(() => Promise.reject(AuthErrors.toError('INVALID_RECOVERY_CODE')));
Expand Down
4 changes: 3 additions & 1 deletion tests/functional/lib/selectors.js
Expand Up @@ -214,6 +214,7 @@ module.exports = {
INPUT: '.recovery-code',
LINK: '#use-recovery-code-link',
MODAL: '#recovery-codes',
SECOND_CODE: '.recovery-code:nth-child(2)',
SUBMIT: 'button[type="submit"]',
},
SIGNIN_TOKEN_CODE: {
Expand Down Expand Up @@ -314,7 +315,8 @@ module.exports = {
MENU_BUTTON: '#totp-section .settings-unit-toggle',
QR_CODE: '.qr-image',
RECOVERY_CODES_DESCRIPTION: '#recovery-codes .description',
RECOVERY_CODES_DONE: '#recovery-codes .description',
RECOVERY_CODES_DONE: '#recovery-codes .two-step-authentication-done',
RECOVERY_CODES_REPLACE: '#recovery-codes .replace-codes-link',
SHOW_CODE_LINK: '.show-code-link',
STATUS_DISABLED: '.two-step-authentication .disabled',
STATUS_ENABLED: '.two-step-authentication .enabled',
Expand Down
62 changes: 55 additions & 7 deletions tests/functional/sign_in_recovery_code.js
Expand Up @@ -17,7 +17,7 @@ const PASSWORD = 'password';
const SYNC_SIGNIN_URL = `${config.fxaContentRoot}signin?context=fx_desktop_v3&service=sync`;

let email;
let recoveryCode;
let recoveryCode, recoveryCode2;
let secret;

const {
Expand All @@ -40,7 +40,7 @@ registerSuite('recovery code', {
beforeEach: function () {
email = TestHelpers.createEmail();
const self = this;
return this.remote.then(clearBrowserState())
return this.remote.then(clearBrowserState({force: true}))
.then(openPage(SIGNUP_URL, selectors.SIGNUP.HEADER))
.then(fillOutSignUp(email, PASSWORD))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
Expand Down Expand Up @@ -71,15 +71,16 @@ registerSuite('recovery code', {
// Store a recovery code
.findByCssSelector(selectors.SIGNIN_RECOVERY_CODE.FIRST_CODE)
.getVisibleText()
.then((code) => recoveryCode = code);
.then((code) => {
recoveryCode = code;
return self.remote.findByCssSelector(selectors.SIGNIN_RECOVERY_CODE.SECOND_CODE)
.getVisibleText()
.then((code) => recoveryCode2 = code);
});
})
.end();
},

afterEach: function () {
return this.remote.then(clearBrowserState());
},

tests: {
'can sign-in with recovery code - sync': function () {
return this.remote
Expand Down Expand Up @@ -108,5 +109,52 @@ registerSuite('recovery code', {
// about:accounts will take over post-verification, no transition
.then(testIsBrowserNotified('fxaccounts:login'));
},

'can regenerate recovery code when low': function () {
return this.remote
.then(click(selectors.SIGNIN_RECOVERY_CODE.DONE_BUTTON))
.then(click(selectors.SETTINGS.SIGNOUT))
.then(openPage(SYNC_SIGNIN_URL, selectors.SIGNIN.HEADER, {
query: {}, webChannelResponses: {
'fxaccounts:can_link_account': {ok: true},
'fxaccounts:fxa_status': {capabilities: null, signedInUser: null},
}
}))

.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.TOTP_SIGNIN.HEADER))
.then(click(selectors.SIGNIN_RECOVERY_CODE.LINK))

.then(type(selectors.SIGNIN_RECOVERY_CODE.INPUT, recoveryCode))
.then(click(selectors.SIGNIN_RECOVERY_CODE.SUBMIT))

.then(testIsBrowserNotified('fxaccounts:login'))

// Next attempt to use recovery code will redirect to
// page where user can generate more recovery codes
.then(clearBrowserState({force: true}))

.then(openPage(SYNC_SIGNIN_URL, selectors.SIGNIN.HEADER, {
query: {}, webChannelResponses: {
'fxaccounts:can_link_account': {ok: true},
'fxaccounts:fxa_status': {capabilities: null, signedInUser: null},
}
}))

.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.TOTP_SIGNIN.HEADER))
.then(click(selectors.SIGNIN_RECOVERY_CODE.LINK))

.then(type(selectors.SIGNIN_RECOVERY_CODE.INPUT, recoveryCode2))
.then(click(selectors.SIGNIN_RECOVERY_CODE.SUBMIT))

.then(testElementExists(selectors.TOTP.RECOVERY_CODES_DESCRIPTION))
.then(click(selectors.TOTP.RECOVERY_CODES_REPLACE))
.then(testElementExists(selectors.SIGNIN_RECOVERY_CODE.FIRST_CODE))

// After dismissing modal, the login message is sent
.then(click(selectors.TOTP.RECOVERY_CODES_DONE))
.then(testIsBrowserNotified('fxaccounts:login'));
},
}
});

0 comments on commit c3cf28e

Please sign in to comment.