From fd3960d337e16090643119882f30b6680fa17620 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:34:29 +0200 Subject: [PATCH] move fido code --- .../hooks/useConfigureMfaStore.tsx | 18 ++++++------ .../src/pages/full/ConfigureMfaPage/types.ts | 12 +------- .../src/pages/full/ConfigureMfaPage/utils.ts | 10 +++---- .../verify/ConfigureMfaVerify.tsx | 4 +-- .../ConfigureSelectMethodsStep.tsx | 15 +++++++--- .../ConfigureSelectVerificationStep.tsx | 6 ++-- new-ui/src/shared/utils/mfa.test.ts | 26 +++++++++++++++++ new-ui/src/shared/utils/mfa.ts | 28 +++++++++++++++---- src-tauri/Cargo.toml | 4 +-- src-tauri/deny.toml | 6 ++-- src-tauri/{ => enterprise}/fido2/Cargo.toml | 2 +- src-tauri/{ => enterprise}/fido2/src/hid.rs | 0 src-tauri/{ => enterprise}/fido2/src/lib.rs | 0 .../{ => enterprise}/fido2/src/protocol.rs | 0 .../fido2/src/protocol/tests.rs | 0 .../{ => enterprise}/fido2/src/windows/api.rs | 0 .../fido2/src/windows/convert.rs | 0 .../{ => enterprise}/fido2/src/windows/mod.rs | 0 .../fido2/src/windows/options.rs | 0 19 files changed, 86 insertions(+), 45 deletions(-) rename src-tauri/{ => enterprise}/fido2/Cargo.toml (97%) rename src-tauri/{ => enterprise}/fido2/src/hid.rs (100%) rename src-tauri/{ => enterprise}/fido2/src/lib.rs (100%) rename src-tauri/{ => enterprise}/fido2/src/protocol.rs (100%) rename src-tauri/{ => enterprise}/fido2/src/protocol/tests.rs (100%) rename src-tauri/{ => enterprise}/fido2/src/windows/api.rs (100%) rename src-tauri/{ => enterprise}/fido2/src/windows/convert.rs (100%) rename src-tauri/{ => enterprise}/fido2/src/windows/mod.rs (100%) rename src-tauri/{ => enterprise}/fido2/src/windows/options.rs (100%) diff --git a/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx index fdf06de92..092ba1920 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/hooks/useConfigureMfaStore.tsx @@ -13,14 +13,12 @@ import { type MfaMethodValue, } from '../../../../shared/rust-api/types'; import { isPresent } from '../../../../shared/utils/isPresent'; -import { setupMethodsOf } from '../../../../shared/utils/mfa'; -import { dismissEdgeComsError } from '../components/EdgeComsError/useEdgeComsErrorStore'; import { - ConfigureMfaStep, - type ConfigureMfaStepValue, - MFA_WIZARD_STEPS, - type MfaVerificationMethod, -} from '../types'; + type ClientAuthorizingMethod, + setupMethodsOf, +} from '../../../../shared/utils/mfa'; +import { dismissEdgeComsError } from '../components/EdgeComsError/useEdgeComsErrorStore'; +import { ConfigureMfaStep, type ConfigureMfaStepValue, MFA_WIZARD_STEPS } from '../types'; import { isCodeMfaMethod, isMfaFactorOfferable, @@ -45,9 +43,9 @@ type StoreValues = { /** Pre-ticked in the selection step, from the entry point or an earlier pass. */ initialSelection: MfaMethodValue[]; /** most preferred first, from the session since an older Core rejects some factors */ - verificationMethods: MfaVerificationMethod[]; + verificationMethods: ClientAuthorizingMethod[]; /** null until picked, asked only when the session offers more than one method */ - verificationMethod: MfaVerificationMethod | null; + verificationMethod: ClientAuthorizingMethod | null; /** No factor was configured, so an emailed code was the only way in. */ emailFallback: boolean; /** ISO timestamp the session dies at, null once nothing in the flow needs it. */ @@ -136,7 +134,7 @@ interface Store extends StoreValues { origin: ConfigureMfaOrigin, ) => void; selectMethods: (methods: MfaMethodValue[]) => void; - selectVerificationMethod: (method: MfaVerificationMethod) => void; + selectVerificationMethod: (method: ClientAuthorizingMethod) => void; /** Keeps the current picks ticked and the session alive. */ backToSelection: () => void; backFromVerification: () => void; diff --git a/new-ui/src/pages/full/ConfigureMfaPage/types.ts b/new-ui/src/pages/full/ConfigureMfaPage/types.ts index 3be9ef2ba..7f6ac7614 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/types.ts +++ b/new-ui/src/pages/full/ConfigureMfaPage/types.ts @@ -1,4 +1,4 @@ -import { MfaMethod, type MfaMethodValue } from '../../../shared/rust-api/types'; +import type { MfaMethodValue } from '../../../shared/rust-api/types'; /** Wizard steps, in the order they run. Setup steps are the ones a factor can claim. */ export const ConfigureMfaStep = { @@ -25,13 +25,3 @@ export type MfaFactor = { /** Whether the account can hold several of these, so it stays offerable once configured. */ repeatable: boolean; }; - -/** most preferred first */ -export const mfaVerificationMethods = [ - MfaMethod.Totp, - MfaMethod.Email, - MfaMethod.Fido2, - MfaMethod.Oidc, -] as const; - -export type MfaVerificationMethod = (typeof mfaVerificationMethods)[number]; diff --git a/new-ui/src/pages/full/ConfigureMfaPage/utils.ts b/new-ui/src/pages/full/ConfigureMfaPage/utils.ts index e0fe944d5..254332a76 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/utils.ts +++ b/new-ui/src/pages/full/ConfigureMfaPage/utils.ts @@ -4,7 +4,9 @@ import { type MfaMethodValue, } from '../../../shared/rust-api/types'; import { + CLIENT_AUTHORIZING_METHODS, CLIENT_CONFIGURABLE_METHODS, + type ClientAuthorizingMethod, type ClientConfigurableMethod, } from '../../../shared/utils/mfa'; import { @@ -12,8 +14,6 @@ import { type ConfigureMfaStepValue, MFA_WIZARD_STEPS, type MfaFactor, - type MfaVerificationMethod, - mfaVerificationMethods, } from './types'; /** Keyed on the shared list, so a new factor fails to compile until mapped here. */ @@ -64,13 +64,13 @@ export const isCodeMfaMethod = (method: MfaMethodValue): boolean => /** ordered by preference, not by the input */ export const verificationMethodsOf = ( availableMethods: MfaMethodValue[], -): MfaVerificationMethod[] => - mfaVerificationMethods.filter((method) => availableMethods.includes(method)); +): ClientAuthorizingMethod[] => + CLIENT_AUTHORIZING_METHODS.filter((method) => availableMethods.includes(method)); /** The fallback mails a code to the address on file, so email is the only way in. */ export const sessionVerificationMethodsOf = ( response: Pick, -): MfaVerificationMethod[] => +): ClientAuthorizingMethod[] => verificationMethodsOf( response.email_fallback ? [MfaMethod.Email] : response.available_methods, ); diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx index 6b54141e4..0b1b70cce 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureMfaVerify.tsx @@ -1,7 +1,7 @@ import { MfaMethod } from '../../../../shared/rust-api/types'; import { isPresent } from '../../../../shared/utils/isPresent'; +import type { ClientAuthorizingMethod } from '../../../../shared/utils/mfa'; import { useConfigureMfaStore } from '../hooks/useConfigureMfaStore'; -import type { MfaVerificationMethod } from '../types'; import { ConfigureSelectMethodsStep } from './ConfigureSelectMethodsStep/ConfigureSelectMethodsStep'; import { ConfigureSelectVerificationStep } from './ConfigureSelectVerificationStep/ConfigureSelectVerificationStep'; import { ConfigureVerifyEmailStep } from './ConfigureVerifyEmailStep/ConfigureVerifyEmailStep'; @@ -29,7 +29,7 @@ export const ConfigureMfaVerify = ({ onCancel, onSessionExpired }: Props) => { } // for the type only, a session with nothing to verify with is refused at start - const method: MfaVerificationMethod = + const method: ClientAuthorizingMethod = verificationMethod ?? candidates[0] ?? MfaMethod.Email; switch (method) { diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectMethodsStep/ConfigureSelectMethodsStep.tsx b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectMethodsStep/ConfigureSelectMethodsStep.tsx index 0c7ef3bcf..fe0eaa9a5 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectMethodsStep/ConfigureSelectMethodsStep.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectMethodsStep/ConfigureSelectMethodsStep.tsx @@ -26,6 +26,7 @@ import { isClientConfigurableMethod, isDesktopDrivableMethod, isMfaMethodAvailable, + isMfaMethodLocked, mfaStepsOf as locationMfaSteps, mfaToText, setupMethodsOf, @@ -46,6 +47,8 @@ type MethodRowState = { badge?: string; /** Why the row is disabled, shown on hover when the badge alone is too terse. */ hint: string | null; + /** The instance withholds the factor. */ + locked: boolean; }; interface Props { @@ -113,10 +116,11 @@ export const ConfigureSelectMethodsStep = ({ onCancel }: Props) => { const configured = accountConfigured.includes(method); const available = isMfaMethodAvailable(method, instance); const satisfied = configuredMethods.includes(method) && available; + const locked = isMfaMethodLocked(method, instance); let hint: string | null = null; let badge: string | undefined; - if (!available) { + if (!available || locked) { hint = `${mfaToText(method, instance ?? undefined)} is not available on this Defguard instance.`; badge = 'Unavailable'; } else if (disabled && !configured) { @@ -145,6 +149,7 @@ export const ConfigureSelectMethodsStep = ({ onCancel }: Props) => { disabled, badge, hint, + locked, }; }, [accountConfigured, configuredMethods, setupMethods, selected, instance], @@ -201,9 +206,11 @@ export const ConfigureSelectMethodsStep = ({ onCancel }: Props) => { useConfigureMfaStore.getState().selectMethods(selected); }, [selected, emailFallback, isLocationAware, locationSteps, isStepSatisfied]); - /** Without `explained` the row drops its badge and hint, leaving the reason to its section. */ - const renderMethod = (method: MfaMethodValue, explained: boolean) => { - const { checked, disabled, badge, hint } = describeMethod(method); + /** Without `sectionExplained` the row drops its badge and hint, leaving the reason to its section. */ + const renderMethod = (method: MfaMethodValue, sectionExplained: boolean) => { + const { checked, disabled, badge, hint, locked } = describeMethod(method); + // The "Not configurable" section text blames version incompatibility, so a withheld row says it. + const explained = sectionExplained || locked; const row = ( { const methods = useConfigureMfaStore((s) => s.verificationMethods); - const [selected, setSelected] = useState(methods[0]); + const [selected, setSelected] = useState( + methods[0], + ); return ( { }); }); +describe('isMfaMethodLocked', () => { + it('locks a client-configurable method Core leaves out of the setup methods', () => { + const instance = { + mfa_capabilities: capabilitiesOf([MfaMethod.Totp, MfaMethod.Email]), + }; + expect(isMfaMethodLocked(MfaMethod.Fido2, instance)).toBe(true); + expect(isMfaMethodLocked(MfaMethod.Totp, instance)).toBe(false); + expect(isMfaMethodLocked(MfaMethod.Oidc, instance)).toBe(false); + + const withoutTotp = { + mfa_capabilities: capabilitiesOf([MfaMethod.Email, MfaMethod.Fido2]), + }; + expect(isMfaMethodLocked(MfaMethod.Totp, withoutTotp)).toBe(true); + }); + + it('does not lock FIDO2 when Core offers it or cannot configure from the client', () => { + const offered = { + mfa_capabilities: capabilitiesOf([MfaMethod.Totp, MfaMethod.Fido2]), + }; + expect(isMfaMethodLocked(MfaMethod.Fido2, offered)).toBe(false); + expect(isMfaMethodLocked(MfaMethod.Fido2, { mfa_capabilities: null })).toBe(false); + expect(isMfaMethodLocked(MfaMethod.Fido2, undefined)).toBe(false); + }); +}); + describe('canSetUpMfaMethod', () => { it('needs both the client and the instance', () => { const instance = { diff --git a/new-ui/src/shared/utils/mfa.ts b/new-ui/src/shared/utils/mfa.ts index 3bd0b3f01..0942476c3 100644 --- a/new-ui/src/shared/utils/mfa.ts +++ b/new-ui/src/shared/utils/mfa.ts @@ -211,14 +211,32 @@ export const canSetUpMfaMethod = ( ): method is ClientConfigurableMethod => setupMethodsOf(instance).some((candidate) => candidate === method); -/** What this client can authorize an MFA configuration session with. Mirrors - * AUTHORIZING_METHODS in core mfa_config.rs. */ -const clientAuthorizingMethods: MfaMethodValue[] = [ +/** + * Every Core that reports capabilities knows all client-configurable methods, so one it leaves out + * of the setup methods is withheld from this instance. + */ +export const isMfaMethodLocked = ( + method: MfaMethodValue, + instance?: Pick | null, +): boolean => { + const coreSetupMethods = instance?.mfa_capabilities?.setup_methods; + return ( + isClientConfigurableMethod(method) && + isPresent(coreSetupMethods) && + !coreSetupMethods.includes(method) + ); +}; + +/** What this client can authorize an MFA configuration session with, most preferred first. + * Mirrors AUTHORIZING_METHODS in core mfa_config.rs. */ +export const CLIENT_AUTHORIZING_METHODS = [ MfaMethod.Totp, MfaMethod.Email, MfaMethod.Fido2, MfaMethod.Oidc, -]; +] as const; + +export type ClientAuthorizingMethod = (typeof CLIENT_AUTHORIZING_METHODS)[number]; export type MfaConfigInstance = Pick & MfaCapabilitiesInstance; @@ -230,7 +248,7 @@ export type MfaConfigInstance = Pick & export const canAuthorizeMfaConfig = (instance?: MfaConfigInstance | null): boolean => { const authorizeMethods = instance?.mfa_capabilities?.authorize_methods ?? []; const configuredMethods = instance?.mfa_configured_methods ?? []; - const holdsAuthorizer = clientAuthorizingMethods.some( + const holdsAuthorizer = CLIENT_AUTHORIZING_METHODS.some( (method) => authorizeMethods.includes(method) && configuredMethods.includes(method) && diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 08dd3c8a5..19d7b1805 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cli", "client-cli", "common", "client-proto", "core", "daemon", "fido2", "enterprise/posture", "enterprise/provisioning", "enterprise/config-sync", "enterprise/service-locations"] +members = ["cli", "client-cli", "common", "client-proto", "core", "daemon", "enterprise/fido2", "enterprise/posture", "enterprise/provisioning", "enterprise/config-sync", "enterprise/service-locations"] default-members = ["cli", "client-cli", "daemon", "."] [workspace.dependencies] @@ -131,7 +131,7 @@ redundant_closure = "warn" base64.workspace = true chrono.workspace = true defguard-cli.path = "client-cli" -defguard-client-fido2.path = "fido2" +defguard-client-fido2.path = "enterprise/fido2" defguard-client-proto.path = "client-proto" defguard-client-core.path = "core" defguard-client-posture.path = "enterprise/posture" diff --git a/src-tauri/deny.toml b/src-tauri/deny.toml index 940b12f23..8cca5d083 100644 --- a/src-tauri/deny.toml +++ b/src-tauri/deny.toml @@ -151,9 +151,6 @@ exceptions = [ { allow = [ "AGPL-3.0-or-later", ], crate = "defguard-client-core" }, - { allow = [ - "AGPL-3.0-or-later", - ], crate = "defguard-client-fido2" }, { allow = [ "AGPL-3.0-or-later", ], crate = "defguard-client-service" }, @@ -175,6 +172,9 @@ exceptions = [ { allow = [ "LicenseRef-Proprietary", ], crate = "defguard-client-service-locations" }, + { allow = [ + "LicenseRef-Proprietary", + ], crate = "defguard-client-fido2" }, ] # Some crates don't have (easily) machine readable licensing information, diff --git a/src-tauri/fido2/Cargo.toml b/src-tauri/enterprise/fido2/Cargo.toml similarity index 97% rename from src-tauri/fido2/Cargo.toml rename to src-tauri/enterprise/fido2/Cargo.toml index d37be7abc..1ff3cf7ef 100644 --- a/src-tauri/fido2/Cargo.toml +++ b/src-tauri/enterprise/fido2/Cargo.toml @@ -4,7 +4,7 @@ description = "FIDO2 security key ceremonies for the Defguard desktop client (Ta authors.workspace = true edition.workspace = true homepage.workspace = true -license-file.workspace = true +license = "LicenseRef-Proprietary" rust-version.workspace = true version.workspace = true diff --git a/src-tauri/fido2/src/hid.rs b/src-tauri/enterprise/fido2/src/hid.rs similarity index 100% rename from src-tauri/fido2/src/hid.rs rename to src-tauri/enterprise/fido2/src/hid.rs diff --git a/src-tauri/fido2/src/lib.rs b/src-tauri/enterprise/fido2/src/lib.rs similarity index 100% rename from src-tauri/fido2/src/lib.rs rename to src-tauri/enterprise/fido2/src/lib.rs diff --git a/src-tauri/fido2/src/protocol.rs b/src-tauri/enterprise/fido2/src/protocol.rs similarity index 100% rename from src-tauri/fido2/src/protocol.rs rename to src-tauri/enterprise/fido2/src/protocol.rs diff --git a/src-tauri/fido2/src/protocol/tests.rs b/src-tauri/enterprise/fido2/src/protocol/tests.rs similarity index 100% rename from src-tauri/fido2/src/protocol/tests.rs rename to src-tauri/enterprise/fido2/src/protocol/tests.rs diff --git a/src-tauri/fido2/src/windows/api.rs b/src-tauri/enterprise/fido2/src/windows/api.rs similarity index 100% rename from src-tauri/fido2/src/windows/api.rs rename to src-tauri/enterprise/fido2/src/windows/api.rs diff --git a/src-tauri/fido2/src/windows/convert.rs b/src-tauri/enterprise/fido2/src/windows/convert.rs similarity index 100% rename from src-tauri/fido2/src/windows/convert.rs rename to src-tauri/enterprise/fido2/src/windows/convert.rs diff --git a/src-tauri/fido2/src/windows/mod.rs b/src-tauri/enterprise/fido2/src/windows/mod.rs similarity index 100% rename from src-tauri/fido2/src/windows/mod.rs rename to src-tauri/enterprise/fido2/src/windows/mod.rs diff --git a/src-tauri/fido2/src/windows/options.rs b/src-tauri/enterprise/fido2/src/windows/options.rs similarity index 100% rename from src-tauri/fido2/src/windows/options.rs rename to src-tauri/enterprise/fido2/src/windows/options.rs