Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: allow retries for MFA form errors #27574

Merged
merged 3 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)/);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea wrapping this in a conditional!

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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test coverage for the default timeout that i added

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
Loading