diff --git a/web/packages/shared/components/AuthorizeDeviceWeb/AuthorizeDeviceWeb.tsx b/web/packages/shared/components/AuthorizeDeviceWeb/AuthorizeDeviceWeb.tsx
index ad5e866dbd901..9a46336ea0efb 100644
--- a/web/packages/shared/components/AuthorizeDeviceWeb/AuthorizeDeviceWeb.tsx
+++ b/web/packages/shared/components/AuthorizeDeviceWeb/AuthorizeDeviceWeb.tsx
@@ -56,11 +56,14 @@ export const PassthroughPage = () => {
useEffect(() => {
window.open(deviceTrustAuthorize);
- // the deviceWebToken is only valid for 60 seconds, so we can forward
+ // the deviceWebToken is only valid for 5 minutes, so we can forward
// to the dashboard
- const id = window.setTimeout(() => {
- history.push(cfg.routes.root, true);
- }, 1000 * 60 /* 1 minute */);
+ const id = window.setTimeout(
+ () => {
+ history.push(cfg.routes.root, true);
+ },
+ 1000 * 60 * 5 /* 5 minutes */
+ );
return () => window.clearTimeout(id);
}, [deviceTrustAuthorize]);
diff --git a/web/packages/teleterm/src/deepLinks.test.ts b/web/packages/teleterm/src/deepLinks.test.ts
index 90cfb1e17c9a5..43843be1454a1 100644
--- a/web/packages/teleterm/src/deepLinks.test.ts
+++ b/web/packages/teleterm/src/deepLinks.test.ts
@@ -213,26 +213,6 @@ describe('parseDeepLink followed by makeDeepLinkWithSafeInput gives the same res
'teleport://cluster.example.com/connect_my_computer',
'teleport://alice@cluster.example.com/connect_my_computer',
'teleport://alice.bobson%40example.com@cluster.example.com:1337/connect_my_computer',
- ];
-
- test.each(inputs)('%s', input => {
- const parseResult = parseDeepLink(input);
- expect(parseResult).toMatchObject({ status: 'success' });
- const { url } = parseResult as DeepLinkParseResultSuccess;
- const deepLink = makeDeepLinkWithSafeInput({
- proxyHost: url.host,
- path: url.pathname,
- username: url.username,
- searchParams: {},
- });
- expect(deepLink).toEqual(input);
- });
-});
-
-describe('"parseDeepLink followed by makeDeepLinkWithSafeInput gives the same result"', () => {
- const inputs: string[] = [
- 'teleport://cluster.example.com/connect_my_computer',
- 'teleport://alice@cluster.example.com/connect_my_computer',
'teleport://alice@cluster.example.com/authenticate_web_device?id=123&token=234',
];
diff --git a/web/packages/teleterm/src/deepLinks.ts b/web/packages/teleterm/src/deepLinks.ts
index cc6b06a7243da..977514bc0949a 100644
--- a/web/packages/teleterm/src/deepLinks.ts
+++ b/web/packages/teleterm/src/deepLinks.ts
@@ -84,8 +84,6 @@ export function parseDeepLink(rawUrl: string): DeepLinkParseResult {
};
}
- // TODO (avatus): remove when authenticate web device case is implemented
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { host, hostname, port, username, pathname, searchParams } = whatwgURL;
const baseUrl = {
host,
diff --git a/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx b/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx
index 4937c8b1884b6..2778d0efcfdb7 100644
--- a/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx
+++ b/web/packages/teleterm/src/ui/ModalsHost/ModalsHost.tsx
@@ -32,6 +32,7 @@ import { assertUnreachable } from '../utils';
import { UsageData } from './modals/UsageData';
import { UserJobRole } from './modals/UserJobRole';
import { ReAuthenticate } from './modals/ReAuthenticate';
+import { AuthenticateWebDevice } from './modals/AuthenticateWebDevice/AuthenticateWebDevice';
export default function ModalsHost() {
const { modalsService } = useAppContext();
@@ -51,6 +52,15 @@ export default function ModalsHost() {
function renderDialog(dialog: Dialog, handleClose: () => void) {
switch (dialog.kind) {
+ case 'device-trust-authorize': {
+ return (
+
+ );
+ }
case 'cluster-connect': {
return (
.
+ */
+
+import React from 'react';
+
+import { makeRootCluster } from 'teleterm/services/tshd/testHelpers';
+import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
+
+import { AuthenticateWebDevice } from './AuthenticateWebDevice';
+
+export default {
+ title: 'Teleterm/ModalsHost/AuthenticateWebDevice',
+};
+
+export const Dialog = () => (
+
+ {}}
+ onAuthorize={async () => {}}
+ />
+
+);
diff --git a/web/packages/teleterm/src/ui/ModalsHost/modals/AuthenticateWebDevice/AuthenticateWebDevice.tsx b/web/packages/teleterm/src/ui/ModalsHost/modals/AuthenticateWebDevice/AuthenticateWebDevice.tsx
new file mode 100644
index 0000000000000..f6662a694d03f
--- /dev/null
+++ b/web/packages/teleterm/src/ui/ModalsHost/modals/AuthenticateWebDevice/AuthenticateWebDevice.tsx
@@ -0,0 +1,76 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import Alert from 'design/Alert';
+import { ButtonPrimary, ButtonSecondary, Text } from 'design';
+import Dialog, { DialogContent } from 'design/Dialog';
+import Flex from 'design/Flex';
+import { useAsync } from 'shared/hooks/useAsync';
+
+import { useAppContext } from 'teleterm/ui/appContextProvider';
+import { RootClusterUri, routing } from 'teleterm/ui/uri';
+
+type Props = {
+ rootClusterUri: RootClusterUri;
+ onClose(): void;
+ onAuthorize(): Promise;
+};
+
+export const AuthenticateWebDevice = ({
+ onAuthorize,
+ onClose,
+ rootClusterUri,
+}: Props) => {
+ const [attempt, run] = useAsync(async () => {
+ await onAuthorize();
+ onClose();
+ });
+ const { clustersService } = useAppContext();
+ const clusterName =
+ clustersService.findCluster(rootClusterUri)?.name ||
+ routing.parseClusterName(rootClusterUri);
+
+ return (
+
+ );
+};
diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts
index 2027b4d93f5b2..4f7a623ff201c 100644
--- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts
+++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts
@@ -146,6 +146,31 @@ export class ClustersService extends ImmutableStore
this.usageService.captureUserLogin(params.clusterUri, params.providerType);
}
+ async authenticateWebDevice(
+ rootClusterUri: uri.RootClusterUri,
+ {
+ id,
+ token,
+ }: {
+ id: string;
+ token: string;
+ }
+ ) {
+ return await this.client.authenticateWebDevice({
+ rootClusterUri,
+ deviceWebToken: {
+ id,
+ token,
+ // empty fields, ignore
+ webSessionId: '',
+ browserIp: '',
+ browserUserAgent: '',
+ user: '',
+ expectedDeviceIds: [],
+ },
+ });
+ }
+
async loginPasswordless(
params: types.LoginPasswordlessParams,
abortSignal: CloneableAbortSignal
diff --git a/web/packages/teleterm/src/ui/services/deepLinks/deepLinksService.ts b/web/packages/teleterm/src/ui/services/deepLinks/deepLinksService.ts
index f14d97cb1c4cc..3e88cb249106b 100644
--- a/web/packages/teleterm/src/ui/services/deepLinks/deepLinksService.ts
+++ b/web/packages/teleterm/src/ui/services/deepLinks/deepLinksService.ts
@@ -16,10 +16,10 @@
* along with this program. If not, see .
*/
-import { DeepURL } from 'shared/deepLinks';
+import { AuthenticateWebDeviceDeepURL, DeepURL } from 'shared/deepLinks';
import { DeepLinkParseResult } from 'teleterm/deepLinks';
-import { routing } from 'teleterm/ui/uri';
+import { RootClusterUri, routing } from 'teleterm/ui/uri';
import { assertUnreachable } from 'teleterm/ui/utils';
import { RuntimeSettings } from 'teleterm/types';
import { ClustersService } from 'teleterm/ui/services/clusters';
@@ -27,6 +27,7 @@ import { WorkspacesService } from 'teleterm/ui/services/workspacesService';
import { ModalsService } from 'teleterm/ui/services/modals';
import { NotificationsService } from 'teleterm/ui/services/notifications';
+const confirmPath = 'webapi/devices/webconfirm';
export class DeepLinksService {
constructor(
private runtimeSettings: RuntimeSettings,
@@ -88,17 +89,54 @@ export class DeepLinksService {
break;
}
case '/authenticate_web_device': {
- // TODO (avatus): add authenticate device web confirmation dialog
- // this case is a stub and will not be reached
+ await this.askAuthorizeDeviceTrust(result.url);
break;
}
}
}
+ /**
+ * askAuthorizeDeviceTrust opens a dialog asking the user if they'd like to authorize
+ * a web session with device trust. If confirmed, the web session will be upgraded and the
+ * user will be directed back to the web UI
+ */
+ private async askAuthorizeDeviceTrust(
+ url: AuthenticateWebDeviceDeepURL
+ ): Promise {
+ const { id, token } = url.searchParams;
+
+ const result = await this.loginAndSetActiveWorkspace(url);
+ if (!result.isAtDesiredWorkspace) {
+ return;
+ }
+
+ const { rootClusterUri } = result;
+ const rootCluster = this.clustersService.findCluster(rootClusterUri);
+
+ this.modalsService.openRegularDialog({
+ kind: 'device-trust-authorize',
+ rootClusterUri,
+ onCancel: () => {},
+ onAuthorize: async () => {
+ const result = await this.clustersService.authenticateWebDevice(
+ rootClusterUri,
+ {
+ id,
+ token,
+ }
+ );
+ // open url to confirm the token. This endpoint verifies the token and "upgrades"
+ // the web session and redirects to "/web"
+ window.open(
+ `https://${rootCluster.proxyHost}/${confirmPath}?id=${result.response.confirmationToken.id}&token=${result.response.confirmationToken.token}`
+ );
+ },
+ });
+ }
+
/**
* launchConnectMyComputer opens a Connect My Computer tab in the cluster workspace that the URL
- * points to. If the relevant cluster is not in the app yet, it opens a login dialog with the
- * cluster address and username prefilled from the URL.
+ * points to.
*/
private async launchConnectMyComputer(url: DeepURL): Promise {
if (this.runtimeSettings.platform === 'win32') {
@@ -108,6 +146,32 @@ export class DeepLinksService {
return;
}
+ const result = await this.loginAndSetActiveWorkspace(url);
+
+ if (!result.isAtDesiredWorkspace) {
+ return;
+ }
+
+ const { rootClusterUri } = result;
+
+ this.workspacesService
+ .getWorkspaceDocumentService(rootClusterUri)
+ .openConnectMyComputerDocument({ rootClusterUri });
+ }
+
+ /**
+ * loginAndSetActiveWorkspace will set the relevant cluster if it is in the app and, if not,
+ * it opens a login dialog with cluster address and username prefilled from the URL.
+ */
+ private async loginAndSetActiveWorkspace(url: DeepURL): Promise<
+ | {
+ isAtDesiredWorkspace: false;
+ }
+ | {
+ isAtDesiredWorkspace: true;
+ rootClusterUri: RootClusterUri;
+ }
+ > {
const rootClusterId = url.hostname;
const clusterAddress = url.host;
const prefill = {
@@ -131,7 +195,9 @@ export class DeepLinksService {
});
if (canceled) {
- return;
+ return {
+ isAtDesiredWorkspace: false,
+ };
}
}
@@ -144,12 +210,6 @@ export class DeepLinksService {
prefill
);
- if (!isAtDesiredWorkspace) {
- return;
- }
-
- this.workspacesService
- .getWorkspaceDocumentService(rootClusterUri)
- .openConnectMyComputerDocument({ rootClusterUri });
+ return { isAtDesiredWorkspace, rootClusterUri };
}
}
diff --git a/web/packages/teleterm/src/ui/services/modals/modalsService.ts b/web/packages/teleterm/src/ui/services/modals/modalsService.ts
index 74996fe606458..ce79ef9133e8f 100644
--- a/web/packages/teleterm/src/ui/services/modals/modalsService.ts
+++ b/web/packages/teleterm/src/ui/services/modals/modalsService.ts
@@ -165,6 +165,13 @@ export interface DialogDocumentsReopen {
onCancel?(): void;
}
+export interface DialogDeviceTrustAuthorize {
+ kind: 'device-trust-authorize';
+ rootClusterUri: RootClusterUri;
+ onAuthorize(): Promise;
+ onCancel(): void;
+}
+
export interface DialogUsageData {
kind: 'usage-data';
onAllow(): void;
@@ -206,6 +213,7 @@ export type Dialog =
| DialogClusterConnect
| DialogClusterLogout
| DialogDocumentsReopen
+ | DialogDeviceTrustAuthorize
| DialogUsageData
| DialogUserJobRole
| DialogResourceSearchErrors