diff --git a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx b/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx
deleted file mode 100644
index c22002a66..000000000
--- a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import clsx from 'clsx';
-import './style.scss';
-import { Icon } from '../../../../../shared/components/Icon';
-import { RadioIndicator } from '../../../../../shared/components/RadioIndicator/RadioIndicator';
-import { mfaMethodIcon } from '../../../../../shared/consts';
-import { MfaMethod, type MfaMethodValue } from '../../../../../shared/rust-api/types';
-import {
- mfaToText,
- type OpenIdInstance,
- openIdProviderName,
-} from '../../../../../shared/utils/mfa';
-
-type FactorText = {
- title: string;
- description: string;
-};
-
-const factorText = (method: MfaMethodValue, instance?: OpenIdInstance): FactorText => {
- switch (method) {
- case MfaMethod.Totp:
- return {
- title: 'Security code from your authenticator app',
- description: `Use an authenticator app on your phone, such as Google Authenticator, Microsoft Authenticator, or Authy, to generate a temporary code and verify your identity.`,
- };
- case MfaMethod.Email:
- return {
- title: `Code from email`,
- description: `Enter the temporary security code sent to your email to verify your identity and continue.`,
- };
- case MfaMethod.Oidc:
- return {
- title: `Verify with ${openIdProviderName(instance)}`,
- description: `Sign in through your identity provider in the browser to verify your identity.`,
- };
- case MfaMethod.Biometric:
- return { title: '', description: '' };
- case MfaMethod.MobileApprove:
- return { title: '', description: '' };
- case MfaMethod.Fido2:
- return {
- title: mfaToText(MfaMethod.Fido2),
- description: `Verify your identity using a registered security key, such as a YubiKey.`,
- };
- }
-};
-
-interface Props {
- mfaMethod: MfaMethodValue;
- instance?: OpenIdInstance;
- selected: boolean;
- disabled?: boolean;
- onClick: () => void;
-}
-
-export const ConfigureMfaVerificatorFactorSelector = ({
- mfaMethod,
- instance,
- selected,
- disabled = false,
- onClick,
-}: Props) => {
- const { title, description } = factorText(mfaMethod, instance);
-
- return (
-
{
- if (!disabled) onClick();
- }}
- >
-
-
-
-
-
-
{title}
-
{description}
-
-
-
-
-
-
- );
-};
diff --git a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/style.scss b/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/style.scss
deleted file mode 100644
index 729a1cc7f..000000000
--- a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/style.scss
+++ /dev/null
@@ -1,55 +0,0 @@
-.mfa-verification-factor-selector {
- --border-color: var(--border-disabled);
-
- border: 1px solid var(--border-color);
- border-radius: 12px;
- box-sizing: border-box;
- padding: var(--spacing-lg) var(--spacing-md);
- transition: border-color 160ms ease-out;
- cursor: pointer;
- user-select: none;
-
- &.selected {
- --border-color: var(--fg-white-100);
- }
-
- &.disabled {
- cursor: not-allowed;
- opacity: 0.5;
- }
-
- > .inner-grid {
- display: grid;
- grid-template-rows: 1fr;
- grid-template-columns: 20px var(--spacing-lg) minmax(0, 1fr) var(--spacing-4xl) 24px;
-
- > .icon-col {
- grid-row: 1;
- grid-column: 1;
- }
-
- > .content-col {
- grid-row: 1;
- grid-column: 3;
-
- > .title {
- font: var(--t-body-sm-500);
- color: var(--fg-white-100);
- padding-bottom: var(--spacing-xs);
- }
-
- > .description {
- font: var(--t-body-xs-400);
- color: var(--fg-white-70);
- }
- }
-
- > .indicator-col {
- grid-row: 1;
- grid-column: 5;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- }
-}
diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx
index b102767d3..7b2532e48 100644
--- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx
+++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx
@@ -1,20 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
-import { useState } from 'react';
import { Button } from '../../../../../shared/components/Button/Button';
import { ButtonVariant } from '../../../../../shared/components/Button/types';
import { Controls } from '../../../../../shared/components/Controls/Controls';
-import { ExternalProviderButton } from '../../../../../shared/components/ExternalProviderButton/ExternalProviderButton';
-import { FullPageTitle } from '../../../../../shared/components/FullPageTitle/FullPageTitle';
+import { Icon, IconKind } from '../../../../../shared/components/Icon';
+import { MfaSelector } from '../../../../../shared/components/LocationCard/components/MfaSelector/MfaSelector';
+import { SizedBox } from '../../../../../shared/components/SizedBox/SizedBox';
import { FullPage } from '../../../../../shared/layouts/FullPage/FullPage';
-import { ConfigureFactorsSource, MfaMethod } from '../../../../../shared/rust-api/types';
+import { ConfigureFactorsSource } from '../../../../../shared/rust-api/types';
+import { ThemeSpacing, ThemeVariable } from '../../../../../shared/types';
import { isPresent } from '../../../../../shared/utils/isPresent';
-import {
- type ClientAuthorizingMethod,
- findOpenIdProvider,
- openIdProviderName,
-} from '../../../../../shared/utils/mfa';
-import { ConfigureMfaVerificatorFactorSelector } from '../../components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector';
import {
discardMfaConfiguration,
useConfigureMfaStore,
@@ -36,10 +31,6 @@ export const ConfigureSelectVerificationStep = ({ onCancel }: Props) => {
);
const instance = useConfigureMfaStore((s) => s.instance) ?? undefined;
- const [selected, setSelected] = useState(
- methods[0],
- );
-
const { mutate: cancel, isPending: isCancelling } = useMutation({
// the source is read before the discard resets the store
mutationFn: async () => {
@@ -63,26 +54,34 @@ export const ConfigureSelectVerificationStep = ({ onCancel }: Props) => {
hideScrollContainer
withControls
>
-
-
- {`For security, verify your identity using one of your existing MFA methods. You'll be able to add a new method after verification.`}
-
- {isPresent(error) && {error}
}
-
-
Select an existing method
-
-
- {methods.map((method) => (
-
{
- setSelected(method);
- }}
- />
- ))}
+
+
+
+
Verify your identity
+
+
+ To add a new MFA method, please select one of the available options to verify
+ your identity.
+
+
+ {isPresent(error) &&
{error}
}
+
+ {methods.map((method) => (
+ {
+ useConfigureMfaStore.getState().selectVerificationMethod(method);
+ }}
+ />
+ ))}
+
);
diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss
index 127a27029..9b2234e78 100644
--- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss
+++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss
@@ -1,18 +1,37 @@
#configure-select-verification-step {
- .label {
- padding-bottom: var(--spacing-md);
+ & > .content {
+ display: flex;
+ flex: 1 1 auto;
+ flex-flow: column;
+ align-items: center;
+ justify-content: center;
- & > p {
- font: var(--t-body-xs-500);
+ & > .title {
+ text-align: center;
+ font: var(--t-h4);
color: var(--fg-white-100);
}
- }
- .methods {
- display: flex;
- flex-flow: column;
- row-gap: var(--spacing-md);
- width: 100%;
- padding-bottom: var(--spacing-xl);
+ & > .subtitle {
+ max-width: 380px;
+ text-align: center;
+ font: var(--t-body-sm-400);
+ color: var(--fg-white-70);
+ }
+
+ & > .error {
+ padding-bottom: var(--spacing-xl);
+ text-align: center;
+ font: var(--t-body-sm-400);
+ color: var(--fg-critical);
+ }
+
+ & > .methods {
+ display: flex;
+ flex-flow: column;
+ row-gap: var(--spacing-md);
+ width: 100%;
+ max-width: 296px;
+ }
}
}
diff --git a/new-ui/src/shared/components/Icon/Icon.tsx b/new-ui/src/shared/components/Icon/Icon.tsx
index 172dafde8..8e63878ab 100644
--- a/new-ui/src/shared/components/Icon/Icon.tsx
+++ b/new-ui/src/shared/components/Icon/Icon.tsx
@@ -130,6 +130,7 @@ import { IconUbuntu } from './icons/IconUbuntu';
import { IconUpload } from './icons/IconUpload';
import { IconUser } from './icons/IconUser';
import { IconUserActive } from './icons/IconUserActive';
+import { IconUserShield } from './icons/IconUserShield';
import { IconUsers } from './icons/IconUsers';
import { IconWarningFilled } from './icons/IconWarningFilled';
import { IconWarningOutlined } from './icons/IconWarningOutlined';
@@ -430,6 +431,8 @@ export const Icon = ({
return IconUser;
case 'user-active':
return IconUserActive;
+ case 'user-shield':
+ return IconUserShield;
case 'users':
return IconUsers;
case 'webhooks':
diff --git a/new-ui/src/shared/components/Icon/icon-types.ts b/new-ui/src/shared/components/Icon/icon-types.ts
index d9b22cd2e..29097a15c 100644
--- a/new-ui/src/shared/components/Icon/icon-types.ts
+++ b/new-ui/src/shared/components/Icon/icon-types.ts
@@ -91,6 +91,7 @@ export const IconKind = {
EmptyPoint: 'empty-point',
DevicesActive: 'devices-active',
UserActive: 'user-active',
+ UserShield: 'user-shield',
Windows: 'windows',
AppStore: 'app-store',
Apple: 'apple',
diff --git a/new-ui/src/shared/components/Icon/icons/IconUserShield.tsx b/new-ui/src/shared/components/Icon/icons/IconUserShield.tsx
new file mode 100644
index 000000000..8ff0c20e4
--- /dev/null
+++ b/new-ui/src/shared/components/Icon/icons/IconUserShield.tsx
@@ -0,0 +1,31 @@
+import type { SVGProps } from 'react';
+
+export const IconUserShield = (props: SVGProps) => {
+ return (
+
+ );
+};
diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx
index b7b3bd3c8..aca37bea0 100644
--- a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx
+++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx
@@ -8,7 +8,7 @@ import {
type MfaMethodValue,
} from '../../../../rust-api/types';
import { isPresent } from '../../../../utils/isPresent';
-import { findOpenIdProvider, mfaToText } from '../../../../utils/mfa';
+import { findOpenIdProvider, mfaToText, openIdProviderName } from '../../../../utils/mfa';
import { Icon } from '../../../Icon';
import checkboxSrc from './assets/checkbox.svg';
import { openIdProviderIcon } from './consts';
@@ -69,7 +69,11 @@ export const MfaSelector = ({
)}
-
{mfaToText(factor, instance)}
+
+ {factor === MfaMethod.Oidc
+ ? `Verify with ${openIdProviderName(instance)}`
+ : mfaToText(factor, instance)}
+
{(isMobileOnly || !configured) && (