Skip to content

Commit

Permalink
Rename useWebAuthn to useMfa and handle SSO challenges (#47819)
Browse files Browse the repository at this point in the history
* Rename useWebauthn to useMfa

* add ssoChallenge to mfa requests/responses

* Update callsites and tests
  • Loading branch information
avatus authored Oct 23, 2024
1 parent 802f2bc commit 8829746
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 200 deletions.
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

0 comments on commit 8829746

Please sign in to comment.