From 3eaca227ad4bd7998bf3e6f00e62052a61fc86fc Mon Sep 17 00:00:00 2001 From: Michael Myers Date: Wed, 23 Oct 2024 16:23:43 -0500 Subject: [PATCH] Add SSO option to AuthDialog --- .../components/AuthnDialog/AuthnDialog.tsx | 44 ++++++++----- .../teleport/src/lib/EventEmitterMfaSender.ts | 14 ++++- web/packages/teleport/src/lib/term/tty.ts | 28 ++++++++- web/packages/teleport/src/lib/useMfa.ts | 62 +++++++++++++++++-- .../teleport/src/services/auth/makeMfa.ts | 21 ++++--- 5 files changed, 141 insertions(+), 28 deletions(-) diff --git a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx index 05685c0d6a3eb..0ea0bfbcbf9ce 100644 --- a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx +++ b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx @@ -23,13 +23,16 @@ import Dialog, { DialogContent, } from 'design/Dialog'; import { Danger } from 'design/Alert'; -import { Text, ButtonPrimary, ButtonSecondary, Flex } from 'design'; +import { Text, ButtonSecondary, Flex, ButtonLink } from 'design'; + +import { guessProviderType } from 'shared/components/ButtonSso'; +import { SSOIcon } from 'shared/components/ButtonSso/ButtonSso'; import { MfaState } from 'teleport/lib/useMfa'; export default function AuthnDialog({ mfa, onCancel }: Props) { return ( - ({ width: '500px' })} open={true}> + ({ width: '400px' })} open={true}> Multi-factor authentication @@ -45,18 +48,31 @@ export default function AuthnDialog({ mfa, onCancel }: Props) { Re-enter your multi-factor authentication in the browser to continue. - - {/* TODO (avatus) this will eventually be conditionally rendered based on what - type of challenges exist. For now, its only webauthn. */} - - {mfa.errorText ? 'Retry' : 'OK'} - - Cancel + + {mfa.ssoChallenge && ( + + + {mfa.ssoChallenge.device.displayName} + + )} + {mfa.webauthnPublicKey && ( + + Passkey/Hardware Key + + )} + + Cancel + ); diff --git a/web/packages/teleport/src/lib/EventEmitterMfaSender.ts b/web/packages/teleport/src/lib/EventEmitterMfaSender.ts index 68eae3367f6ea..2117197e72195 100644 --- a/web/packages/teleport/src/lib/EventEmitterMfaSender.ts +++ b/web/packages/teleport/src/lib/EventEmitterMfaSender.ts @@ -18,13 +18,25 @@ import { EventEmitter } from 'events'; -import { WebauthnAssertionResponse } from 'teleport/services/auth'; +import { + MfaChallengeResponse, + WebauthnAssertionResponse, +} from 'teleport/services/auth'; class EventEmitterMfaSender extends EventEmitter { constructor() { super(); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + sendChallengeResponse(data: MfaChallengeResponse) { + throw new Error('Not implemented'); + } + + // TODO (avatus) DELETE IN 19 + /** + * @deprecated Use sendChallengeResponse instead. + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars sendWebAuthn(data: WebauthnAssertionResponse) { throw new Error('Not implemented'); diff --git a/web/packages/teleport/src/lib/term/tty.ts b/web/packages/teleport/src/lib/term/tty.ts index 2eb11957b8fbd..62bbca2f7c22f 100644 --- a/web/packages/teleport/src/lib/term/tty.ts +++ b/web/packages/teleport/src/lib/term/tty.ts @@ -19,7 +19,10 @@ import Logger from 'shared/libs/logger'; import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender'; -import { WebauthnAssertionResponse } from 'teleport/services/auth'; +import { + MfaChallengeResponse, + WebauthnAssertionResponse, +} from 'teleport/services/auth'; import { AuthenticatedWebSocket } from 'teleport/lib/AuthenticatedWebSocket'; import { EventType, TermEvent, WebsocketCloseCode } from './enums'; @@ -80,6 +83,29 @@ class Tty extends EventEmitterMfaSender { this.socket.send(bytearray.buffer); } + sendChallengeResponse(data: MfaChallengeResponse) { + // we want to have the backend listen on a single message type + // for any responses. so our data will look like data.webauthn, data.sso, etc + // but to be backward compatible, we need to still spread the existing webauthn only fields + // as "top level" fields so old proxies can still respond to webauthn challenges. + // in 19, we can just pass "data" without this extra step + // TODO (avatus): DELETE IN 19 + const backwardCompatibleData = { + ...data.webauthn_response, + ...data, + }; + console.log({ data }); + const encoded = this._proto.encodeChallengeResponse( + JSON.stringify(backwardCompatibleData) + ); + const bytearray = new Uint8Array(encoded); + this.socket.send(bytearray); + } + + // TODO (avatus) DELETE IN 19 + /** + * @deprecated Use sendChallengeResponse instead. + */ sendWebAuthn(data: WebauthnAssertionResponse) { const encoded = this._proto.encodeChallengeResponse(JSON.stringify(data)); const bytearray = new Uint8Array(encoded); diff --git a/web/packages/teleport/src/lib/useMfa.ts b/web/packages/teleport/src/lib/useMfa.ts index 8d55cf4c73f75..3c4f552696255 100644 --- a/web/packages/teleport/src/lib/useMfa.ts +++ b/web/packages/teleport/src/lib/useMfa.ts @@ -41,11 +41,64 @@ export function useMfa(emitterSender: EventEmitterMfaSender): MfaState { totpChallenge: false, }); - // TODO (avatus), this is stubbed for types but will not be called - // until SSO as MFA backend is in. + function clearChallenges() { + setState(prevState => ({ + ...prevState, + totpChallenge: false, + webauthnPublicKey: null, + ssoChallenge: null, + })); + } + + // open a broadcast channel if sso challenge exists so it can listen + // for a confirmation response token + useEffect(() => { + if (!state.ssoChallenge) { + return; + } + + const channel = new BroadcastChannel(state.ssoChallenge.channelId); + + function handleMessage(e: MessageEvent<{ mfaToken: string }>) { + if (!state.ssoChallenge) { + return; + } + + emitterSender.sendChallengeResponse({ + sso_response: { + requestId: state.ssoChallenge.requestId, + token: e.data.mfaToken, + }, + }); + clearChallenges(); + } + + channel.addEventListener('message', handleMessage); + + return () => { + channel.removeEventListener('message', handleMessage); + channel.close(); + }; + }, [state, emitterSender, state.ssoChallenge]); + function onSsoAuthenticate() { - // eslint-disable-next-line no-console - console.error('not yet implemented'); + if (!state.ssoChallenge) { + setState(prevState => ({ + ...prevState, + errorText: 'Invalid or missing SSO challenge', + })); + return; + } + + // try to center the screen + const width = 1045; + const height = 550; + const left = (screen.width - width) / 2; + const top = (screen.height - height) / 2; + + // these params will open a tiny window. + const params = `width=${width},height=${height},left=${left},top=${top}`; + window.open(state.ssoChallenge.redirectUrl, '_blank', params); } function onWebauthnAuthenticate() { @@ -83,6 +136,7 @@ export function useMfa(emitterSender: EventEmitterMfaSender): MfaState { setState(prevState => ({ ...prevState, + addMfaToScpUrls: true, ssoChallenge, webauthnPublicKey, totpChallenge, diff --git a/web/packages/teleport/src/services/auth/makeMfa.ts b/web/packages/teleport/src/services/auth/makeMfa.ts index 506cca4a874c7..163bcd007b041 100644 --- a/web/packages/teleport/src/services/auth/makeMfa.ts +++ b/web/packages/teleport/src/services/auth/makeMfa.ts @@ -56,7 +56,7 @@ export function makeMfaRegistrationChallenge(json): MfaRegistrationChallenge { // - allowCredentials[i].id export function makeMfaAuthenticateChallenge(json): MfaAuthenticateChallenge { const challenge = typeof json === 'string' ? JSON.parse(json) : json; - const { sso_challenge, webauthn_challenge } = challenge; + const { sso_challenge, webauthn_challenge, totp_challenge } = challenge; const webauthnPublicKey = webauthn_challenge?.publicKey; if (webauthnPublicKey) { @@ -73,13 +73,8 @@ export function makeMfaAuthenticateChallenge(json): MfaAuthenticateChallenge { } return { - ssoChallenge: sso_challenge - ? { - redirectUrl: sso_challenge.redirect_url, - requestId: sso_challenge.request_id, - } - : null, - totpChallenge: json.totp_challenge, + ssoChallenge: sso_challenge, + totpChallenge: totp_challenge, webauthnPublicKey: webauthnPublicKey, }; } @@ -146,6 +141,11 @@ export function makeWebauthnAssertionResponse(res): WebauthnAssertionResponse { }; } +export type SsoChallengeResponse = { + requestId: string; + token: string; +}; + export type WebauthnAssertionResponse = { id: string; type: string; @@ -160,3 +160,8 @@ export type WebauthnAssertionResponse = { userHandle: string; }; }; + +export type MfaChallengeResponse = { + webauthn_response?: WebauthnAssertionResponse; + sso_response?: SsoChallengeResponse; +};