UI: Small design updates following TOTP self-enroll demo (#9578) (#9619)

* copy changes WIP

* update descriptions and headers to match latest designs

* add details to method info table

* update test assertion

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
Vault Automation 2025-09-25 19:42:49 -04:00 committed by GitHub
parent 34696b573d
commit fefc549e59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 175 additions and 182 deletions

View File

@ -6,7 +6,8 @@
{{! This component renders when there are either multiple constraints with self-enroll methods or a single constraint with multiple methods }}
<Mfa::SplashCard
@header="Verify your identity"
@subheader="Multi-factor authentication is enabled for your account. Choose one of the following methods to continue:"
@subheader="Multi-factor authentication is enabled for your account."
@description={{this.description}}
@renderLogo={{true}}
data-test-mfa-form
>

View File

@ -20,6 +20,12 @@ const METHOD_MAP = {
};
export default class MfaFormChooseMethod extends Component<Args> {
get description() {
return this.singleConstraint
? 'Choose one of the following methods to continue:'
: 'Select a method for each enforcement to continue. Choosing a self-enroll method will redirect you to setup your device.';
}
get nonSelfEnrollMethods() {
return this.singleConstraint?.methods.filter((m) => !m.self_enrollment_enabled);
}

View File

@ -4,8 +4,8 @@
}}
<Mfa::SplashCard
@header="Sign in to Vault"
@subheader={{this.headerText.subheader}}
@header="Verify your identity"
@subheader="Multi-factor authentication is enabled for your account."
@description={{this.description}}
@renderLogo={{true}}
data-test-mfa-form

View File

