diff --git a/changelog/27574.txt b/changelog/27574.txt new file mode 100644 index 000000000000..8c1f888242c8 --- /dev/null +++ b/changelog/27574.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Display an error and force a timeout when TOTP passcode is incorrect +``` \ No newline at end of file diff --git a/ui/app/components/mfa/mfa-form.js b/ui/app/components/mfa/mfa-form.js index 484d13905f9a..c81ff42ff98d 100644 --- a/ui/app/components/mfa/mfa-form.js +++ b/ui/app/components/mfa/mfa-form.js @@ -102,10 +102,20 @@ export default class MfaForm extends Component { } } - @task *newCodeDelay(message) { + @task *newCodeDelay(errorMessage) { + let delay; + // parse validity period from error string to initialize countdown - this.countdown = parseInt(message.match(/(\d\w seconds)/)[0].split(' ')[0]); + const delayRegExMatches = errorMessage.match(/(\d+\w seconds)/); + if (delayRegExMatches && delayRegExMatches.length) { + delay = delayRegExMatches[0].split(' ')[0]; + } else { + // default to 30 seconds if error message doesn't specify one + delay = 30; + } + this.countdown = parseInt(delay); + // skip countdown in testing environment if (Ember.testing) return; while (this.countdown > 0) { diff --git a/ui/tests/integration/components/mfa-form-test.js b/ui/tests/integration/components/mfa-form-test.js index 128b5b38519d..cc32103fd7c3 100644 --- a/ui/tests/integration/components/mfa-form-test.js +++ b/ui/tests/integration/components/mfa-form-test.js @@ -177,9 +177,10 @@ module('Integration | Component | mfa-form', function (hooks) { test('it should show countdown on passcode already used and rate limit errors', async function (assert) { const messages = { - used: 'code already used; new code is available in 45 seconds', + used: 'code already used; new code is available in 30 seconds', + // note: the backend returns a duplicate "s" in "30s seconds" in the limit message below. we have intentionally left it as is to ensure our regex for parsing the delay time can handle it limit: - 'maximum TOTP validation attempts 4 exceeded the allowed attempts 3. Please try again in 15 seconds', + 'maximum TOTP validation attempts 4 exceeded the allowed attempts 3. Please try again in 30s seconds', }; const codes = ['used', 'limit']; for (const code of codes) { @@ -188,25 +189,46 @@ module('Integration | Component | mfa-form', function (hooks) { throw { errors: [messages[code]] }; }, }); - const expectedTime = code === 'used' ? 45 : 15; await render(hbs``); - await fillIn('[data-test-mfa-passcode]', code); - + await fillIn('[data-test-mfa-passcode]', 'foo'); await click('[data-test-mfa-validate]'); await waitFor('[data-test-mfa-countdown]'); assert .dom('[data-test-mfa-countdown]') - .includesText(expectedTime, 'countdown renders with correct initial value from error response'); + .includesText('30', 'countdown renders with correct initial value from error response'); assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled during countdown'); assert.dom('[data-test-mfa-passcode]').isDisabled('Input is disabled during countdown'); assert.dom('[data-test-inline-error-message]').exists('Alert message renders'); } }); + test('it defaults countdown to 30 seconds if error message does not indicate when user can try again ', async function (assert) { + this.owner.lookup('service:auth').reopen({ + totpValidate() { + throw { + errors: ['maximum TOTP validation attempts 4 exceeded the allowed attempts 3. Beep-boop.'], + }; + }, + }); + await render(hbs``); + + await fillIn('[data-test-mfa-passcode]', 'foo'); + await click('[data-test-mfa-validate]'); + + await waitFor('[data-test-mfa-countdown]'); + + assert + .dom('[data-test-mfa-countdown]') + .includesText('30', 'countdown renders with correct initial value from error response'); + assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled during countdown'); + assert.dom('[data-test-mfa-passcode]').isDisabled('Input is disabled during countdown'); + assert.dom('[data-test-inline-error-message]').exists('Alert message renders'); + }); + test('it should show error message for passcode invalid error', async function (assert) { this.owner.lookup('service:auth').reopen({ totpValidate() {