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() {