diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 0f163d54..dbd015a4 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -1,4 +1,4 @@ vulnerabilities: - id: GHSA-wrw7-89jp-8q8g - expired_at: 2026-09-30 + expired_at: 2026-10-30 statement: "glib is a transitive dependency of Tauri which we cannot update ourselves. Waiting for tauri to finish migration to gtk4-rs: https://github.com/tauri-apps/tauri/issues/12563" diff --git a/new-ui/src/pages/full/OverviewPage/components/ConnectModal/ConnectModal.tsx b/new-ui/src/pages/full/OverviewPage/components/ConnectModal/ConnectModal.tsx index 01c7cadd..ac5eca07 100644 --- a/new-ui/src/pages/full/OverviewPage/components/ConnectModal/ConnectModal.tsx +++ b/new-ui/src/pages/full/OverviewPage/components/ConnectModal/ConnectModal.tsx @@ -32,13 +32,16 @@ export const ConnectModal = () => { useShallow((s) => [s.view, s.visible, s.location, s.stepIndex, s.stepPlan]), ); + const { instances } = useAppData(); + const instance = instances.find((entry) => entry.id === location?.instance_id); + const stepCount = isPresent(location) ? mfaStepCount(location) : 0; const stepMethod = stepPlan[stepIndex]; const isOnMfaStepView = isPresent(stepMethod) && view === mfaMethodToConnectModalView(stepMethod); const stepLabel = stepCount > 1 && isOnMfaStepView - ? `Step ${stepIndex + 1}/${stepCount}: ${mfaToText(stepMethod)}` + ? `Step ${stepIndex + 1}/${stepCount}: ${mfaToText(stepMethod, instance)}` : null; const isOpen = isPresent(view) && isPresent(location) && visible; diff --git a/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaOidc/ConnectModalMfaOidc.tsx b/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaOidc/ConnectModalMfaOidc.tsx index 2de80b46..7b4844a3 100644 --- a/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaOidc/ConnectModalMfaOidc.tsx +++ b/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaOidc/ConnectModalMfaOidc.tsx @@ -5,11 +5,17 @@ import { useShallow } from 'zustand/shallow'; import { Button } from '../../../../../../../shared/components/Button/Button'; import { ButtonVariant } from '../../../../../../../shared/components/Button/types'; import { Checkbox } from '../../../../../../../shared/components/Checkbox/Checkbox'; +import { ExternalProviderButton } from '../../../../../../../shared/components/ExternalProviderButton/ExternalProviderButton'; import { SizedBox } from '../../../../../../../shared/components/SizedBox/SizedBox'; +import { useAppData } from '../../../../../../../shared/providers/AppDataContext'; import { api } from '../../../../../../../shared/rust-api/api'; import { getAppConfigQueryOptions } from '../../../../../../../shared/rust-api/query'; import { ThemeSpacing } from '../../../../../../../shared/types'; import { isPresent } from '../../../../../../../shared/utils/isPresent'; +import { + findOpenIdProvider, + openIdProviderName, +} from '../../../../../../../shared/utils/mfa'; import { ConnectModalPostureCheckLoading } from '../../components/ConnectModalPostureCheckLoading/ConnectModalPostureCheckLoading'; import { ConnectModalView } from '../../hooks/types'; import { useConnectModal } from '../../hooks/useConnectModal'; @@ -22,6 +28,9 @@ export const ConnectModalMfaOidc = () => { const [perviousView, location, initAutoStart] = useConnectModal( useShallow((s) => [s.perviousView, s.location, s.autoStartOpenId]), ); + const { instances } = useAppData(); + const instance = instances.find((entry) => entry.id === location?.instance_id); + const providerName = openIdProviderName(instance); const { start, isStarting, startError, isPolling, pollError } = useConnectModalMfaOidc({ autoStart: initAutoStart, @@ -60,8 +69,7 @@ export const ConnectModalMfaOidc = () => {
{screen === 'idle' && (

- To connect to the VPN, authenticate via your OpenID provider. A browser window - will open for you to sign in. + {`To connect to the VPN, sign in with ${providerName} by clicking button below. A browser window will open and redirect you to ${providerName} for authentication.`}

)} {screen === 'polling' && ( @@ -72,9 +80,9 @@ export const ConnectModalMfaOidc = () => { {screen === 'error' &&

{errorMessage}

}
{screen !== 'error' && ( - + ); +}; diff --git a/new-ui/src/shared/components/ExternalProviderButton/assets/custom.svg b/new-ui/src/shared/components/ExternalProviderButton/assets/custom.svg new file mode 100644 index 00000000..be6d7c6d --- /dev/null +++ b/new-ui/src/shared/components/ExternalProviderButton/assets/custom.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/new-ui/src/shared/components/ExternalProviderButton/assets/google.svg b/new-ui/src/shared/components/ExternalProviderButton/assets/google.svg new file mode 100644 index 00000000..0210acb3 --- /dev/null +++ b/new-ui/src/shared/components/ExternalProviderButton/assets/google.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/new-ui/src/shared/components/ExternalProviderButton/assets/jumpcloud.svg b/new-ui/src/shared/components/ExternalProviderButton/assets/jumpcloud.svg new file mode 100644 index 00000000..bb3ac3fc --- /dev/null +++ b/new-ui/src/shared/components/ExternalProviderButton/assets/jumpcloud.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/new-ui/src/shared/components/ExternalProviderButton/assets/microsoft.svg b/new-ui/src/shared/components/ExternalProviderButton/assets/microsoft.svg new file mode 100644 index 00000000..18b689fc --- /dev/null +++ b/new-ui/src/shared/components/ExternalProviderButton/assets/microsoft.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/new-ui/src/shared/components/ExternalProviderButton/assets/okta.svg b/new-ui/src/shared/components/ExternalProviderButton/assets/okta.svg new file mode 100644 index 00000000..eb19a930 --- /dev/null +++ b/new-ui/src/shared/components/ExternalProviderButton/assets/okta.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/new-ui/src/shared/components/ExternalProviderButton/style.scss b/new-ui/src/shared/components/ExternalProviderButton/style.scss new file mode 100644 index 00000000..06c1c4bc --- /dev/null +++ b/new-ui/src/shared/components/ExternalProviderButton/style.scss @@ -0,0 +1,152 @@ +.external-provider-button { + --btn-font: var(--t-body-xs-600); + --btn-size: 36px; + --border-color: transparent; + --bg-color: transparent; + --bg-image: none; + --text-color: var(--fg-white-100); + --loader-bg: transparent; + --loader-track: transparent; + --loader-part: transparent; + --shadow: none; + + height: var(--btn-size); + border: 1px solid var(--border-color); + box-sizing: border-box; + border-radius: var(--radius-md); + background-color: var(--bg-color); + background-image: var(--bg-image); + box-shadow: var(--shadow); + color: var(--text-color); + min-height: var(--btn-size); + min-width: 0; + padding: 0 var(--spacing-lg); + position: relative; + user-select: none; + display: grid; + grid-template-columns: 16px auto; + grid-template-rows: 1fr; + align-items: center; + column-gap: var(--spacing-sm); + text-decoration: none !important; + justify-content: center; + + @include animate(background-color, border-color, opacity, box-shadow); + + &:not(.disabled, .loading) { + cursor: pointer; + } + + &.disabled { + cursor: not-allowed; + pointer-events: none; + } + + .text { + font: var(--btn-font); + color: inherit; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + @include animate(color); + } + + &.size-primary { + --btn-size: 36px; + } + + &.size-big { + --btn-size: 44px; + + border-radius: 100px; + } + + &.variant-primary { + --bg-color: var(--bg-white-100); + --text-color: var(--fg-action); + --loader-bg: var(--bg-white-100); + --loader-track: var(--c-saturated-blue-200); + --loader-part: var(--fg-action); + --shadow: 0 4px 5px 0 rgb(53 84 179 / 7%); + + &:hover { + --bg-image: linear-gradient(180deg, var(--c-white-100) 0%, #d3ddfb 100%); + } + + &.disabled { + --bg-color: var(--bg-white-20); + --text-color: var(--fg-white-40); + --shadow: none; + } + } + + &.variant-secondary { + --bg-color: var(--bg-white-10); + --text-color: var(--fg-white-100); + --loader-bg: var(--bg-white-10); + + &:hover { + --bg-color: var(--bg-white-20); + } + + &.disabled { + --bg-color: var(--bg-white-5); + --text-color: var(--fg-white-40); + } + } + + &.variant-critical { + --bg-color: var(--bg-critical); + --text-color: var(--fg-white-100); + --loader-bg: var(--bg-critical-disabled); + + &:hover { + --bg-color: var(--bg-critical-faded); + } + + &.disabled { + --bg-color: var(--bg-critical-disabled); + --text-color: var(--fg-white-40); + } + } + + &.variant-outlined { + --bg-color: transparent; + --border-color: var(--border-default); + --loader-bg: var(--bg-white-10); + --text-color: var(--fg-white-100); + + &:hover { + --bg-color: var(--bg-white-5); + } + + &.disabled { + --border-color: var(--border-disabled); + --text-color: var(--fg-white-40); + } + } + + .loader-overlay { + --spinner-track: var(--loader-track); + --spinner-indicator: var(--loader-part); + + background-color: var(--loader-bg); + border: none; + border-radius: inherit; + position: absolute; + inset: 0; + pointer-events: none; + width: 100%; + height: 100%; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + } +} + +a:has(.external-provider-button) { + text-decoration: none; +} diff --git a/new-ui/src/shared/components/ExternalProviderButton/types.ts b/new-ui/src/shared/components/ExternalProviderButton/types.ts new file mode 100644 index 00000000..a7515c7f --- /dev/null +++ b/new-ui/src/shared/components/ExternalProviderButton/types.ts @@ -0,0 +1,19 @@ +import type { ButtonHTMLAttributes, HTMLAttributes, Ref } from 'react'; + +type DefaultButtonProps = ButtonHTMLAttributes; + +type ButtonVariant = 'primary' | 'secondary' | 'critical' | 'outlined'; + +type ButtonSize = 'primary' | 'big'; + +export type ExternalProviderButtonProps = { + text: string; + variant?: ButtonVariant; + size?: ButtonSize; + type?: DefaultButtonProps['type']; + provider: 'microsoft' | 'google' | 'okta' | 'jumpcloud' | 'custom'; + testId?: string; + disabled?: boolean; + loading?: boolean; + ref?: Ref; +} & HTMLAttributes; diff --git a/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx b/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx index bd48eec5..02540c85 100644 --- a/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx +++ b/new-ui/src/shared/components/LocationCard/components/LocationCardMfaEdit/LocationCardMfaEdit.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import { TooltipContent } from '../../../../providers/tooltip/TooltipContent'; import { TooltipProvider } from '../../../../providers/tooltip/TooltipContext'; import { TooltipTrigger } from '../../../../providers/tooltip/TooltipTrigger'; -import type { LocationInfo } from '../../../../rust-api/types'; +import type { InstanceInfo, LocationInfo } from '../../../../rust-api/types'; import { ConnectionAbility, type ConnectionAbilityValue, @@ -19,6 +19,7 @@ import { IconButtonVariant } from '../../../IconButton/types'; interface Props { variant: 'compact' | 'full'; location: LocationInfo; + instance?: InstanceInfo; /** Supplied by the caller - the tray card reads it off the LocationCard context, * the desktop card computes it with `useConnectionAbility`. */ connectionAbility: ConnectionAbilityValue; @@ -30,6 +31,7 @@ const CONFIGURE_REQUIRED_TOOLTIP = export const LocationCardMfaEdit = ({ location, + instance, onEdit, variant, connectionAbility, @@ -38,7 +40,7 @@ export const LocationCardMfaEdit = ({ const label = stepCount > 1 ? mfaStepsToText(stepCount) - : mfaToText(resolveMfaStepPlan(location)[0]); + : mfaToText(resolveMfaStepPlan(location)[0], instance); // `Configurable` stays editable, configuring a factor unblocks it. const canEdit = connectionAbility !== ConnectionAbility.Unavailable; 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 b537a689..aaeb76cd 100644 --- a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx @@ -2,13 +2,20 @@ import './style.scss'; import clsx from 'clsx'; import type { HTMLProps, MouseEventHandler } from 'react'; import { mfaMethodIcon } from '../../../../consts'; -import { MfaMethod, type MfaMethodValue } from '../../../../rust-api/types'; -import { mfaToText } from '../../../../utils/mfa'; +import { + type InstanceInfo, + MfaMethod, + type MfaMethodValue, +} from '../../../../rust-api/types'; +import { isPresent } from '../../../../utils/isPresent'; +import { findOpenIdProvider, mfaToText } from '../../../../utils/mfa'; import { Icon } from '../../../Icon'; import checkboxSrc from './assets/checkbox.svg'; +import { openIdProviderIcon } from './consts'; interface Props { factor: MfaMethodValue; + instance?: Pick; selected?: boolean; active?: boolean; isDefault?: boolean; @@ -20,6 +27,7 @@ interface Props { export const MfaSelector = ({ factor, + instance, onClick, containerProps, selected = false, @@ -30,6 +38,8 @@ export const MfaSelector = ({ }: Props) => { const isMobileOnly = factor === MfaMethod.Biometric; const showCheckbox = isSelectable && selected; + const openIdProvider = + factor === MfaMethod.Oidc ? findOpenIdProvider(instance) : undefined; return (
} {!showCheckbox && (
- + {isPresent(openIdProvider) && ( + + )} + {!isPresent(openIdProvider) && ( + + )}
)}
-

{mfaToText(factor)}

+

{mfaToText(factor, instance)}

{(isMobileOnly || !configured) && ( diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/google-white.svg b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/google-white.svg new file mode 100644 index 00000000..2c82b96e --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/google-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/jumpcloud-white.svg b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/jumpcloud-white.svg new file mode 100644 index 00000000..d034bfe7 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/jumpcloud-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/microsoft-white.svg b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/microsoft-white.svg new file mode 100644 index 00000000..983f8519 --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/microsoft-white.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/okta-white.svg b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/okta-white.svg new file mode 100644 index 00000000..81c5456c --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/assets/okta-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/consts.ts b/new-ui/src/shared/components/LocationCard/components/MfaSelector/consts.ts new file mode 100644 index 00000000..c926d06e --- /dev/null +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/consts.ts @@ -0,0 +1,12 @@ +import { OpenIdProvider, type OpenIdProviderValue } from '../../../../consts'; +import googleWhiteImage from './assets/google-white.svg'; +import jumpcloudWhiteImage from './assets/jumpcloud-white.svg'; +import microsoftWhiteImage from './assets/microsoft-white.svg'; +import oktaWhiteImage from './assets/okta-white.svg'; + +export const openIdProviderIcon: Record = { + [OpenIdProvider.Microsoft]: microsoftWhiteImage, + [OpenIdProvider.Google]: googleWhiteImage, + [OpenIdProvider.Okta]: oktaWhiteImage, + [OpenIdProvider.JumpCloud]: jumpcloudWhiteImage, +}; diff --git a/new-ui/src/shared/components/LocationCard/context/context.tsx b/new-ui/src/shared/components/LocationCard/context/context.tsx index 9940b167..f739666c 100644 --- a/new-ui/src/shared/components/LocationCard/context/context.tsx +++ b/new-ui/src/shared/components/LocationCard/context/context.tsx @@ -189,7 +189,7 @@ export const LocationCardProvider = ({ const stepMethod = stepPlan[stepIndex]; const showStepLabel = isMultiStep && isPresent(stepMethod); const stepLabel = showStepLabel - ? `Step ${stepIndex + 1}/${mfaSteps.length}: ${mfaToText(stepMethod)}` + ? `Step ${stepIndex + 1}/${mfaSteps.length}: ${mfaToText(stepMethod, instance)}` : null; return ( diff --git a/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx b/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx index eea1b521..f70750e4 100644 --- a/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx @@ -51,6 +51,7 @@ export const DefaultView = () => { { setView(LocationCardViews.MfaSettings); diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx index ac264fe2..6b39854e 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaOidcView/LocationCardMfaOidcView.tsx @@ -4,11 +4,13 @@ import { useCallback, useEffect, useState } from 'react'; import { api } from '../../../../rust-api/api'; import { getAppConfigQueryOptions } from '../../../../rust-api/query'; import { ThemeSpacing } from '../../../../types'; +import { findOpenIdProvider, openIdProviderName } from '../../../../utils/mfa'; import { Button } from '../../../Button/Button'; import { ButtonVariant } from '../../../Button/types'; import { Checkbox } from '../../../Checkbox/Checkbox'; import { Controls } from '../../../Controls/Controls'; import { Divider } from '../../../Divider/Divider'; +import { ExternalProviderButton } from '../../../ExternalProviderButton/ExternalProviderButton'; import { IconKind } from '../../../Icon'; import { IconButton } from '../../../IconButton/IconButton'; import { IconButtonVariant } from '../../../IconButton/types'; @@ -22,8 +24,9 @@ type Screen = 'idle' | 'polling' | 'error'; export const LocationCardMfaOidcView = () => { const { data: appConfig } = useQuery(getAppConfigQueryOptions); - const { setView, setPostureError, autoConnectOpenid, stepLabel } = + const { setView, setPostureError, autoConnectOpenid, stepLabel, instance } = useLocationCardContext(); + const providerName = openIdProviderName(instance); const { start, isStarting, startError, isPolling, pollError } = useMfaOidcConnect(autoConnectOpenid); const [screen, setScreen] = useState('idle'); @@ -54,8 +57,7 @@ export const LocationCardMfaOidcView = () => { {screen === 'idle' && (

- To connect to the VPN, authenticate via your OpenID provider. A browser window - will open for you to sign in. + {`To connect to the VPN, sign in with ${providerName} by clicking button below. A browser window will open and redirect you to ${providerName} for authentication.`}

)} {screen === 'polling' && ( @@ -70,7 +72,7 @@ export const LocationCardMfaOidcView = () => { { void api.setAppConfig( { @@ -91,9 +93,9 @@ export const LocationCardMfaOidcView = () => { />
{screen !== 'error' && ( -