Skip to content

Commit 8648354

Browse files
committed
display posture check errors
1 parent 118cf8a commit 8648354

8 files changed

Lines changed: 120 additions & 12 deletions

File tree

new-ui/src/shared/components/LocationCard/LocationCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { LocationCardMfaMobileView } from './views/LocationCardMfaMobileView/Loc
2121
import { LocationCardMfaOidcView } from './views/LocationCardMfaOidcView/LocationCardMfaOidcView';
2222
import { LocationCardMfaSettings } from './views/LocationCardMfaSettings/LocationCardMfaSettings';
2323
import { LocationCardMfaTotpView } from './views/LocationCardMfaTotpView/LocationCardMfaTotpView';
24+
import { LocationCardPostureCheckFailView } from './views/LocationCardPostureCheckFailView/LocationCardPostureCheckFailView';
2425

2526
interface Props {
2627
location: LocationInfo;
@@ -39,7 +40,7 @@ const views: Record<LocationCardViewsValue, ReactNode> = {
3940
[LocationCardViews.MfaSettings]: <LocationCardMfaSettings />,
4041
[LocationCardViews.Connecting]: null,
4142
[LocationCardViews.Connected]: <ConnectedView />,
42-
[LocationCardViews.PostureCheckFail]: null,
43+
[LocationCardViews.PostureCheckFail]: <LocationCardPostureCheckFailView />,
4344
};
4445

4546
interface InnerProps {

new-ui/src/shared/components/LocationCard/api/startClientMfaSession.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import type {
99
export const CLIENT_MFA_ENDPOINT = 'api/v1/client-mfa';
1010

1111
export class MfaStartError extends Error {
12-
constructor(message: string) {
12+
public readonly status?: number;
13+
14+
constructor(message: string, status?: number) {
1315
super(message);
1416
this.name = 'MfaStartError';
17+
this.status = status;
1518
}
1619
}
1720

@@ -26,6 +29,9 @@ type MfaStartErrorResponse = {
2629
error?: string;
2730
};
2831

32+
export const shouldShowPostureError = (err: unknown, location: LocationInfo): boolean =>
33+
err instanceof MfaStartError && err.status === 403 && location.posture_check_required;
34+
2935
type StartClientMfaSessionParams = {
3036
instance: InstanceInfo;
3137
location: LocationInfo;
@@ -74,8 +80,14 @@ export const startClientMfaSession = async ({
7480
});
7581

7682
if (!response.ok) {
77-
const data = (await response.json()) as MfaStartErrorResponse;
78-
throw new MfaStartError(data.error ?? 'Failed to start MFA');
83+
let message = 'Failed to start MFA';
84+
try {
85+
const data = (await response.json()) as MfaStartErrorResponse;
86+
message = data.error ?? message;
87+
} catch {
88+
// Keep the response status even if the proxy sends a malformed error body.
89+
}
90+
throw new MfaStartError(message, response.status);
7991
}
8092

8193
return {

new-ui/src/shared/components/LocationCard/context/context.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ interface LocationCardContextValue {
88
instance: InstanceInfo;
99
currentView: LocationCardViewsValue;
1010
previousView: LocationCardViewsValue | null;
11+
postureError: string | null;
1112
setView: (view: LocationCardViewsValue) => void;
13+
setPostureError: (error: string | null) => void;
1214
startMfa: () => void;
1315
}
1416

@@ -34,6 +36,7 @@ export const LocationCardProvider = ({
3436
children,
3537
}: LocationCardProviderProps) => {
3638
const [previousView, setPreviousView] = useState<LocationCardViewsValue | null>(null);
39+
const [postureError, setPostureError] = useState<string | null>(null);
3740
const [currentView, setCurrentView] = useState<LocationCardViewsValue>(
3841
location.active ? LocationCardViews.Connected : LocationCardViews.Default,
3942
);
@@ -68,7 +71,9 @@ export const LocationCardProvider = ({
6871
value={{
6972
currentView,
7073
previousView,
74+
postureError,
7175
setView,
76+
setPostureError,
7277
location,
7378
instance,
7479
startMfa,

new-ui/src/shared/components/LocationCard/hooks/useMfaConnect.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { useCallback, useEffect, useRef, useState } from 'react';
55
import { api } from '../../../rust-api/api';
66
import { getInstancesQueryOptions } from '../../../rust-api/query';
77
import type { EdgeRequestHeaders } from '../../../rust-api/types';
8-
import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession';
8+
import {
9+
CLIENT_MFA_ENDPOINT,
10+
shouldShowPostureError,
11+
startClientMfaSession,
12+
} from '../api/startClientMfaSession';
913
import { useLocationCardContext } from '../context/context';
1014
import { LocationCardViews } from '../context/types';
1115

@@ -18,7 +22,7 @@ type MfaErrorResponse = {
1822
};
1923

2024
export const useMfaConnect = (method: 0 | 1) => {
21-
const { location, setView } = useLocationCardContext();
25+
const { location, setPostureError, setView } = useLocationCardContext();
2226

2327
const [token, setToken] = useState<string | null>(null);
2428
const [isStarting, setIsStarting] = useState(false);
@@ -62,6 +66,11 @@ export const useMfaConnect = (method: 0 | 1) => {
6266
setRequestHeaders(headers);
6367
setToken(response.token);
6468
} catch (err) {
69+
if (shouldShowPostureError(err, location)) {
70+
setPostureError(err.message);
71+
setView(LocationCardViews.PostureCheckFail);
72+
return;
73+
}
6574
setStartError(err instanceof Error ? err.message : 'Failed to start MFA');
6675
} finally {
6776
setIsStarting(false);

new-ui/src/shared/components/LocationCard/hooks/useMfaMobileConnect.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { useMutation } from '@tanstack/react-query';
33
import { error } from '@tauri-apps/plugin-log';
44
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
55
import { api } from '../../../rust-api/api';
6-
import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession';
6+
import {
7+
CLIENT_MFA_ENDPOINT,
8+
shouldShowPostureError,
9+
startClientMfaSession,
10+
} from '../api/startClientMfaSession';
711
import { useLocationCardContext } from '../context/context';
812
import { LocationCardViews } from '../context/types';
913

@@ -13,7 +17,7 @@ type TokenData = {
1317
};
1418

1519
export const useMfaMobileConnect = () => {
16-
const { location, instance, setView } = useLocationCardContext();
20+
const { location, instance, setPostureError, setView } = useLocationCardContext();
1721

1822
const [isStarting, setIsStarting] = useState(false);
1923
const [startError, setStartError] = useState<string | null>(null);
@@ -146,14 +150,19 @@ export const useMfaMobileConnect = () => {
146150

147151
setTokenData({ token: response.token, challenge: response.challenge });
148152
} catch (e) {
153+
if (shouldShowPostureError(e, location)) {
154+
setPostureError(e.message);
155+
setView(LocationCardViews.PostureCheckFail);
156+
return;
157+
}
149158
setStartError(
150159
e instanceof Error ? e.message : 'Failed to start mobile authentication',
151160
);
152161
error(`Mobile MFA start network error for location ${location.id}: ${e}`);
153162
} finally {
154163
setIsStarting(false);
155164
}
156-
}, [instance, location]);
165+
}, [instance, location, setPostureError, setView]);
157166

158167
const reset = useCallback(() => {
159168
if (wsRef.current) {

new-ui/src/shared/components/LocationCard/hooks/useMfaOidcConnect.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { error } from '@tauri-apps/plugin-log';
44
import { useCallback, useEffect, useRef, useState } from 'react';
55
import { api } from '../../../rust-api/api';
66
import { getInstancesQueryOptions } from '../../../rust-api/query';
7-
import { CLIENT_MFA_ENDPOINT, startClientMfaSession } from '../api/startClientMfaSession';
7+
import {
8+
CLIENT_MFA_ENDPOINT,
9+
shouldShowPostureError,
10+
startClientMfaSession,
11+
} from '../api/startClientMfaSession';
812
import { useLocationCardContext } from '../context/context';
913
import { LocationCardViews } from '../context/types';
1014

@@ -15,7 +19,7 @@ type MfaFinishResponse = { preshared_key: string };
1519
type MfaErrorResponse = { error: string };
1620

1721
export const useMfaOidcConnect = () => {
18-
const { location, setView } = useLocationCardContext();
22+
const { location, setPostureError, setView } = useLocationCardContext();
1923

2024
const [isStarting, setIsStarting] = useState(false);
2125
const [startError, setStartError] = useState<string | null>(null);
@@ -136,14 +140,19 @@ export const useMfaOidcConnect = () => {
136140
await api.openLink(`${instance.proxy_url}openid/mfa?token=${response.token}`);
137141
startPolling(response.token, instance.proxy_url, headers);
138142
} catch (e) {
143+
if (shouldShowPostureError(e, location)) {
144+
setPostureError(e.message);
145+
setView(LocationCardViews.PostureCheckFail);
146+
return;
147+
}
139148
setStartError(
140149
e instanceof Error ? e.message : 'Failed to start OIDC authentication',
141150
);
142151
error(`OIDC MFA start network error for location ${location.id}: ${e}`);
143152
} finally {
144153
setIsStarting(false);
145154
}
146-
}, [instance, location, startPolling, stopPolling]);
155+
}, [instance, location, setPostureError, setView, startPolling, stopPolling]);
147156

148157
return { start, isStarting, startError, isPolling, pollError };
149158
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import './style.scss';
2+
import { ThemeSpacing } from '../../../../types';
3+
import { Button } from '../../../Button/Button';
4+
import { ButtonVariant } from '../../../Button/types';
5+
import { Controls } from '../../../Controls/Controls';
6+
import { Divider } from '../../../Divider/Divider';
7+
import { IconKind } from '../../../Icon';
8+
import { IconButton } from '../../../IconButton/IconButton';
9+
import { IconButtonVariant } from '../../../IconButton/types';
10+
import { SizedBox } from '../../../SizedBox/SizedBox';
11+
import { LocationViewHeader } from '../../components/LocationViewHeader/LocationViewHeader';
12+
import { useLocationCardContext } from '../../context/context';
13+
import { LocationCardViews } from '../../context/types';
14+
15+
export const LocationCardPostureCheckFailView = () => {
16+
const { postureError, previousView, setPostureError, setView } =
17+
useLocationCardContext();
18+
19+
const retryView =
20+
previousView && previousView !== LocationCardViews.PostureCheckFail
21+
? previousView
22+
: LocationCardViews.Default;
23+
24+
const goToDefault = () => {
25+
setPostureError(null);
26+
setView(LocationCardViews.Default);
27+
};
28+
29+
const tryAgain = () => {
30+
setPostureError(null);
31+
setView(retryView);
32+
};
33+
34+
return (
35+
<div className="location-card-posture-check-fail-view">
36+
<Divider spacing={ThemeSpacing.Md} />
37+
<LocationViewHeader title="Posture check failed">
38+
<p className="error">
39+
{postureError ?? 'Your device did not pass posture check.'}
40+
</p>
41+
</LocationViewHeader>
42+
<SizedBox height={ThemeSpacing.Xl} />
43+
<Controls>
44+
<IconButton
45+
variant={IconButtonVariant.BigSelected}
46+
icon={IconKind.ArrowBig}
47+
iconRotation="left"
48+
onClick={goToDefault}
49+
/>
50+
<div className="right">
51+
<Button text="Try again" variant={ButtonVariant.Primary} onClick={tryAgain} />
52+
</div>
53+
</Controls>
54+
</div>
55+
);
56+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.location-card-posture-check-fail-view {
2+
.location-card-view-header {
3+
p.error {
4+
color: var(--fg-critical);
5+
}
6+
}
7+
}

0 commit comments

Comments
 (0)