claire bontempo 9832c90037
UI: Implement accessible auth form components (#30500)
* UI: Move `wrapped_token` login functionality to route (#30465)

* move token unwrap functionality to page component

* update mfa test

* remove wrapped_token logic from page component

* more cleanup to relocate unwrap logic

* move wrapped_token to route

* move unwrap tests to acceptance

* move mfa form back

* add some padding

* update mfa-form tests

* get param from params

* wait for auth form on back

* run rests

* UI: Add MFA support for SSO methods (#30489)

* initial implementation of mfa validation for sso methods

* update typescript interfaces

* add stopgap changes to auth service

* switch order backend is defined

* update login form for tests even though it will be deleted

* attempt to stabilize wrapped_query test

* =update login form test why not

* Update ui/app/components/auth/form/saml.ts

Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>

---------

Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>

* Move CSP error to page component (#30492)

* initial implementation of mfa validation for sso methods

* update typescript interfaces

* add stopgap changes to auth service

* switch order backend is defined

* update login form for tests even though it will be deleted

* attempt to stabilize wrapped_query test

* =update login form test why not

* move csp error to page component

* move csp error to page component

* Move fetching unauthenticated mounts to the route (#30509)

* rename namespace arg to namespaceQueryParam

* move fetch mounts to route

* add margin to sign in button spacing

* update selectors for oidc provider test

* add todo delete comments

* fix arg typo in test

* change method name

* fix args handling tab click

* remove tests that no longer relate to components functionality

* add tests for preselectedAuthType functionality

* move typescript interfaces, fix selector

* add await

* oops

* move format method down, make private

* move tab formatting to the route

* move to page object

* fix token unwrap aborting transition

* not sure what that is doing there..

* add comments

* rename to presetAuthType

* use did-insert instead

* UI: Implement `Auth::FormTemplate` (#30521)

* replace Auth::LoginForm with Auth::FormTemplate

* first round of test updates

* return null if mounts object is empty

* add comment and test for empty sys/internal/mounts data

* more test updates

* delete listing_visibility test, delete login-form component test

* update divs to Hds::Card::Container

* add overflow class

* remove unused getters

* move requesting stored auth type to page component

* fix typo

* Update ui/app/components/auth/form/oidc-jwt.ts

make comment make more sense

* small cleanup items, update imports

* Delete old auth components (#30527)

* delete old components

* update codeowners

* Update `with` query param functionality (#30537)

* update path input to type=hidden

* add test coverage

* update page test

* update auth route

* delete login form

* update ent test

* consolidate logic in getter

* add more comments

* more comments..

* rename selector

* refresh model as well

* redirect for invalid query params

* move unwrap to redirect

* only redirect on invalid query params

* add tests for query param

* test selector updates

* remove todos, update relevant ones with initials

* add changelog

---------

Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
2025-05-08 09:58:20 -07:00

125 lines
4.5 KiB
TypeScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import { sanitizePath } from 'core/utils/sanitize-path';
import { POSSIBLE_FIELDS } from 'vault/utils/supported-login-methods';
import type AuthService from 'vault/vault/services/auth';
import type ClusterModel from 'vault/models/cluster';
import type FlagsService from 'vault/services/flags';
import type VersionService from 'vault/services/version';
import type { AuthResponse } from 'vault/vault/services/auth';
import type { HTMLElementEvent } from 'vault/forms';
import type { LoginFields } from 'vault/vault/auth/form';
import type { MfaRequirementApiResponse, ParsedMfaRequirement } from 'vault/vault/auth/mfa';
/**
* @module Auth::Base
*
* @param {string} authType - chosen login method type
* @param {object} cluster - The cluster model which contains information such as cluster id, name and boolean for if the cluster is in standby
* @param {function} onError - callback if there is a login error
* @param {function} onSuccess - calls onAuthResponse in auth/page redirects if successful
*/
interface Args {
authType: string;
cluster: ClusterModel;
onError: CallableFunction;
onSuccess: CallableFunction;
}
export default class AuthBase extends Component<Args> {
@service declare readonly auth: AuthService;
@service declare readonly flags: FlagsService;
@service declare readonly version: VersionService;
@action
onSubmit(event: HTMLElementEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const data = this.parseFormData(formData);
this.login.unlinked().perform(data);
}
login = task(
waitFor(async (formData) => {
try {
const authResponse = await this.auth.authenticate({
clusterId: this.args.cluster.id,
backend: this.args.authType,
data: formData,
selectedAuth: this.args.authType,
});
const path = formData?.path;
this.handleAuthResponse(authResponse, path);
} catch (error) {
this.onError(error as Error);
}
})
);
// Standard methods get mfa_requirements from the authenticate method in the auth service
// methodData is necessary if there's an MfaRequirement because persisting auth data happens after that
handleAuthResponse(authResponse: AuthResponse | ParsedMfaRequirement, path?: string) {
const methodData: { selectedAuth: string; path?: string } = { selectedAuth: this.args.authType, path };
// calls onAuthResponse in parent auth/page.js component
this.args.onSuccess(authResponse, methodData);
}
// SSO methods with a different token exchange workflow skip the auth service authenticate method
// and need mfa handle separately
handleMfa(mfaRequirement: MfaRequirementApiResponse, path: string) {
const parsedMfaAuthResponse = this.auth._parseMfaResponse(mfaRequirement);
this.handleAuthResponse(parsedMfaAuthResponse, path);
}
onError(error: Error | string) {
if (!this.auth.mfaErrors) {
const errorMessage = `Authentication failed: ${this.auth.handleError(error)}`;
this.args.onError(errorMessage);
}
}
parseFormData(formData: FormData) {
const data: LoginFields = {};
// iterate over method specific fields
for (const field of POSSIBLE_FIELDS) {
const value = formData.get(field);
if (value) {
data[field] = typeof value === 'string' ? value : undefined;
}
}
// path is supported by all auth methods except token
if (this.args.authType !== 'token') {
// strip leading or trailing slashes for consistency.
// fallback to auth type which is the default path Vault expects.
data['path'] = sanitizePath(formData?.get('path')) || this.args.authType;
}
if (this.version.isEnterprise) {
// strip leading or trailing slashes for consistency
let namespace = sanitizePath(formData?.get('namespace')) || '';
const hvdRootNs = this.flags.hvdManagedNamespaceRoot; // if HVD managed, this is "admin"
if (hvdRootNs) {
// HVD managed clusters can only input child namespaces, manually prepend with the hvd root
namespace = namespace ? `${hvdRootNs}/${namespace}` : hvdRootNs;
}
data['namespace'] = namespace;
}
return data;
}
}