Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename useWebAuthn to useMfa and handle SSO challenges #47819

Merged
merged 3 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions web/packages/teleport/src/Account/Account.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,11 @@ test('adding an MFA device', async () => {
const user = userEvent.setup();
const ctx = createTeleportContext();
jest.spyOn(ctx.mfaService, 'fetchDevices').mockResolvedValue([testPasskey]);
jest
.spyOn(auth, 'getChallenge')
.mockResolvedValue({ webauthnPublicKey: null, totpChallenge: true });
jest.spyOn(auth, 'getChallenge').mockResolvedValue({
webauthnPublicKey: null,
totpChallenge: true,
ssoChallenge: null,
});
jest
.spyOn(auth, 'createNewWebAuthnDevice')
.mockResolvedValueOnce(dummyCredential);
Expand Down Expand Up @@ -325,9 +327,11 @@ test('removing an MFA method', async () => {
const user = userEvent.setup();
const ctx = createTeleportContext();
jest.spyOn(ctx.mfaService, 'fetchDevices').mockResolvedValue([testMfaMethod]);
jest
.spyOn(auth, 'getChallenge')
.mockResolvedValue({ webauthnPublicKey: null, totpChallenge: false });
jest.spyOn(auth, 'getChallenge').mockResolvedValue({
webauthnPublicKey: null,
totpChallenge: false,
ssoChallenge: null,
});
jest
.spyOn(auth, 'createPrivilegeTokenWithWebauthn')
.mockResolvedValueOnce('webauthn-privilege-token');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Box, Indicator } from 'design';

import * as stores from 'teleport/Console/stores/types';
import { Terminal, TerminalRef } from 'teleport/Console/DocumentSsh/Terminal';
import useWebAuthn from 'teleport/lib/useWebAuthn';
import { useMfa } from 'teleport/lib/useMfa';
import useKubeExecSession from 'teleport/Console/DocumentKubeExec/useKubeExecSession';

import Document from 'teleport/Console/Document';
Expand All @@ -39,11 +39,11 @@ export default function DocumentKubeExec({ doc, visible }: Props) {
const terminalRef = useRef<TerminalRef>();
const { tty, status, closeDocument, sendKubeExecData } =
useKubeExecSession(doc);
const webauthn = useWebAuthn(tty);
const mfa = useMfa(tty);
useEffect(() => {
// when switching tabs or closing tabs, focus on visible terminal
terminalRef.current?.focus();
}, [visible, webauthn.requested]);
}, [visible, mfa.requested]);
const theme = useTheme();

const terminal = (
Expand All @@ -63,13 +63,7 @@ export default function DocumentKubeExec({ doc, visible }: Props) {
<Indicator />
</Box>
)}
{webauthn.requested && (
<AuthnDialog
onContinue={webauthn.authenticate}
onCancel={closeDocument}
errorText={webauthn.errorText}
/>
)}
{mfa.requested && <AuthnDialog mfa={mfa} onCancel={closeDocument} />}

{status === 'waiting-for-exec-data' && (
<KubeExecData onExec={sendKubeExecData} onClose={closeDocument} />
Expand Down
16 changes: 5 additions & 11 deletions web/packages/teleport/src/Console/DocumentSsh/DocumentSsh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
import * as stores from 'teleport/Console/stores';

import AuthnDialog from 'teleport/components/AuthnDialog';
import useWebAuthn from 'teleport/lib/useWebAuthn';
import { useMfa } from 'teleport/lib/useMfa';

import Document from '../Document';

Expand All @@ -50,13 +50,13 @@ export default function DocumentSshWrapper(props: PropTypes) {
function DocumentSsh({ doc, visible }: PropTypes) {
const terminalRef = useRef<TerminalRef>();
const { tty, status, closeDocument, session } = useSshSession(doc);
const webauthn = useWebAuthn(tty);
const mfa = useMfa(tty);
const {
getMfaResponseAttempt,
getDownloader,
getUploader,
fileTransferRequests,
} = useFileTransfer(tty, session, doc, webauthn.addMfaToScpUrls);
} = useFileTransfer(tty, session, doc, mfa.addMfaToScpUrls);
const theme = useTheme();

function handleCloseFileTransfer() {
Expand All @@ -70,7 +70,7 @@ function DocumentSsh({ doc, visible }: PropTypes) {
useEffect(() => {
// when switching tabs or closing tabs, focus on visible terminal
terminalRef.current?.focus();
}, [visible, webauthn.requested]);
}, [visible, mfa.requested]);

const terminal = (
<Terminal
Expand All @@ -89,13 +89,7 @@ function DocumentSsh({ doc, visible }: PropTypes) {
<Indicator />
</Box>
)}
{webauthn.requested && (
<AuthnDialog
onContinue={webauthn.authenticate}
onCancel={closeDocument}
errorText={webauthn.errorText}
/>
)}
{mfa.requested && <AuthnDialog mfa={mfa} onCancel={closeDocument} />}
{status === 'initialized' && terminal}
<FileTransfer
FileTransferRequestsComponent={
Expand Down
18 changes: 8 additions & 10 deletions web/packages/teleport/src/DesktopSession/DesktopSession.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { NotificationItem } from 'shared/components/Notification';
import { throttle } from 'shared/utils/highbar';

import { TdpClient, TdpClientEvent } from 'teleport/lib/tdp';
import { makeDefaultMfaState } from 'teleport/lib/useMfa';

import { State } from './useDesktopSession';
import { DesktopSession } from './DesktopSession';
Expand Down Expand Up @@ -81,13 +82,7 @@ const props: State = {
canvasOnFocusOut: () => {},
clientOnClipboardData: async () => {},
setTdpConnection: () => {},
webauthn: {
errorText: '',
requested: false,
authenticate: () => {},
setState: () => {},
addMfaToScpUrls: false,
},
mfa: makeDefaultMfaState(),
showAnotherSessionActiveDialog: false,
setShowAnotherSessionActiveDialog: () => {},
alerts: [],
Expand Down Expand Up @@ -265,12 +260,15 @@ export const WebAuthnPrompt = () => (
writeState: 'granted',
}}
wsConnection={{ status: 'open' }}
webauthn={{
mfa={{
errorText: '',
requested: true,
authenticate: () => {},
setState: () => {},
setErrorText: () => null,
addMfaToScpUrls: false,
onWebauthnAuthenticate: () => null,
onSsoAuthenticate: () => null,
webauthnPublicKey: null,
ssoChallenge: null,
}}
/>
);
Expand Down
27 changes: 11 additions & 16 deletions web/packages/teleport/src/DesktopSession/DesktopSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import useDesktopSession, {
import TopBar from './TopBar';

import type { State, WebsocketAttempt } from './useDesktopSession';
import type { WebAuthnState } from 'teleport/lib/useWebAuthn';
import type { MfaState } from 'teleport/lib/useMfa';

export function DesktopSessionContainer() {
const state = useDesktopSession();
Expand All @@ -54,7 +54,7 @@ declare global {

export function DesktopSession(props: State) {
const {
webauthn,
mfa,
tdpClient,
username,
hostname,
Expand Down Expand Up @@ -105,15 +105,15 @@ export function DesktopSession(props: State) {
tdpConnection,
wsConnection,
showAnotherSessionActiveDialog,
webauthn
mfa
)
);
}, [
fetchAttempt,
tdpConnection,
wsConnection,
showAnotherSessionActiveDialog,
webauthn,
mfa,
]);

return (
Expand Down Expand Up @@ -144,7 +144,7 @@ export function DesktopSession(props: State) {
{screenState.screen === 'anotherSessionActive' && (
<AnotherSessionActiveDialog {...props} />
)}
{screenState.screen === 'mfa' && <MfaDialog webauthn={webauthn} />}
{screenState.screen === 'mfa' && <MfaDialog mfa={mfa} />}
{screenState.screen === 'alert dialog' && (
<AlertDialog screenState={screenState} />
)}
Expand Down Expand Up @@ -181,20 +181,15 @@ export function DesktopSession(props: State) {
);
}

const MfaDialog = ({ webauthn }: { webauthn: WebAuthnState }) => {
const MfaDialog = ({ mfa }: { mfa: MfaState }) => {
return (
<AuthnDialog
onContinue={webauthn.authenticate}
mfa={mfa}
onCancel={() => {
webauthn.setState(prevState => {
return {
...prevState,
errorText:
'This session requires multi factor authentication to continue. Please hit "Retry" and follow the prompts given by your browser to complete authentication.',
};
});
mfa.setErrorText(
'This session requires multi factor authentication to continue. Please hit "Retry" and follow the prompts given by your browser to complete authentication.'
);
}}
errorText={webauthn.errorText}
/>
);
};
Expand Down Expand Up @@ -282,7 +277,7 @@ const nextScreenState = (
tdpConnection: Attempt,
wsConnection: WebsocketAttempt,
showAnotherSessionActiveDialog: boolean,
webauthn: WebAuthnState
webauthn: MfaState
): ScreenState => {
// We always want to show the user the first alert that caused the session to fail/end,
// so if we're already showing an alert, don't change the screen.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useParams } from 'react-router';
import useAttempt from 'shared/hooks/useAttemptNext';

import { ButtonState } from 'teleport/lib/tdp';
import useWebAuthn from 'teleport/lib/useWebAuthn';
import { useMfa } from 'teleport/lib/useMfa';
import desktopService from 'teleport/services/desktops';
import userService from 'teleport/services/user';

Expand Down Expand Up @@ -130,7 +130,7 @@ export default function useDesktopSession() {
});
const tdpClient = clientCanvasProps.tdpClient;

const webauthn = useWebAuthn(tdpClient);
const mfa = useMfa(tdpClient);

const onShareDirectory = () => {
try {
Expand Down Expand Up @@ -205,7 +205,7 @@ export default function useDesktopSession() {
fetchAttempt,
tdpConnection,
wsConnection,
webauthn,
mfa,
setTdpConnection,
showAnotherSessionActiveDialog,
setShowAnotherSessionActiveDialog,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import React from 'react';

import { makeDefaultMfaState } from 'teleport/lib/useMfa';

import AuthnDialog, { Props } from './AuthnDialog';

export default {
Expand All @@ -26,12 +28,9 @@ export default {

export const Loaded = () => <AuthnDialog {...props} />;

export const Error = () => (
<AuthnDialog {...props} errorText="some error message" />
);
export const Error = () => <AuthnDialog {...props} />;

const props: Props = {
onContinue: () => null,
mfa: makeDefaultMfaState(),
onCancel: () => null,
errorText: '',
};
35 changes: 19 additions & 16 deletions web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,48 +18,51 @@

import React from 'react';
import Dialog, {
DialogFooter,
DialogHeader,
DialogTitle,
DialogContent,
} from 'design/Dialog';
import { Danger } from 'design/Alert';
import { Text, ButtonPrimary, ButtonSecondary } from 'design';
import { Text, ButtonPrimary, ButtonSecondary, Flex } from 'design';

export default function AuthnDialog({
onContinue,
onCancel,
errorText,
}: Props) {
import { MfaState } from 'teleport/lib/useMfa';

export default function AuthnDialog({ mfa, onCancel }: Props) {
return (
<Dialog dialogCss={() => ({ width: '400px' })} open={true}>
<Dialog dialogCss={() => ({ width: '500px' })} open={true}>
<DialogHeader style={{ flexDirection: 'column' }}>
<DialogTitle textAlign="center">
Multi-factor authentication
</DialogTitle>
</DialogHeader>
<DialogContent mb={6}>
{errorText && (
{mfa.errorText && (
<Danger mt={2} width="100%">
{errorText}
{mfa.errorText}
</Danger>
)}
<Text textAlign="center">
Re-enter your multi-factor authentication in the browser to continue.
</Text>
</DialogContent>
<DialogFooter textAlign="center">
<ButtonPrimary onClick={onContinue} autoFocus mr={3} width="130px">
{errorText ? 'Retry' : 'OK'}
<Flex textAlign="center" justifyContent="center">
{/* TODO (avatus) this will eventually be conditionally rendered based on what
type of challenges exist. For now, its only webauthn. */}
<ButtonPrimary
onClick={mfa.onWebauthnAuthenticate}
autoFocus
mr={3}
width="130px"
>
{mfa.errorText ? 'Retry' : 'OK'}
</ButtonPrimary>
<ButtonSecondary onClick={onCancel}>Cancel</ButtonSecondary>
</DialogFooter>
</Flex>
</Dialog>
);
}

export type Props = {
onContinue: () => void;
mfa: MfaState;
onCancel: () => void;
errorText: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { EventEmitter } from 'events';

import { WebauthnAssertionResponse } from 'teleport/services/auth';

class EventEmitterWebAuthnSender extends EventEmitter {
class EventEmitterMfaSender extends EventEmitter {
constructor() {
super();
}
Expand All @@ -31,4 +31,4 @@ class EventEmitterWebAuthnSender extends EventEmitter {
}
}

export { EventEmitterWebAuthnSender };
export { EventEmitterMfaSender };
4 changes: 2 additions & 2 deletions web/packages/teleport/src/lib/tdp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import init, {
} from 'teleport/ironrdp/pkg/ironrdp';

import { WebsocketCloseCode, TermEvent } from 'teleport/lib/term/enums';
import { EventEmitterWebAuthnSender } from 'teleport/lib/EventEmitterWebAuthnSender';
import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender';
import { AuthenticatedWebSocket } from 'teleport/lib/AuthenticatedWebSocket';

import Codec, {
Expand Down Expand Up @@ -93,7 +93,7 @@ export enum LogType {
// sending client commands, and receiving and processing server messages. Its creator is responsible for
// ensuring the websocket gets closed and all of its event listeners cleaned up when it is no longer in use.
// For convenience, this can be done in one fell swoop by calling Client.shutdown().
export default class Client extends EventEmitterWebAuthnSender {
export default class Client extends EventEmitterMfaSender {
protected codec: Codec;
protected socket: AuthenticatedWebSocket | undefined;
private socketAddr: string;
Expand Down
Loading
Loading