Skip to content

Commit

Permalink
Add SSO option to AuthDialog
Browse files Browse the repository at this point in the history
  • Loading branch information
avatus committed Oct 23, 2024
1 parent e28c316 commit 3eaca22
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 28 deletions.
44 changes: 30 additions & 14 deletions web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Dialog dialogCss={() => ({ width: '500px' })} open={true}>
<Dialog dialogCss={() => ({ width: '400px' })} open={true}>
<DialogHeader style={{ flexDirection: 'column' }}>
<DialogTitle textAlign="center">
Multi-factor authentication
Expand All @@ -45,18 +48,31 @@ export default function AuthnDialog({ mfa, onCancel }: Props) {
Re-enter your multi-factor authentication in the browser to continue.
</Text>
</DialogContent>
<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>
<Flex textAlign="center" width="100%" flexDirection="column" gap={3}>
{mfa.ssoChallenge && (
<ButtonSecondary
onClick={mfa.onSsoAuthenticate}
autoFocus
gap={2}
block
>
<SSOIcon
type={guessProviderType(
mfa.ssoChallenge.device.displayName,
mfa.ssoChallenge.device.connectorType
)}
/>
{mfa.ssoChallenge.device.displayName}
</ButtonSecondary>
)}
{mfa.webauthnPublicKey && (
<ButtonSecondary onClick={mfa.onWebauthnAuthenticate} autoFocus block>
Passkey/Hardware Key
</ButtonSecondary>
)}
<ButtonLink block onClick={onCancel}>
Cancel
</ButtonLink>
</Flex>
</Dialog>
);
Expand Down
14 changes: 13 additions & 1 deletion web/packages/teleport/src/lib/EventEmitterMfaSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
28 changes: 27 additions & 1 deletion web/packages/teleport/src/lib/term/tty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
62 changes: 58 additions & 4 deletions web/packages/teleport/src/lib/useMfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -83,6 +136,7 @@ export function useMfa(emitterSender: EventEmitterMfaSender): MfaState {

setState(prevState => ({
...prevState,
addMfaToScpUrls: true,
ssoChallenge,
webauthnPublicKey,
totpChallenge,
Expand Down
21 changes: 13 additions & 8 deletions web/packages/teleport/src/services/auth/makeMfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
};
}
Expand Down Expand Up @@ -146,6 +141,11 @@ export function makeWebauthnAssertionResponse(res): WebauthnAssertionResponse {
};
}

export type SsoChallengeResponse = {
requestId: string;
token: string;
};

export type WebauthnAssertionResponse = {
id: string;
type: string;
Expand All @@ -160,3 +160,8 @@ export type WebauthnAssertionResponse = {
userHandle: string;
};
};

export type MfaChallengeResponse = {
webauthn_response?: WebauthnAssertionResponse;
sso_response?: SsoChallengeResponse;
};

0 comments on commit 3eaca22

Please sign in to comment.