+ {{#if @oidcProviderQueryParam}}
+ Once you log in, you will be redirected back to your application. If you require login credentials, contact your
+ administrator.
+ {{else}}
+ Contact your administrator for login credentials.
+ {{/if}}
+
+
+
+
{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/auth/page.js b/ui/app/components/auth/page.js
index 83fb397116..85dc29c207 100644
--- a/ui/app/components/auth/page.js
+++ b/ui/app/components/auth/page.js
@@ -4,10 +4,7 @@
*/
import Component from '@glimmer/component';
-import Ember from 'ember';
import { service } from '@ember/service';
-import { task, timeout } from 'ember-concurrency';
-import { waitFor } from '@ember/test-waiters';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
@@ -16,93 +13,60 @@ import { action } from '@ember/object';
* The Auth::Page wraps OktaNumberChallenge and AuthForm to manage the login flow and is responsible for calling the authenticate method
*
* @example
- *
+ *
*
- * @param {string} wrappedToken - Query param value of a wrapped token that can be used to login when added directly to the URL via the "wrapped_token" query param
- * @param {object} cluster - The route model which is the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
- * @param {string} namespace- Namespace query param, passed to AuthForm and set by typing in namespace input or URL
- * @param {string} selectedAuth - The auth method selected in the dropdown, passed to auth service's authenticate method
- * @param {function} onSuccess - Callback that fires the "onAuthResponse" action in the auth controller and handles transitioning after success
- */
+ * @param {string} authMethodQueryParam - auth method type to login with, updated by selecting an auth method from the dropdown
+ * @param {string} namespaceQueryParam - namespace to login with, updated by typing in to the namespace input
+ * @param {string} oidcProviderQueryParam - oidc provider query param, set in url as "?o=someprovider"
+ * @param {function} onAuthSuccess - callback task in controller that receives the auth response (after MFA, if enabled) when login is successful
+ * @param {function} onNamespaceUpdate - callback task that passes user input to the controller to update the login namespace in the url query params
+ * @param {string} wrappedToken - passed down to the AuthForm component and can be used to login if added directly to the URL via the "wrapped_token" query param
+ * */
-export default class AuthPageComponent extends Component {
- @service auth;
+export default class AuthPage extends Component {
+ @service flags;
- @tracked authError = null;
- @tracked oktaNumberChallengeAnswer = '';
- @tracked waitingForOktaNumberChallenge = false;
+ @tracked mfaErrors;
+ @tracked mfaAuthData;
- @action
- performAuth(backendType, data) {
- this.authenticate.unlinked().perform(backendType, data);
+ get namespaceInput() {
+ const namespaceQP = this.args.namespaceQueryParam;
+ if (this.flags.hvdManagedNamespaceRoot) {
+ // When managed, the user isn't allowed to edit the prefix `admin/` for their nested namespace
+ const split = namespaceQP.split('/');
+ if (split.length > 1) {
+ split.shift();
+ return `/${split.join('/')}`;
+ }
+ return '';
+ }
+ return namespaceQP;
}
- @task
- @waitFor
- *delayAuthMessageReminder() {
- if (Ember.testing) {
- yield timeout(0);
+ @action
+ handleNamespaceUpdate(event) {
+ this.args.onNamespaceUpdate(event.target.value);
+ }
+
+ @action
+ onAuthResponse(authResponse, backend, data) {
+ const { mfa_requirement } = authResponse;
+ // if an mfa requirement exists further action is required
+ if (mfa_requirement) {
+ this.mfaAuthData = { mfa_requirement, backend, data };
} else {
- yield timeout(5000);
+ this.args.onAuthSuccess(authResponse);
}
}
- @task
- @waitFor
- *authenticate(backendType, data) {
- const {
- selectedAuth,
- cluster: { id: clusterId },
- } = this.args;
- try {
- if (backendType === 'okta') {
- this.pollForOktaNumberChallenge.perform(data.nonce, data.path);
- } else {
- this.delayAuthMessageReminder.perform();
- }
- const authResponse = yield this.auth.authenticate({
- clusterId,
- backend: backendType,
- data,
- selectedAuth,
- });
-
- this.args.onSuccess(authResponse, backendType, data);
- } catch (e) {
- if (!this.auth.mfaError) {
- this.authError = `Authentication failed: ${this.auth.handleError(e)}`;
- }
- }
- }
-
- @task
- @waitFor
- *pollForOktaNumberChallenge(nonce, mount) {
- // yield for 1s to wait to see if there is a login error before polling
- yield timeout(1000);
- if (this.authError) return;
-
- this.waitingForOktaNumberChallenge = true;
- // keep polling /auth/okta/verify/:nonce API every 1s until response returns with correct_number
- let response = null;
- while (response === null) {
- // disable polling for tests otherwise promises reject and acceptance tests fail
- if (Ember.testing) return;
-
- yield timeout(1000);
- response = yield this.auth.getOktaNumberChallengeAnswer(nonce, mount);
- }
- // display correct number so user can select on personal MFA device
- this.oktaNumberChallengeAnswer = response;
- }
-
@action
- onCancel() {
- // reset variables and stop polling tasks if canceling login
- this.authError = null;
- this.oktaNumberChallengeAnswer = null;
- this.waitingForOktaNumberChallenge = false;
- this.authenticate.cancelAll();
- this.pollForOktaNumberChallenge.cancelAll();
+ onMfaSuccess(authResponse) {
+ this.args.onAuthSuccess(authResponse);
+ }
+
+ @action
+ onMfaErrorDismiss() {
+ this.mfaAuthData = null;
+ this.mfaErrors = null;
}
}
diff --git a/ui/app/controllers/vault/cluster/auth.js b/ui/app/controllers/vault/cluster/auth.js
index 66a798cac0..b68f22285f 100644
--- a/ui/app/controllers/vault/cluster/auth.js
+++ b/ui/app/controllers/vault/cluster/auth.js
@@ -26,20 +26,6 @@ export default Controller.extend({
authMethod: '',
oidcProvider: '',
- get namespaceInput() {
- const namespaceQP = this.clusterController.namespaceQueryParam;
- if (this.hvdManagedNamespaceRoot) {
- // When managed, the user isn't allowed to edit the prefix `admin/` for their nested namespace
- const split = namespaceQP.split('/');
- if (split.length > 1) {
- split.shift();
- return `/${split.join('/')}`;
- }
- return '';
- }
- return namespaceQP;
- },
-
fullNamespaceFromInput(value) {
const strippedNs = sanitizePath(value);
if (this.hvdManagedNamespaceRoot) {
@@ -57,48 +43,29 @@ export default Controller.extend({
this.set('namespaceQueryParam', ns);
}).restartable(),
- authSuccess({ isRoot, namespace }) {
- let transition;
- this.version.fetchVersion();
- if (this.redirectTo) {
- // here we don't need the namespace because it will be encoded in redirectTo
- transition = this.router.transitionTo(this.redirectTo);
- // reset the value on the controller because it's bound here
- this.set('redirectTo', '');
- } else {
- transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } });
- }
- transition.followRedirects().then(() => {
- if (this.version.isEnterprise) {
- this.customMessages.fetchMessages(namespace);
- }
-
- if (isRoot) {
- this.auth.set('isRootToken', true);
- this.flashMessages.warning(
- 'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.'
- );
- }
- });
- },
-
actions: {
- onAuthResponse(authResponse, backend, data) {
- const { mfa_requirement } = authResponse;
- // if an mfa requirement exists further action is required
- if (mfa_requirement) {
- this.set('mfaAuthData', { mfa_requirement, backend, data });
+ authSuccess({ isRoot, namespace }) {
+ let transition;
+ this.version.fetchVersion();
+ if (this.redirectTo) {
+ // here we don't need the namespace because it will be encoded in redirectTo
+ transition = this.router.transitionTo(this.redirectTo);
+ // reset the value on the controller because it's bound here
+ this.set('redirectTo', '');
} else {
- this.authSuccess(authResponse);
+ transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } });
}
- },
- onMfaSuccess(authResponse) {
- this.authSuccess(authResponse);
- },
- onMfaErrorDismiss() {
- this.setProperties({
- mfaAuthData: null,
- mfaErrors: null,
+ transition.followRedirects().then(() => {
+ if (this.version.isEnterprise) {
+ this.customMessages.fetchMessages(namespace);
+ }
+
+ if (isRoot) {
+ this.auth.set('isRootToken', true);
+ this.flashMessages.warning(
+ 'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.'
+ );
+ }
});
},
},
diff --git a/ui/app/services/auth.js b/ui/app/services/auth.js
index cc369c819f..c070a7794c 100644
--- a/ui/app/services/auth.js
+++ b/ui/app/services/auth.js
@@ -371,6 +371,7 @@ export default Service.extend({
shouldRenew() {
const now = this.now();
const lastFetch = this.lastFetch;
+ // renewAfterEpoch is a unix timestamp of login time + half of ttl
const renewTime = this.renewAfterEpoch;
if (!this.currentTokenName || this.tokenExpired || this.allowExpiration || !renewTime) {
return false;
diff --git a/ui/app/templates/components/mfa/mfa-form.hbs b/ui/app/templates/components/mfa/mfa-form.hbs
index c1f1b1d911..34808f8fa5 100644
--- a/ui/app/templates/components/mfa/mfa-form.hbs
+++ b/ui/app/templates/components/mfa/mfa-form.hbs
@@ -57,9 +57,7 @@
{{/each}}
{{#if this.countdown}}
-
- {{#if this.oidcProvider}}
- Once you log in, you will be redirected back to your application. If you require login credentials, contact your
- administrator.
- {{else}}
- Contact your administrator for login credentials.
- {{/if}}
-
-
-
-
-{{/if}}
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ui/tests/acceptance/access/namespaces/index-test.js b/ui/tests/acceptance/access/namespaces/index-test.js
index 0a64769e26..ae361e9505 100644
--- a/ui/tests/acceptance/access/namespaces/index-test.js
+++ b/ui/tests/acceptance/access/namespaces/index-test.js
@@ -3,24 +3,23 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import { currentRouteName } from '@ember/test-helpers';
+import { currentRouteName, visit } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
-import page from 'vault/tests/pages/access/namespaces/index';
-import authPage from 'vault/tests/pages/auth';
+import { login } from 'vault/tests/helpers/auth/auth-helpers';
module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
- return authPage.login();
+ return login();
});
test('it navigates to namespaces page', async function (assert) {
assert.expect(1);
- await page.visit();
+ await visit('/vault/access/namespaces');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.namespaces.index',
@@ -30,7 +29,7 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
test('it should render correct number of namespaces', async function (assert) {
assert.expect(3);
- await page.visit();
+ await visit('/vault/access/namespaces');
const store = this.owner.lookup('service:store');
// Default page size is 15
assert.strictEqual(store.peekAll('namespace').length, 15, 'Store has 15 namespaces records');
diff --git a/ui/tests/helpers/auth/auth-helpers.ts b/ui/tests/helpers/auth/auth-helpers.ts
new file mode 100644
index 0000000000..2e82473a1b
--- /dev/null
+++ b/ui/tests/helpers/auth/auth-helpers.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { click, fillIn, visit } from '@ember/test-helpers';
+import VAULT_KEYS from 'vault/tests/helpers/vault-keys';
+import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
+
+const { rootToken } = VAULT_KEYS;
+
+export const login = async (token = rootToken) => {
+ // make sure we're always logged out and logged back in
+ await visit('/vault/logout');
+ // clear session storage to ensure we have a clean state
+ window.localStorage.clear();
+ await visit('/vault/auth?with=token');
+ await fillIn(AUTH_FORM.input('token'), token);
+ return await click(AUTH_FORM.login);
+};
diff --git a/ui/tests/integration/components/auth/page-test.js b/ui/tests/integration/components/auth/page-test.js
index b9b55abd4d..45628cd391 100644
--- a/ui/tests/integration/components/auth/page-test.js
+++ b/ui/tests/integration/components/auth/page-test.js
@@ -28,7 +28,7 @@ module('Integration | Component | auth | page ', function (hooks) {
this.renderComponent = async () => {
return render(hbs`
-