mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-16 03:27:01 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
216 lines
8.6 KiB
JavaScript
216 lines
8.6 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import { setupRenderingTest } from 'ember-qunit';
|
|
import { render } from '@ember/test-helpers';
|
|
import { hbs } from 'ember-cli-htmlbars';
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
import { fillIn, click, waitUntil } from '@ember/test-helpers';
|
|
import { _cancelTimers as cancelTimers, later } from '@ember/runloop';
|
|
import { TOTP_VALIDATION_ERROR } from 'vault/components/mfa/mfa-form';
|
|
|
|
module('Integration | Component | mfa-form', function (hooks) {
|
|
setupRenderingTest(hooks);
|
|
setupMirage(hooks);
|
|
|
|
hooks.beforeEach(function () {
|
|
this.clusterId = '123456';
|
|
this.mfaAuthData = {
|
|
backend: 'userpass',
|
|
data: { username: 'foo', password: 'bar' },
|
|
};
|
|
this.authService = this.owner.lookup('service:auth');
|
|
// setup basic totp mfa_requirement
|
|
// override in tests that require different scenarios
|
|
this.totpConstraint = this.server.create('mfa-method', { type: 'totp' });
|
|
const { mfa_requirement } = this.authService._parseMfaResponse({
|
|
mfa_request_id: 'test-mfa-id',
|
|
mfa_constraints: { test_mfa: { any: [this.totpConstraint] } },
|
|
});
|
|
this.mfaAuthData.mfa_requirement = mfa_requirement;
|
|
});
|
|
|
|
test('it should render correct descriptions', async function (assert) {
|
|
const totpConstraint = this.server.create('mfa-method', { type: 'totp' });
|
|
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
|
|
const duoConstraint = this.server.create('mfa-method', { type: 'duo' });
|
|
|
|
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
|
|
mfa_request_id: 'test-mfa-id',
|
|
mfa_constraints: { test_mfa_1: { any: [totpConstraint] } },
|
|
}).mfa_requirement;
|
|
|
|
await render(
|
|
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
|
|
);
|
|
assert
|
|
.dom('[data-test-mfa-description]')
|
|
.includesText(
|
|
'Enter your authentication code to log in.',
|
|
'Correct description renders for single passcode'
|
|
);
|
|
|
|
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
|
|
mfa_request_id: 'test-mfa-id',
|
|
mfa_constraints: { test_mfa_1: { any: [duoConstraint, oktaConstraint] } },
|
|
}).mfa_requirement;
|
|
|
|
await render(
|
|
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
|
|
);
|
|
assert
|
|
.dom('[data-test-mfa-description]')
|
|
.includesText(
|
|
'Select the MFA method you wish to use.',
|
|
'Correct description renders for multiple methods'
|
|
);
|
|
|
|
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
|
|
mfa_request_id: 'test-mfa-id',
|
|
mfa_constraints: { test_mfa_1: { any: [oktaConstraint] }, test_mfa_2: { any: [duoConstraint] } },
|
|
}).mfa_requirement;
|
|
|
|
await render(
|
|
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
|
|
);
|
|
assert
|
|
.dom('[data-test-mfa-description]')
|
|
.includesText(
|
|
'Two methods are required for successful authentication.',
|
|
'Correct description renders for multiple constraints'
|
|
);
|
|
});
|
|
|
|
test('it should render method selects and passcode inputs', async function (assert) {
|
|
assert.expect(2);
|
|
const duoConstraint = this.server.create('mfa-method', { type: 'duo', uses_passcode: true });
|
|
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
|
|
const pingidConstraint = this.server.create('mfa-method', { type: 'pingid' });
|
|
const { mfa_requirement } = this.authService._parseMfaResponse({
|
|
mfa_request_id: 'test-mfa-id',
|
|
mfa_constraints: {
|
|
test_mfa_1: {
|
|
any: [pingidConstraint, oktaConstraint],
|
|
},
|
|
test_mfa_2: {
|
|
any: [duoConstraint],
|
|
},
|
|
},
|
|
});
|
|
this.mfaAuthData.mfa_requirement = mfa_requirement;
|
|
|
|
this.server.post('/sys/mfa/validate', (schema, req) => {
|
|
const json = JSON.parse(req.requestBody);
|
|
const payload = {
|
|
mfa_request_id: 'test-mfa-id',
|
|
mfa_payload: { [oktaConstraint.id]: [], [duoConstraint.id]: ['passcode=test-code'] },
|
|
};
|
|
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
|
|
return {};
|
|
});
|
|
|
|
this.owner.lookup('service:auth').reopen({
|
|
// override to avoid authSuccess method since it expects an auth payload
|
|
async totpValidate({ mfa_requirement }) {
|
|
await this.clusterAdapter().mfaValidate(mfa_requirement);
|
|
return 'test response';
|
|
},
|
|
});
|
|
|
|
this.onSuccess = (resp) =>
|
|
assert.strictEqual(resp, 'test response', 'Response is returned in onSuccess callback');
|
|
|
|
await render(
|
|
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onSuccess={{this.onSuccess}} />`
|
|
);
|
|
await fillIn('[data-test-mfa-select="0"] select', oktaConstraint.id);
|
|
await fillIn('[data-test-mfa-passcode="1"]', 'test-code');
|
|
await click('[data-test-mfa-validate]');
|
|
});
|
|
|
|
test('it should validate mfa requirement', async function (assert) {
|
|
assert.expect(5);
|
|
this.server.post('/sys/mfa/validate', (schema, req) => {
|
|
const json = JSON.parse(req.requestBody);
|
|
const payload = {
|
|
mfa_request_id: 'test-mfa-id',
|
|
mfa_payload: { [this.totpConstraint.id]: ['test-code'] },
|
|
};
|
|
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
|
|
return {};
|
|
});
|
|
|
|
const expectedAuthData = { clusterId: this.clusterId, ...this.mfaAuthData };
|
|
this.owner.lookup('service:auth').reopen({
|
|
// override to avoid authSuccess method since it expects an auth payload
|
|
async totpValidate(authData) {
|
|
await waitUntil(() =>
|
|
assert.dom('[data-test-mfa-validate]').hasClass('is-loading', 'Loading class applied to button')
|
|
);
|
|
assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled while loading');
|
|
assert.deepEqual(authData, expectedAuthData, 'Mfa auth data passed to validate method');
|
|
await this.clusterAdapter().mfaValidate(authData.mfa_requirement);
|
|
return 'test response';
|
|
},
|
|
});
|
|
|
|
this.onSuccess = (resp) =>
|
|
assert.strictEqual(resp, 'test response', 'Response is returned in onSuccess callback');
|
|
|
|
await render(
|
|
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onSuccess={{this.onSuccess}} />`
|
|
);
|
|
|
|
await fillIn('[data-test-mfa-passcode]', 'test-code');
|
|
await click('[data-test-mfa-validate]');
|
|
});
|
|
|
|
// TODO JLR: It doesn't appear that cancelTimers is working and tests wait for the full countdown
|
|
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',
|
|
limit:
|
|
'maximum TOTP validation attempts 4 exceeded the allowed attempts 3. Please try again in 15 seconds',
|
|
};
|
|
const codes = ['used', 'limit'];
|
|
for (const code of codes) {
|
|
this.owner.lookup('service:auth').reopen({
|
|
totpValidate() {
|
|
throw { errors: [messages[code]] };
|
|
},
|
|
});
|
|
await render(hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);
|
|
|
|
await fillIn('[data-test-mfa-passcode]', code);
|
|
later(() => cancelTimers(), 50);
|
|
await click('[data-test-mfa-validate]');
|
|
const expectedTime = code === 'used' ? '45' : '15';
|
|
assert
|
|
.dom('[data-test-mfa-countdown]')
|
|
.includesText(expectedTime, '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() {
|
|
throw { errors: ['failed to validate'] };
|
|
},
|
|
});
|
|
await render(hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);
|
|
|
|
await fillIn('[data-test-mfa-passcode]', 'test-code');
|
|
later(() => cancelTimers(), 50);
|
|
await click('[data-test-mfa-validate]');
|
|
assert
|
|
.dom('[data-test-message-error]')
|
|
.includesText(TOTP_VALIDATION_ERROR, 'Generic error message renders for passcode validation error');
|
|
});
|
|
});
|