@ -24,15 +24,15 @@ interface Args {
export default class MfaFormVerify extends Component<Args> {
get description() {
const base = 'Multi-factor authentication is enabled for your account.';
if (this.args.constraints.length > 1) {
const num = this.args.constraints.length;
return base + ` ${numberToWord(num, true)} methods are required for successful authentication.`;
const num = numberToWord(this.args.constraints.length, true);
return `${num} methods are required for successful authentication.`;
}
if (this.singleConstraint?.selectedMethod?.uses_passcode) {
return base + ' Enter your authentication code to log in.';
return 'Enter your authentication code to log in.';
}
return base;
// Otherwise it's a single push notification and we do not need a description.
return '';
}
get singleConstraint() {

View File

@ -34,7 +34,7 @@ import type { MfaAuthData } from 'vault/vault/auth/mfa';
*/
export const TOTP_VALIDATION_ERROR =
'The passcode failed to validate. If you entered the correct passcode, contact your administrator.';
'The passcode failed to validate. If you entered the correct passcode, please wait for a new code and try again. If the problem persists contact your administrator.';
interface Args {
authData: MfaAuthData;

View File

@ -144,11 +144,13 @@ export default class MfaMethod extends Model {
@attr('number', {
label: 'Key size',
subText: 'The size in bytes of the Vault generated key.',
helperText: 'Byte size of the generated key.',
})
key_size;
@attr('number', {
label: 'QR size',
subText: 'The pixel size of the generated square QR code.',
helperText: 'Pixel size of the QR code.',
})
qr_size;
@attr('string', {
@ -163,6 +165,7 @@ export default class MfaMethod extends Model {
editType: 'radio',
possibleValues: [6, 8],
subText: 'The number digits in the generated TOTP code.',
helperText: 'TOTP code length.',
})
digits;
@attr('number', {

View File

@ -79,6 +79,8 @@
@alwaysRender={{not (is-empty-value (get this.model.method attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.model.method attr.name}}
@formatTtl={{eq attr.options.editType "ttl"}}
@helperText={{attr.options.helperText}}
/>
{{/if}}
{{/each}}

View File

@ -40,6 +40,7 @@ module('Acceptance | mfa-login', function (hooks) {
await fillIn(GENERAL.inputByAttr('password'), 'test');
await click(GENERAL.submitButton);
};
const didLogin = async (assert) => {
await waitFor('[data-test-dashboard-card-header]', {
timeout: 5000,
@ -47,6 +48,7 @@ module('Acceptance | mfa-login', function (hooks) {
});
assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'Route transitions after login');
};
const validate = async (multi) => {
await fillIn(MFA_SELECTORS.passcode(0), 'test');
if (multi) {
@ -55,24 +57,25 @@ module('Acceptance | mfa-login', function (hooks) {
await click(GENERAL.button('Verify'));
};
// 7 assertions
const assertSelfEnroll = async (assert) => {
// Wait for QR code
await waitFor(MFA_SELECTORS.qrCode);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders a QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
// Click "Continue" to go onto next step and verify code
await click(GENERAL.button('Continue'));
assert.dom(GENERAL.button('Continue')).doesNotExist('"Continue" button is replaced by "Verify"');
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert.dom(MFA_SELECTORS.label).hasText('Enter your one-time code');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, '1 passcode inputs renders');
assert.dom(GENERAL.button('Verify')).exists();
};
test('it should handle single constraint with passcode method', async function (assert) {
assert.expect(5);
assert.expect(4);
await login('mfa-a');
assert.dom(GENERAL.title).hasText('Sign in to Vault');
assert
.dom(MFA_SELECTORS.description)
.includesText(
'Enter your authentication code to log in.',
'Mfa form displays with correct description'
);
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.select()).doesNotExist('Select is hidden for single method');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, 'Single passcode input renders');
await validate();
@ -81,15 +84,9 @@ module('Acceptance | mfa-login', function (hooks) {
test('it should handle single constraint with push method', async function (assert) {
assert.expect(6);
server.post('/sys/mfa/validate', async (schema, req) => {
await waitUntil(() => find(MFA_SELECTORS.description));
assert
.dom(MFA_SELECTORS.description)
.hasText(
'Multi-factor authentication is enabled for your account.',
'Mfa form displays with correct description'
);
this.server.post('/sys/mfa/validate', async (schema, req) => {
await waitUntil(() => find(MFA_SELECTORS.verifyForm));
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.label).hasText('Okta push notification', 'Correct method renders');
assert
.dom(MFA_SELECTORS.push)
@ -106,15 +103,9 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('it should handle single constraint with 2 passcode methods', async function (assert) {
assert.expect(6);
assert.expect(5);
await login('mfa-c');
assert.dom(GENERAL.title).hasText('Verify your identity');
assert
.dom(MFA_SELECTORS.subheader)
.hasText(
'Multi-factor authentication is enabled for your account. Choose one of the following methods to continue:',
'Mfa form displays with correct description'
);
assert.dom(GENERAL.button('Verify with Duo')).exists('It renders button for Duo');
assert.dom(GENERAL.button('Verify with TOTP')).exists('It renders button for TOTP');
assert.dom(MFA_SELECTORS.passcode()).doesNotExist('Passcode input hidden until selection is made');
@ -124,8 +115,9 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('it should handle single constraint with 2 push methods', async function (assert) {
assert.expect(3);
assert.expect(4);
await login('mfa-d');
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(GENERAL.button('Verify with Okta')).exists('It renders button for Okta');
assert.dom(GENERAL.button('Verify with Duo')).exists('It renders button for Duo');
await click(GENERAL.button('Verify with Okta'));
@ -145,14 +137,8 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('it should handle multiple constraints with 1 passcode method each', async function (assert) {
assert.expect(3);
assert.expect(2);
await login('mfa-f');
assert
.dom(MFA_SELECTORS.description)
.includesText(
'Two methods are required for successful authentication.',
'Mfa form displays with correct description'
);
assert.dom(MFA_SELECTORS.select()).doesNotExist('Selects do not render for single methods');
await validate(true);
await didLogin(assert);
@ -165,14 +151,8 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('it should handle multiple constraints with 1 passcode and 1 push method', async function (assert) {
assert.expect(4);
assert.expect(3);
await login('mfa-h');
assert
.dom(MFA_SELECTORS.description)
.includesText(
'Two methods are required for successful authentication.',
'Mfa form displays with correct description'
);
assert.dom(MFA_SELECTORS.select()).doesNotExist('Select is hidden for single method');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, 'Passcode input renders');
await validate();
@ -180,14 +160,8 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('it should handle multiple constraints with multiple mixed methods', async function (assert) {
assert.expect(2);
assert.expect(1);
await login('mfa-i');
assert
.dom(MFA_SELECTORS.description)
.includesText(
'Two methods are required for successful authentication.',
'Mfa form displays with correct description'
);
await this.select();
await fillIn(MFA_SELECTORS.passcode(1), 'test');
await click(GENERAL.button('Verify'));
@ -213,64 +187,32 @@ module('Acceptance | mfa-login', function (hooks) {
* Even though self-enrollment is an enterprise-only feature, these tests use Mirage so we don't need to filter them out of CE test runs
*/
test('self-enroll: single constraint with one TOTP passcode', async function (assert) {
assert.expect(8);
await login('mfa-a-self');
await waitFor(MFA_SELECTORS.qrCode);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
await click(GENERAL.button('Continue'));
assert.dom(GENERAL.button('Continue')).doesNotExist('"Continue" button is replaced by "Verify"');
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert.dom(GENERAL.button('Verify')).exists();
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, 'Single passcode input renders');
await assertSelfEnroll(assert);
await validate();
await didLogin(assert);
});
test('self-enroll: single constraint with 2 passcode methods', async function (assert) {
assert.expect(10);
await login('mfa-c-self');
// Buttons render for both Duo and TOTP, we want to click TOTP to initiate self-enrollment flow
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(GENERAL.button('Verify with Duo')).exists();
await click(GENERAL.button('Setup to verify with TOTP'));
await waitFor(MFA_SELECTORS.qrCode);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
// Click "Continue" for second setup step to verify passcode
await click(GENERAL.button('Continue'));
assert
.dom(MFA_SELECTORS.description)
.hasText('To verify your device, enter the code generated from your authenticator.');
assert.dom(GENERAL.button('Continue')).doesNotExist('"Continue" button is replaced by "Verify"');
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert.dom(GENERAL.button('Verify')).exists();
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, 'Passcode input renders');
await assertSelfEnroll(assert);
await validate();
await didLogin(assert);
});
test('self-enroll: multiple constraints with 1 passcode method each', async function (assert) {
assert.expect(13);
await login('mfa-f-self');
await waitFor(MFA_SELECTORS.qrCode);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
// Click "Continue" for second setup step to verify passcode
await click(GENERAL.button('Continue'));
assert.dom(GENERAL.button('Continue')).doesNotExist('"Continue" button is replaced by "Verify"');
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert
.dom(MFA_SELECTORS.description)
.hasText('To verify your device, enter the code generated from your authenticator.');
assert.dom(MFA_SELECTORS.label).hasText('Enter your one-time code');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, '1 passcode inputs renders');
await assertSelfEnroll(assert);
// Fill in and click "Verify" which should render passcode for second constraint
await fillIn(MFA_SELECTORS.passcode(0), 'test');
await click(GENERAL.button('Verify'));
assert
.dom(MFA_SELECTORS.description)
.hasText(
'Multi-factor authentication is enabled for your account. Two methods are required for successful authentication.'
);
assert.dom(MFA_SELECTORS.verifyBadge('TOTP passcode')).hasText('TOTP passcode');
assert.dom(GENERAL.button('Verify')).exists();
assert.dom(MFA_SELECTORS.label).hasText('Duo passcode');
@ -282,19 +224,9 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('self-enroll: multiple constraints with 1 passcode and 1 push method', async function (assert) {
assert.expect(8);
await login('mfa-h-self');
await waitFor(MFA_SELECTORS.qrCode);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
// Click "Continue" for second setup step to verify passcode
await click(GENERAL.button('Continue'));
assert.dom(GENERAL.button('Continue')).doesNotExist('"Continue" button is replaced by "Verify"');
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert
.dom(MFA_SELECTORS.description)
.hasText('To verify your device, enter the code generated from your authenticator.');
assert.dom(MFA_SELECTORS.label).hasText('Enter your one-time code');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, '1 passcode inputs renders');
await assertSelfEnroll(assert);
// Fill in and click "Verify" which should immediately trigger MFA validation because the
// second constraint is a push notification and no user input is required.
await fillIn(MFA_SELECTORS.passcode(0), 'test');
@ -303,27 +235,12 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('self-enroll: multiple constraints with multiple mixed methods', async function (assert) {
assert.expect(14);
await login('mfa-i-self');
assert.dom(GENERAL.title).hasText('Verify your identity');
assert
.dom(MFA_SELECTORS.subheader)
.hasText(
'Multi-factor authentication is enabled for your account. Choose one of the following methods to continue:'
);
assert.dom(GENERAL.button('Verify with Okta')).exists();
await click(GENERAL.button('Setup to verify with TOTP'));
await waitFor(MFA_SELECTORS.qrCode);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
// Click "Continue" to validate TOTP
await click(GENERAL.button('Continue'));
assert.dom(GENERAL.button('Continue')).doesNotExist('"Continue" button is replaced by "Verify"');
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert
.dom(MFA_SELECTORS.description)
.hasText('To verify your device, enter the code generated from your authenticator.');
assert.dom(MFA_SELECTORS.label).hasText('Enter your one-time code');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, '1 passcode inputs renders');
await assertSelfEnroll(assert);
// Fill in and click "Verify" which should render passcode for second constraint
await fillIn(MFA_SELECTORS.passcode(0), 'test');
await click(GENERAL.button('Verify'));
@ -337,6 +254,7 @@ module('Acceptance | mfa-login', function (hooks) {
});
test('self-enroll: multiple constraints, 1 with 2 methods (one that supports self-enroll), 1 with push method', async function (assert) {
assert.expect(17);
await login('mfa-z-self');
// For the constraint that supports self-enrollment, user must select it first.
assert.dom(MFA_SELECTORS.select(0)).exists();
@ -351,7 +269,7 @@ module('Acceptance | mfa-login', function (hooks) {
.hasText('TOTP passcode (supports self-enrollment)', 'TOTP is pre-selected for the first constraint');
await this.select(1, 2);
// On second selection we are redirected
assert.dom(GENERAL.title).hasText('Sign in to Vault');
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.verifyForm).exists('it renders mfa validation form');
assert.dom(MFA_SELECTORS.select(0)).isDisabled();
assert.dom(MFA_SELECTORS.verifyBadge('TOTP passcode')).exists('pending verification badge exists');
@ -373,7 +291,6 @@ module('Acceptance | mfa-login', function (hooks) {
module('error handling', function (hooks) {
hooks.beforeEach(function () {
// TODO confirm with backend what errors could be returned
this.server.post('/identity/mfa/method/totp/self-enroll', async () => {
return overrideResponse(500, JSON.stringify({ errors: ['uh oh!'] }));
});

View File

@ -12,6 +12,7 @@ import mfaConfigHandler from 'vault/mirage/handlers/mfa-config';
import { Response } from 'miragejs';
import { underscore } from '@ember/string';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { duration } from 'vault/helpers/format-duration';
module('Acceptance | mfa-method', function (hooks) {
setupApplicationTest(hooks);
@ -147,7 +148,10 @@ module('Acceptance | mfa-method', function (hooks) {
'Passcode reminder': 'use_passcode',
'Organization name': 'org_name',
}[label] || underscore(label);
const value = typeof model[key] === 'boolean' ? (model[key] ? 'Yes' : 'No') : model[key].toString();
let value = typeof model[key] === 'boolean' ? (model[key] ? 'Yes' : 'No') : model[key].toString();
if (key === 'period') {
value = duration([Number(value)]);
}
assert.dom(GENERAL.infoRowValue(label)).hasText(value, `${label} value renders`);
});
await click('.hds-breadcrumb a');

View File

@ -21,7 +21,7 @@ import { click, fillIn, waitFor } from '@ember/test-helpers';
const mfaTests = (test) => {
test('it displays mfa requirement for default paths', async function (assert) {
const loginKeys = Object.keys(this.loginData);
assert.expect(3 + loginKeys.length);
assert.expect(5 + loginKeys.length);
this.stubRequests();
await this.renderComponent();
@ -37,11 +37,9 @@ const mfaTests = (test) => {
await click(GENERAL.submitButton);
await waitFor(MFA_SELECTORS.mfaForm);
assert
.dom(MFA_SELECTORS.mfaForm)
.hasText(
'Sign in to Vault Multi-factor authentication is enabled for your account. Enter your authentication code to log in. TOTP passcode Verify Cancel'
);
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).hasText('Enter your authentication code to log in.');
await click(GENERAL.cancelButton);
assert.dom(AUTH_FORM.form).exists('clicking back returns to auth form');
assert.dom(AUTH_FORM.selectMethod).hasValue(this.authType, 'preserves method type on back');
@ -53,7 +51,7 @@ const mfaTests = (test) => {
test('it displays mfa requirement for custom paths', async function (assert) {
this.path = `${this.authType}-custom`;
const loginKeys = Object.keys(this.loginData);
assert.expect(3 + loginKeys.length);
assert.expect(5 + loginKeys.length);
this.stubRequests();
await this.renderComponent();
@ -69,11 +67,9 @@ const mfaTests = (test) => {
await click(GENERAL.submitButton);
await waitFor(MFA_SELECTORS.mfaForm);
assert
.dom(MFA_SELECTORS.mfaForm)
.hasText(
'Sign in to Vault Multi-factor authentication is enabled for your account. Enter your authentication code to log in. TOTP passcode Verify Cancel'
);
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).hasText('Enter your authentication code to log in.');
await click(GENERAL.cancelButton);
assert.dom(AUTH_FORM.form).exists('clicking back returns to auth form');
assert.dom(AUTH_FORM.selectMethod).hasValue(this.authType, 'preserves method type on back');

View File

@ -61,14 +61,25 @@ module('Integration | Component | mfa-form', function (hooks) {
});
await this.renderComponent();
assert
.dom(MFA_SELECTORS.description)
.hasText(
'Multi-factor authentication is enabled for your account. Enter your authentication code to log in.'
);
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).hasText('Enter your authentication code to log in.');
});
test('it renders correct text for multiple methods', async function (assert) {
test('it renders correct text for single push notification', async function (assert) {
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
this.mfaAuthData.mfaRequirement = this.authService.parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa_1: { any: [oktaConstraint] } },
});
await this.renderComponent();
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).doesNotExist();
});
test('it renders correct text for a single constraint with multiple methods', async function (assert) {
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
const duoConstraint = this.server.create('mfa-method', { type: 'duo' });
this.mfaAuthData.mfaRequirement = this.authService.parseMfaResponse({
@ -77,11 +88,9 @@ module('Integration | Component | mfa-form', function (hooks) {
});
await this.renderComponent();
assert
.dom(MFA_SELECTORS.subheader)
.hasText(
'Multi-factor authentication is enabled for your account. Choose one of the following methods to continue:'
);
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).hasText('Choose one of the following methods to continue:');
});
test('it renders correct text for multiple constraints', async function (assert) {
@ -93,11 +102,8 @@ module('Integration | Component | mfa-form', function (hooks) {
});
await this.renderComponent();
assert
.dom(MFA_SELECTORS.description)
.hasText(
'Multi-factor authentication is enabled for your account. Two methods are required for successful authentication.'
);
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).hasText('Two methods are required for successful authentication.');
});
test('it should render a submit button', async function (assert) {
@ -296,6 +302,35 @@ module('Integration | Component | mfa-form', function (hooks) {
});
});
test('it should render qr code and copy button', async function (assert) {
const clipboardSpy = sinon.stub(navigator.clipboard, 'writeText').resolves();
const totpConstraint = this.server.create('mfa-method', {
type: 'totp',
self_enrollment_enabled: true,
});
const mfaRequirement = this.authService.parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa: { any: [totpConstraint] } },
});
this.setMfaAuthData(mfaRequirement);
await this.renderComponent();
await waitFor(MFA_SELECTORS.qrCode);
assert
.dom(MFA_SELECTORS.mfaForm)
.hasText(
'Set up MFA TOTP to continue Your organization has enforced MFA TOTP to protect your accounts. Set up to continue. Scan the QR code to continue Scan the QR code with your authenticator app. If you currently do not have a device on hand, you can copy the MFA secret below and enter it manually. Or Copy TOTP setup URL For your security, this code is only shown once. Please scan or copy the setup URL into your authenticator app now. Continue Cancel',
'it renders self-enrollment text'
);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders qr code');
assert.dom(GENERAL.cancelButton).exists();
assert.dom(MFA_SELECTORS.verifyForm).doesNotExist('it does not render input field for TOTP code');
assert.dom(GENERAL.button('Verify')).doesNotExist('it does not render Validate button');
await click(GENERAL.copyButton);
assert.strictEqual(clipboardSpy.firstCall.args[0], QR_CODE_URL, 'copy value is qr code URL');
// Restore original clipboard
clipboardSpy.restore(); // cleanup
});
test('it makes request to self-enroll endpoint when self_enrollment_enabled is true', async function (assert) {
assert.expect(3);
const request_id = crypto.randomUUID();
@ -351,7 +386,7 @@ module('Integration | Component | mfa-form', function (hooks) {
assert.dom(GENERAL.button('Continue')).exists();
assert.dom(GENERAL.cancelButton).exists();
// Go on to next step
// Go on to next step which is input code to verify device
await click(GENERAL.button('Continue'));
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
assert
@ -378,11 +413,8 @@ module('Integration | Component | mfa-form', function (hooks) {
});
await this.renderComponent();
assert.dom(GENERAL.title).hasText('Verify your identity');
assert
.dom(MFA_SELECTORS.subheader)
.hasText(
'Multi-factor authentication is enabled for your account. Choose one of the following methods to continue:'
);
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).hasText('Choose one of the following methods to continue:');
assert.dom(MFA_SELECTORS.subtitle).doesNotExist();
assert.dom(GENERAL.button('Verify')).doesNotExist();
assert.dom(GENERAL.cancelButton).exists();
@ -431,11 +463,8 @@ module('Integration | Component | mfa-form', function (hooks) {
});
await this.renderComponent();
assert.dom(GENERAL.title).hasText('Verify your identity');
assert
.dom(MFA_SELECTORS.subheader)
.hasText(
'Multi-factor authentication is enabled for your account. Choose one of the following methods to continue:'
);
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert.dom(MFA_SELECTORS.description).hasText('Choose one of the following methods to continue:');
assert.dom(MFA_SELECTORS.subtitle).doesNotExist();
assert.dom(GENERAL.button('Verify')).doesNotExist();
assert.dom(GENERAL.cancelButton).exists();
@ -472,33 +501,68 @@ module('Integration | Component | mfa-form', function (hooks) {
assert.dom(GENERAL.cancelButton).exists('it renders "Cancel" after self-enroll workflow');
});
test('it should render qr code and copy button', async function (assert) {
const clipboardSpy = sinon.stub(navigator.clipboard, 'writeText').resolves();
test('it renders correct text for multiple constraints (1 passcode 1 push)', async function (assert) {
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
const totpConstraint = this.server.create('mfa-method', {
type: 'totp',
self_enrollment_enabled: true,
});
const mfaRequirement = this.authService.parseMfaResponse({
this.mfaAuthData.mfaRequirement = this.authService.parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa: { any: [totpConstraint] } },
mfa_constraints: { test_mfa_1: { any: [oktaConstraint] }, test_mfa_2: { any: [totpConstraint] } },
});
this.setMfaAuthData(mfaRequirement);
await this.renderComponent();
await waitFor(MFA_SELECTORS.qrCode);
assert
.dom(MFA_SELECTORS.mfaForm)
.hasText(
'Set up MFA TOTP to continue Your organization has enforced MFA TOTP to protect your accounts. Set up to continue. Scan the QR code to continue Scan the QR code with your authenticator app. If you currently do not have a device on hand, you can copy the MFA secret below and enter it manually. Or Copy TOTP setup URL For your security, this code is only shown once. Please scan or copy the setup URL into your authenticator app now. Continue Cancel',
'it renders self-enrollment text'
);
assert.dom(MFA_SELECTORS.qrCode).exists('it renders qr code');
assert.dom(MFA_SELECTORS.qrCode).exists('it renders QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
assert.dom(GENERAL.button('Verify')).doesNotExist();
assert.dom(GENERAL.cancelButton).exists();
assert.dom(MFA_SELECTORS.verifyForm).doesNotExist('it does not render input field for TOTP code');
assert.dom(GENERAL.button('Verify')).doesNotExist('it does not render Validate button');
await click(GENERAL.copyButton);
assert.strictEqual(clipboardSpy.firstCall.args[0], QR_CODE_URL, 'copy value is qr code URL');
// Restore original clipboard
clipboardSpy.restore(); // cleanup
// Click "Continue" for second setup step to verify passcode
await click(GENERAL.button('Continue'));
assert.dom(GENERAL.button('Continue')).doesNotExist('"Continue" button is replaced by "Verify"');
assert.dom(MFA_SELECTORS.qrCode).doesNotExist('Clicking "Continue" removes QR code');
assert.dom(GENERAL.title).hasText('Set up MFA TOTP to continue');
assert
.dom(MFA_SELECTORS.description)
.hasText('To verify your device, enter the code generated from your authenticator.');
await click(GENERAL.button('Verify'));
// Final view which manages the loading state while validate task runs
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert
.dom(MFA_SELECTORS.description)
.hasText('Two methods are required for successful authentication.');
assert.dom(MFA_SELECTORS.label).hasText('Okta push notification');
assert.dom(MFA_SELECTORS.push).hasText('Check device for push notification');
assert.dom(MFA_SELECTORS.verifyBadge('TOTP passcode')).hasText('TOTP passcode');
});
// Unlikely in the real-world, but test coverage just in case
test('it renders correct text for multiple constraints, one with multiple methods including self-enroll', async function (assert) {
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
const duoConstraint = this.server.create('mfa-method', { type: 'duo' });
const totpConstraint = this.server.create('mfa-method', {
type: 'totp',
self_enrollment_enabled: true,
});
this.mfaAuthData.mfaRequirement = this.authService.parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: {
test_mfa_1: { any: [oktaConstraint, totpConstraint] },
test_mfa_2: { any: [totpConstraint, duoConstraint] },
},
});
await this.renderComponent();
assert.dom(MFA_SELECTORS.subheader).hasText('Multi-factor authentication is enabled for your account.');
assert
.dom(MFA_SELECTORS.description)
.hasText(
'Select a method for each enforcement to continue. Choosing a self-enroll method will redirect you to setup your device.'
);
});
});
});