Skip to content

Commit

Permalink
UI: allow retries for MFA form errors (#27574)
Browse files Browse the repository at this point in the history
* mfa-form: fix regex matching so error msg displays

* changelog

* chore: add comments
  • Loading branch information
Noelle Daley authored Jun 25, 2024
1 parent ad5ca3e commit aa828f1
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 8 deletions.
3 changes: 3 additions & 0 deletions changelog/27574.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Display an error and force a timeout when TOTP passcode is incorrect
```
14 changes: 12 additions & 2 deletions ui/app/components/mfa/mfa-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 28 additions & 6 deletions ui/tests/integration/components/mfa-form-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -188,25 +189,46 @@ module('Integration | Component | mfa-form', function (hooks) {
throw { errors: [messages[code]] };
},
});
const expectedTime = code === 'used' ? 45 : 15;

await render(hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);

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`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);

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

0 comments on commit aa828f1

Please sign in to comment.