From 1975b5448cd42b6afe8b7d94a192dc3e749c2b74 Mon Sep 17 00:00:00 2001 From: joerger Date: Tue, 1 Aug 2023 11:48:25 -0700 Subject: [PATCH 1/4] Support TELEPORT_HEADLESS_SKIP_CONFIRM env flag in teleport connect. --- .../src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx | 6 ++++++ .../src/ui/services/headlessAuthn/headlessAuthnService.ts | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx index cf495ce7a4a24..a0e64e49012c0 100644 --- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx +++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx @@ -50,6 +50,12 @@ export function HeadlessPrompt({ }: HeadlessPromptProps) { const [waitForMfa, setWaitForMfa] = useState(false); + // skip to MFA confirmation step. + if (process.env.TELEPORT_HEADLESS_SKIP_CONFIRM) { + setWaitForMfa(true); + onApprove(); + } + return ( ({ diff --git a/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts b/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts index d20ffef588214..e3c46b2a2fae6 100644 --- a/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts +++ b/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts @@ -31,7 +31,11 @@ export class HeadlessAuthenticationService { request: SendPendingHeadlessAuthenticationRequest, onRequestCancelled: (callback: () => void) => void ): Promise { - this.mainProcessClient.forceFocusWindow(); + // If the user wants to skip the confirmation step, then don't force the window. + // Instead, they can just tap their blinking yubikey with the window in the background. + if (!process.env.TELEPORT_HEADLESS_SKIP_CONFIRM) { + this.mainProcessClient.forceFocusWindow(); + } return new Promise(resolve => { const { closeDialog } = this.modalsService.openImportantDialog({ From 5633daadb0e1c2d7215b4d665c878aba62a470fd Mon Sep 17 00:00:00 2001 From: joerger Date: Tue, 1 Aug 2023 13:00:44 -0700 Subject: [PATCH 2/4] Replace reject button with cancel button that persists through approval state. This makes it possible to reject a headless authentication when it skips the initial confirmation step. --- .../HeadlessAuthn/HeadlessAuthentication.tsx | 6 +- .../HeadlessPrompt/HeadlessPrompt.story.tsx | 1 + .../HeadlessPrompt/HeadlessPrompt.tsx | 92 +++++++++++-------- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx index 3055740aba833..05d917033da60 100644 --- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx +++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx @@ -73,13 +73,11 @@ export function HeadlessAuthentication(props: HeadlessAuthenticationProps) { cluster={cluster} clientIp={props.clientIp} onApprove={handleHeadlessApprove} + abortApproval={refAbortCtrl.current.abort} onReject={handleHeadlessReject} headlessAuthenticationId={props.headlessAuthenticationId} updateHeadlessStateAttempt={updateHeadlessStateAttempt} - onCancel={() => { - props.onCancel(); - refAbortCtrl.current.abort(); - }} + onCancel={props.onCancel} /> ); } diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.story.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.story.tsx index 66d79f760dade..35012c03b1c5d 100644 --- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.story.tsx +++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.story.tsx @@ -31,6 +31,7 @@ export const Story = () => ( cluster={makeRootCluster()} clientIp="localhost" onApprove={async () => {}} + abortApproval={() => {}} onReject={async () => {}} updateHeadlessStateAttempt={makeEmptyAttempt()} onCancel={() => {}} diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx index a0e64e49012c0..d30e9f8e41e9f 100644 --- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx +++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx @@ -33,6 +33,7 @@ export type HeadlessPromptProps = { cluster: tsh.Cluster; clientIp: string; onApprove(): Promise; + abortApproval(): void; onReject(): Promise; headlessAuthenticationId: string; updateHeadlessStateAttempt: Attempt; @@ -43,6 +44,7 @@ export function HeadlessPrompt({ cluster, clientIp, onApprove, + abortApproval, onReject, headlessAuthenticationId, updateHeadlessStateAttempt, @@ -69,7 +71,14 @@ export function HeadlessPrompt({ Headless command on {cluster.name} - + { + abortApproval(); + onCancel(); + }} + > @@ -78,52 +87,55 @@ export function HeadlessPrompt({ {updateHeadlessStateAttempt.statusText} )} - - {!waitForMfa && ( - <> - - - Someone initiated a headless command from {clientIp}. -
- If it was not you, click Reject and contact your administrator. -
- - Request ID: {headlessAuthenticationId} - -
- - { - e.preventDefault(); - setWaitForMfa(true); - onApprove(); - }} - > - Approve - - { - e.preventDefault(); - onReject(); - }} - > - Reject - - - - )} + + + Someone initiated a headless command from {clientIp}. +
+ If it was not you, click Cancel and contact your administrator. +
+ + Request ID: {headlessAuthenticationId} + +
{waitForMfa && ( Complete MFA verification to approve the Headless Login. - + { + abortApproval(); + onReject(); + }} + /> )} + {!waitForMfa && ( + + { + e.preventDefault(); + setWaitForMfa(true); + onApprove(); + }} + > + Approve + + { + e.preventDefault(); + onReject(); + }} + > + Cancel + + + )}
); } From 0d9f770202b70f44757721e0eb056152baf40ddf Mon Sep 17 00:00:00 2001 From: joerger Date: Wed, 9 Aug 2023 13:36:52 -0700 Subject: [PATCH 3/4] Add headlessSkipConfirm config option. --- .../teleterm/src/services/config/appConfigSchema.ts | 4 ++++ .../src/ui/HeadlessAuthn/HeadlessAuthentication.tsx | 2 ++ .../HeadlessPrompt/HeadlessPrompt.story.tsx | 1 + .../ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx | 4 +++- web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx | 1 + web/packages/teleterm/src/ui/appContext.ts | 3 ++- .../ui/services/headlessAuthn/headlessAuthnService.ts | 9 +++++++-- .../teleterm/src/ui/services/modals/modalsService.ts | 1 + 8 files changed, 21 insertions(+), 4 deletions(-) diff --git a/web/packages/teleterm/src/services/config/appConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts index a18f81469dbdf..1d96f571cf0bf 100644 --- a/web/packages/teleterm/src/services/config/appConfigSchema.ts +++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts @@ -114,6 +114,10 @@ export const createAppConfigSchema = (platform: Platform) => { .boolean() .default(false) .describe('Enables sharing the computer.'), + 'feature.headlessSkipConfirm': z + .boolean() + .default(false) + .describe('Skips the confirmation tab for headless login approval.'), }); }; diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx index 05d917033da60..08da0c1aa668b 100644 --- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx +++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx @@ -29,6 +29,7 @@ interface HeadlessAuthenticationProps { rootClusterUri: RootClusterUri; headlessAuthenticationId: string; clientIp: string; + skipConfirm: boolean; onCancel(): void; onSuccess(): void; } @@ -72,6 +73,7 @@ export function HeadlessAuthentication(props: HeadlessAuthenticationProps) { ( {}} abortApproval={() => {}} onReject={async () => {}} diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx index d30e9f8e41e9f..6b941a15b3910 100644 --- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx +++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx @@ -32,6 +32,7 @@ import type * as tsh from 'teleterm/services/tshd/types'; export type HeadlessPromptProps = { cluster: tsh.Cluster; clientIp: string; + skipConfirm: boolean; onApprove(): Promise; abortApproval(): void; onReject(): Promise; @@ -43,6 +44,7 @@ export type HeadlessPromptProps = { export function HeadlessPrompt({ cluster, clientIp, + skipConfirm, onApprove, abortApproval, onReject, @@ -53,7 +55,7 @@ export function HeadlessPrompt({ const [waitForMfa, setWaitForMfa] = useState(false); // skip to MFA confirmation step. - if (process.env.TELEPORT_HEADLESS_SKIP_CONFIRM) { + if (skipConfirm) { setWaitForMfa(true); onApprove(); } diff --git a/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx b/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx index 139516f240162..255c9fd565d20 100644 --- a/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx +++ b/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx @@ -139,6 +139,7 @@ function renderDialog(dialog: Dialog, handleClose: () => void) { rootClusterUri={dialog.rootClusterUri} headlessAuthenticationId={dialog.headlessAuthenticationId} clientIp={dialog.headlessAuthenticationClientIp} + skipConfirm={dialog.skipConfirm} onCancel={() => { handleClose(); dialog.onCancel(); diff --git a/web/packages/teleterm/src/ui/appContext.ts b/web/packages/teleterm/src/ui/appContext.ts index 9e57201994a98..ae4b61410f92a 100644 --- a/web/packages/teleterm/src/ui/appContext.ts +++ b/web/packages/teleterm/src/ui/appContext.ts @@ -143,7 +143,8 @@ export default class AppContext implements IAppContext { this.headlessAuthenticationService = new HeadlessAuthenticationService( mainProcessClient, this.modalsService, - tshClient + tshClient, + this.configService ); } diff --git a/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts b/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts index e3c46b2a2fae6..a1987737c2308 100644 --- a/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts +++ b/web/packages/teleterm/src/ui/services/headlessAuthn/headlessAuthnService.ts @@ -17,6 +17,7 @@ import { SendPendingHeadlessAuthenticationRequest } from 'teleterm/services/tshdEvents'; import { MainProcessClient } from 'teleterm/types'; import { ModalsService } from 'teleterm/ui/services/modals'; +import { ConfigService } from 'teleterm/services/config'; import type * as types from 'teleterm/services/tshd/types'; @@ -24,16 +25,19 @@ export class HeadlessAuthenticationService { constructor( private mainProcessClient: MainProcessClient, private modalsService: ModalsService, - private tshClient: types.TshClient + private tshClient: types.TshClient, + private configService: ConfigService, ) {} sendPendingHeadlessAuthentication( request: SendPendingHeadlessAuthenticationRequest, onRequestCancelled: (callback: () => void) => void ): Promise { + const skipConfirm = this.configService.get('feature.headlessSkipConfirm').value + // If the user wants to skip the confirmation step, then don't force the window. // Instead, they can just tap their blinking yubikey with the window in the background. - if (!process.env.TELEPORT_HEADLESS_SKIP_CONFIRM) { + if (!skipConfirm) { this.mainProcessClient.forceFocusWindow(); } @@ -43,6 +47,7 @@ export class HeadlessAuthenticationService { rootClusterUri: request.rootClusterUri, headlessAuthenticationId: request.headlessAuthenticationId, headlessAuthenticationClientIp: request.headlessAuthenticationClientIp, + skipConfirm: skipConfirm, onSuccess: () => resolve(), onCancel: () => resolve(), }); diff --git a/web/packages/teleterm/src/ui/services/modals/modalsService.ts b/web/packages/teleterm/src/ui/services/modals/modalsService.ts index 1ee4ff057a351..c7136022b3701 100644 --- a/web/packages/teleterm/src/ui/services/modals/modalsService.ts +++ b/web/packages/teleterm/src/ui/services/modals/modalsService.ts @@ -198,6 +198,7 @@ export interface DialogHeadlessAuthentication { rootClusterUri: RootClusterUri; headlessAuthenticationId: string; headlessAuthenticationClientIp: string; + skipConfirm: boolean; onSuccess(): void; onCancel(): void; } From c64fdd1cf5eb46f7972b6b7373718f14163e0dda Mon Sep 17 00:00:00 2001 From: joerger Date: Thu, 10 Aug 2023 11:48:26 -0700 Subject: [PATCH 4/4] Apply changes from CR. --- .../teleterm/src/services/config/appConfigSchema.ts | 6 ++++-- .../src/ui/HeadlessAuthn/HeadlessAuthentication.tsx | 8 +++++++- .../ui/HeadlessAuthn/HeadlessPrompt/HeadlessPrompt.tsx | 10 +++------- .../ui/services/headlessAuthn/headlessAuthnService.ts | 6 +++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/web/packages/teleterm/src/services/config/appConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts index 1d96f571cf0bf..264a6b8e4b5c9 100644 --- a/web/packages/teleterm/src/services/config/appConfigSchema.ts +++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts @@ -114,10 +114,12 @@ export const createAppConfigSchema = (platform: Platform) => { .boolean() .default(false) .describe('Enables sharing the computer.'), - 'feature.headlessSkipConfirm': z + 'headless.skipConfirm': z .boolean() .default(false) - .describe('Skips the confirmation tab for headless login approval.'), + .describe( + 'Skips the confirmation prompt for headless login approval and instead prompts for WebAuthn immediately.' + ), }); }; diff --git a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx index 08da0c1aa668b..fc73d9b1c3482 100644 --- a/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx +++ b/web/packages/teleterm/src/ui/HeadlessAuthn/HeadlessAuthentication.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import { useAsync } from 'shared/hooks/useAsync'; @@ -69,6 +69,12 @@ export function HeadlessAuthentication(props: HeadlessAuthenticationProps) { } } + useEffect(() => { + if (props.skipConfirm && updateHeadlessStateAttempt.status === '') { + handleHeadlessApprove(); + } + }, []); + return ( void) => void ): Promise { - const skipConfirm = this.configService.get('feature.headlessSkipConfirm').value + const skipConfirm = this.configService.get('headless.skipConfirm').value; // If the user wants to skip the confirmation step, then don't force the window. // Instead, they can just tap their blinking yubikey with the window in the background. @@ -47,7 +47,7 @@ export class HeadlessAuthenticationService { rootClusterUri: request.rootClusterUri, headlessAuthenticationId: request.headlessAuthenticationId, headlessAuthenticationClientIp: request.headlessAuthenticationClientIp, - skipConfirm: skipConfirm, + skipConfirm: skipConfirm, onSuccess: () => resolve(), onCancel: () => resolve(), });