{{#if this.showCustomAuthOptions}}
- {{#if @directLinkData.isVisibleMount}}
- {{! URL contains a "with" query param that references a mount with listing_visibility="unauth" }}
- {{! Treat it as a "preferred" mount and hide all other tabs }}
-
- {{else}}
-
- {{/if}}
+
{{else}}
{{! fallback view is the dropdown with all auth methods }}
{
@tracked selectedAuthMethod = '';
@tracked errorMessage = '';
+ get tabData() {
+ const { directLinkData } = this.args;
+ // URL contains a "with" query param that references a mount with listing_visibility="unauth"
+ // Treat it as a "preferred" mount and hide all other tabs
+ if (directLinkData?.isVisibleMount && directLinkData?.type) {
+ return { [directLinkData.type]: [this.args.directLinkData] };
+ }
+ return this.args.visibleMountsByType;
+ }
+
get authTabTypes() {
const visibleMounts = this.args.visibleMountsByType;
return visibleMounts ? Object.keys(visibleMounts) : [];
diff --git a/ui/app/components/auth/mounts-display.hbs b/ui/app/components/auth/mounts-display.hbs
deleted file mode 100644
index 841986f326..0000000000
--- a/ui/app/components/auth/mounts-display.hbs
+++ /dev/null
@@ -1,33 +0,0 @@
-{{!
- Copyright (c) HashiCorp, Inc.
- SPDX-License-Identifier: BUSL-1.1
-}}
-
-{{#if (gt @mounts.length 1)}}
- {{! render dropdown of mount paths }}
-
- Mount path
-
- {{#each @mounts as |mount|}}
-
- {{/each}}
-
-
-{{else}}
- {{! render a single mount path }}
- {{#let (get @mounts "0") as |mount|}}
- {{#unless @hideType}}
-
- {{auth-display-name mount.type}}
-
- {{/unless}}
- {{#if mount.description}}
- {{mount.description}}
- {{/if}}
- {{! the token auth method does't support custom paths so no need to render an input }}
- {{#if @shouldRenderPath}}
- {{! path is hidden so it is submitted with FormData but does not clutter the login form }}
-
- {{/if}}
- {{/let}}
-{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/auth/page.js b/ui/app/components/auth/page.ts
similarity index 68%
rename from ui/app/components/auth/page.js
rename to ui/app/components/auth/page.ts
index dbb0841525..b0c1c9a3c0 100644
--- a/ui/app/components/auth/page.js
+++ b/ui/app/components/auth/page.ts
@@ -8,6 +8,11 @@ import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
+import type { AuthResponse, AuthResponseWithMfa } from 'vault/vault/services/auth';
+import type { UnauthMountsByType, UnauthMountsResponse } from 'vault/vault/auth/form';
+import type ClusterModel from 'vault/models/cluster';
+import type CspEventService from 'vault/services/csp-event';
+
/**
* @module AuthPage
* The Auth::Page is the route template for the login splash view. It renders the Auth::FormTemplate or MFA component if an
@@ -24,23 +29,36 @@ import { action } from '@ember/object';
* @directLinkData={{this.model.directLinkData}}
* />
*
- * @param {string} directLinkData - type or mount data gleaned from query param
* @param {object} cluster - the ember data cluster model. contains information such as cluster id, name and boolean for if the cluster is in standby
+ * @param {object} directLinkData - mount data built from the "with" query param. If param is a mount path and maps to a visible mount, the login form defaults to this mount. Otherwise the form preselects the passed auth type.
+ * @param {object} loginSettings - * enterprise only * login settings configured for the namespace
* @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 {object} visibleAuthMounts - mount paths with listing_visibility="unauth", keys are the mount path and value is it's mount data such as "type" or "description," if it exists
+ * @param {object} visibleAuthMounts - response from unauthenticated request to sys/internal/ui/mounts which returns mount paths tuned with `listing_visibility="unauth"`. keys are the mount path, values are mount data such as "type" or "description," if it exists
* */
export const CSP_ERROR =
"This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.";
-export default class AuthPage extends Component {
- @service('csp-event') csp;
+interface Args {
+ visibleAuthMounts: UnauthMountsResponse;
+ cluster: ClusterModel;
+ onAuthSuccess: CallableFunction;
+}
+
+interface MfaAuthData {
+ mfa_requirement: object;
+ selectedAuth: string;
+ path: string;
+}
+
+export default class AuthPage extends Component {
+ @service('csp-event') declare readonly csp: CspEventService;
@tracked canceledMfaAuth = '';
- @tracked mfaAuthData;
+ @tracked mfaAuthData: MfaAuthData | null = null;
@tracked mfaErrors = '';
get visibleMountsByType() {
@@ -52,7 +70,7 @@ export default class AuthPage extends Component {
obj[type] ??= []; // if an array doesn't already exist for that type, create it
obj[type].push({ path, ...mountData });
return obj;
- }, {});
+ }, {} as UnauthMountsByType);
}
return null;
}
@@ -64,8 +82,8 @@ export default class AuthPage extends Component {
}
@action
- onAuthResponse(authResponse, { selectedAuth, path }) {
- const { mfa_requirement } = authResponse;
+ onAuthResponse(authResponse: AuthResponse | AuthResponseWithMfa, { selectedAuth = '', path = '' }) {
+ const mfa_requirement = 'mfa_requirement' in authResponse ? authResponse.mfa_requirement : undefined;
/*
Checking for an mfa_requirement happens in two places.
If doSubmit in is called directly (by the
diff --git a/ui/app/models/role-jwt.js b/ui/app/models/role-jwt.js
index 965364848f..bf5505f3a7 100644
--- a/ui/app/models/role-jwt.js
+++ b/ui/app/models/role-jwt.js
@@ -13,6 +13,7 @@ const DOMAIN_STRINGS = {
'ping.com': 'Ping',
'okta.com': 'Okta',
'auth0.com': 'Auth0',
+ 'login.microsoftonline.com': 'Azure',
};
const PROVIDER_WITH_LOGO = {
@@ -21,6 +22,7 @@ const PROVIDER_WITH_LOGO = {
Google: 'google',
Okta: 'okta',
Auth0: 'auth0',
+ Azure: 'azure',
};
export { DOMAIN_STRINGS, PROVIDER_WITH_LOGO };
diff --git a/ui/app/styles/core/columns.scss b/ui/app/styles/core/columns.scss
index a8e029e589..e8437cb2c0 100644
--- a/ui/app/styles/core/columns.scss
+++ b/ui/app/styles/core/columns.scss
@@ -199,5 +199,6 @@
.column.is-4-desktop {
flex: none;
width: 33.33333%;
+ max-width: 600px;
}
}
diff --git a/ui/mirage/scenarios/custom-login.js b/ui/mirage/scenarios/custom-login.js
index 44d45f7e50..5fe4bf312e 100644
--- a/ui/mirage/scenarios/custom-login.js
+++ b/ui/mirage/scenarios/custom-login.js
@@ -8,7 +8,7 @@ export default function (server) {
name: 'Root namespace default',
namespace: '',
default_auth_type: 'userpass',
- backup_auth_types: ['okta'],
+ backup_auth_types: ['okta', 'token'],
disable_inheritance: true,
});
server.create('login-rule', {
@@ -16,7 +16,7 @@ export default function (server) {
default_auth_type: 'oidc',
backup_auth_types: ['token'],
});
- server.create('login-rule', { default_auth_type: 'jwt', backup_auth_types: [] });
- server.create('login-rule', { default_auth_type: '', backup_auth_types: ['oidc', 'jwt'] });
- server.create('login-rule', { default_auth_type: '', backup_auth_types: ['token'] });
+ server.create('login-rule', { default_auth_type: 'jwt', backup_auth_types: null }); // namespace-2
+ server.create('login-rule', { default_auth_type: '', backup_auth_types: ['oidc', 'jwt'] }); // namespace-3
+ server.create('login-rule', { default_auth_type: '', backup_auth_types: ['token'] }); // namespace-4
}
diff --git a/ui/tests/acceptance/auth/auth-test.js b/ui/tests/acceptance/auth/auth-test.js
index a2d9ec8dd7..4414888577 100644
--- a/ui/tests/acceptance/auth/auth-test.js
+++ b/ui/tests/acceptance/auth/auth-test.js
@@ -103,15 +103,15 @@ module('Acceptance | auth login form', function (hooks) {
test('it renders preferred mount view if "with" query param is a mount path with listing_visibility="unauth"', async function (assert) {
await visit('/vault/auth?with=my-oidc%2F');
- await waitFor(AUTH_FORM.preferredMethod('oidc'));
- assert.dom(AUTH_FORM.preferredMethod('oidc')).hasText('OIDC', 'it renders mount type');
+ await waitFor(AUTH_FORM.tabBtn('oidc'));
+ assert.dom(AUTH_FORM.authForm('oidc')).exists();
+ assert.dom(AUTH_FORM.tabBtn('oidc')).exists();
assert.dom(GENERAL.inputByAttr('role')).exists();
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
assert.dom(AUTH_FORM.otherMethodsBtn).exists('"Sign in with other methods" renders');
- assert.dom(AUTH_FORM.tabBtn('oidc')).doesNotExist('tab does not render');
- assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist();
+ assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist('dropdown does not render');
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
assert.dom(GENERAL.backButton).doesNotExist();
});
@@ -122,7 +122,9 @@ module('Acceptance | auth login form', function (hooks) {
assert
.dom(AUTH_FORM.tabBtn('oidc'))
.hasAttribute('aria-selected', 'true', 'it selects tab matching query param');
- assert.dom(AUTH_FORM.preferredMethod('oidc')).doesNotExist('it does not render single mount view');
+ assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
+ assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
+ assert.dom(AUTH_FORM.otherMethodsBtn).exists('"Sign in with other methods" renders');
assert.dom(GENERAL.backButton).doesNotExist();
});
diff --git a/ui/tests/helpers/auth/auth-form-selectors.ts b/ui/tests/helpers/auth/auth-form-selectors.ts
index 5f4f34cf90..762060a039 100644
--- a/ui/tests/helpers/auth/auth-form-selectors.ts
+++ b/ui/tests/helpers/auth/auth-form-selectors.ts
@@ -7,7 +7,6 @@ export const AUTH_FORM = {
selectMethod: '[data-test-select="auth type"]',
form: '[data-test-auth-form]',
login: '[data-test-auth-submit]',
- preferredMethod: (method: string) => `p[data-test-auth-method="${method}"]`,
tabs: '[data-test-auth-tab]',
tabBtn: (method: string) => `[data-test-auth-tab="${method}"] button`, // method is all lowercased
description: '[data-test-description]',
diff --git a/ui/tests/integration/components/auth/form-template-test.js b/ui/tests/integration/components/auth/form-template-test.js
index 99eeee1304..ab1db62d7c 100644
--- a/ui/tests/integration/components/auth/form-template-test.js
+++ b/ui/tests/integration/components/auth/form-template-test.js
@@ -183,7 +183,7 @@ module('Integration | Component | auth | form template', function (hooks) {
test('it renders the mount description', async function (assert) {
await this.renderComponent();
await click(AUTH_FORM.tabBtn('token'));
- assert.dom('section p').hasText('token based credentials');
+ assert.dom(AUTH_FORM.description).hasText('token based credentials');
});
test('it renders a dropdown if multiple mount paths are returned', async function (assert) {
@@ -261,14 +261,15 @@ module('Integration | Component | auth | form template', function (hooks) {
test('it renders single mount view instead of tabs if @directLinkData data exists and includes mount data', async function (assert) {
this.directLinkData = { path: 'my-oidc/', type: 'oidc', isVisibleMount: true };
await this.renderComponent();
- assert.dom(AUTH_FORM.preferredMethod('oidc')).hasText('OIDC', 'it renders mount type');
+ assert.dom(AUTH_FORM.authForm('oidc')).exists;
+ assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders auth type tab');
+ assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
assert.dom(GENERAL.inputByAttr('role')).exists();
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
assert.dom(AUTH_FORM.otherMethodsBtn).exists('"Sign in with other methods" renders');
- assert.dom(AUTH_FORM.tabBtn('oidc')).doesNotExist('tab does not render');
- assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist();
+ assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist('dropdown does not render');
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
assert.dom(GENERAL.backButton).doesNotExist();
});
@@ -284,8 +285,6 @@ module('Integration | Component | auth | form template', function (hooks) {
assert.dom(GENERAL.inputByAttr('password')).exists();
await click(AUTH_FORM.advancedSettings);
assert.dom(GENERAL.inputByAttr('path')).exists();
-
- assert.dom(AUTH_FORM.preferredMethod('ldap')).doesNotExist('single mount view does not render');
assert.dom(AUTH_FORM.tabBtn('ldap')).doesNotExist('tab does not render');
assert
.dom(GENERAL.backButton)
diff --git a/ui/tests/integration/components/auth/page-test.js b/ui/tests/integration/components/auth/page-test.js
index d1d41c8f3c..d855041ab5 100644
--- a/ui/tests/integration/components/auth/page-test.js
+++ b/ui/tests/integration/components/auth/page-test.js
@@ -132,8 +132,6 @@ module('Integration | Component | auth | page', function (hooks) {
assert.dom(GENERAL.inputByAttr('password')).exists();
await click(AUTH_FORM.advancedSettings);
assert.dom(GENERAL.inputByAttr('path')).exists();
-
- assert.dom(AUTH_FORM.preferredMethod('ldap')).doesNotExist('single mount view does not render');
assert.dom(AUTH_FORM.tabBtn('ldap')).doesNotExist('tab does not render');
assert
.dom(GENERAL.backButton)
@@ -144,13 +142,11 @@ module('Integration | Component | auth | page', function (hooks) {
test('it renders single mount view instead of tabs if @directLinkData data references a visible type', async function (assert) {
this.directLinkData = { path: 'my-oidc/', type: 'oidc', isVisibleMount: true };
await this.renderComponent();
- assert.dom(AUTH_FORM.preferredMethod('oidc')).hasText('OIDC', 'it renders mount type');
+ assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders tab for type');
assert.dom(GENERAL.inputByAttr('role')).exists();
assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
assert.dom(AUTH_FORM.otherMethodsBtn).exists('"Sign in with other methods" renders');
-
- assert.dom(AUTH_FORM.tabBtn('oidc')).doesNotExist('tab does not render');
assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist();
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
assert.dom(GENERAL.backButton).doesNotExist();
diff --git a/ui/tests/unit/models/role-jwt-test.js b/ui/tests/unit/models/role-jwt-test.js
index 71777c4c7e..6f2e1775df 100644
--- a/ui/tests/unit/models/role-jwt-test.js
+++ b/ui/tests/unit/models/role-jwt-test.js
@@ -28,7 +28,7 @@ module('Unit | Model | role-jwt', function (hooks) {
});
test('it provides a providerName for listed known providers', function (assert) {
- assert.expect(12);
+ assert.expect(14);
Object.keys(DOMAIN_STRINGS).forEach((domain) => {
const model = this.owner.lookup('service:store').createRecord('role-jwt', {
authUrl: `http://provider-${domain}`,
diff --git a/ui/types/vault/auth/form.d.ts b/ui/types/vault/auth/form.d.ts
index 409acac270..cf32073d7f 100644
--- a/ui/types/vault/auth/form.d.ts
+++ b/ui/types/vault/auth/form.d.ts
@@ -7,6 +7,10 @@ export interface UnauthMountsByType {
// key is the auth method type
[key: string]: AuthTabMountData[];
}
+export interface UnauthMountsResponse {
+ // key is the mount path
+ [key: string]: { type: string; description?: string; config?: object | null };
+}
export interface AuthTabMountData {
path: string;
diff --git a/ui/types/vault/services/auth.d.ts b/ui/types/vault/services/auth.d.ts
index 4e91797ca3..dee261b39c 100644
--- a/ui/types/vault/services/auth.d.ts
+++ b/ui/types/vault/services/auth.d.ts
@@ -16,7 +16,6 @@ export interface AuthData {
renewable: boolean;
entity_id: string;
displayName?: string;
- mfa_requirement?: MfaRequirementApiResponse;
}
export interface AuthResponse {
@@ -24,6 +23,9 @@ export interface AuthResponse {
token: string; // the name of the token in local storage, not the actual token
isRoot: boolean;
}
+export interface AuthResponseWithMfa {
+ mfa_requirement: MfaRequirementApiResponse;
+}
export default class AuthService extends Service {
authData: AuthData;