diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx
index 7cfb7dfd93b7d..75bbc151d6181 100644
--- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx
+++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEKSCluster.test.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import { act, fireEvent, render, screen } from 'design/utils/testing';
+import { act, fireEvent, render, screen, waitFor } from 'design/utils/testing';
import cfg from 'teleport/config';
import { ComponentWrapper } from 'teleport/Discover/Fixtures/kubernetes';
@@ -179,6 +179,7 @@ describe('test EnrollEksCluster.tsx', () => {
expect(integrationService.enrollEksClusters).not.toHaveBeenCalled();
});
+
test('auto enroll disabled, enrolls cluster', async () => {
jest.spyOn(integrationService, 'fetchEksClusters').mockResolvedValue({
clusters: mockEKSClusters,
@@ -197,7 +198,9 @@ describe('test EnrollEksCluster.tsx', () => {
act(() => screen.getByRole('radio').click());
- act(() => screen.getByText('Enroll EKS Cluster').click());
+ await waitFor(() => {
+ screen.getByText('Enroll EKS Cluster').click();
+ });
expect(discoveryService.createDiscoveryConfig).not.toHaveBeenCalled();
expect(KubeService.prototype.fetchKubernetes).toHaveBeenCalledTimes(1);
diff --git a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
index 909f2c885605d..4f53a90e25a9d 100644
--- a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
+++ b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
@@ -70,11 +70,12 @@ function makeTokenResource(token: JoinToken): Resource {
export const JoinTokens = () => {
const ctx = useTeleport();
+
const [creatingToken, setCreatingToken] = useState(false);
const [editingToken, setEditingToken] = useState(null);
const [tokenToDelete, setTokenToDelete] = useState(null);
const [joinTokensAttempt, runJoinTokensAttempt, setJoinTokensAttempt] =
- useAsync(async () => await ctx.joinTokenService.fetchJoinTokens());
+ useAsync(() => ctx.joinTokenService.fetchJoinTokens());
const resources = useResources(
joinTokensAttempt.data?.items.map(makeTokenResource) || [],
diff --git a/web/packages/teleport/src/MFAContext/MFAContext.tsx b/web/packages/teleport/src/MFAContext/MFAContext.tsx
new file mode 100644
index 0000000000000..fcb83604bb900
--- /dev/null
+++ b/web/packages/teleport/src/MFAContext/MFAContext.tsx
@@ -0,0 +1,54 @@
+import { createContext, PropsWithChildren, useCallback, useState } from 'react';
+
+import AuthnDialog from 'teleport/components/AuthnDialog';
+import { useMfa } from 'teleport/lib/useMfa';
+import api from 'teleport/services/api/api';
+import { CreateAuthenticateChallengeRequest } from 'teleport/services/auth';
+import auth from 'teleport/services/auth/auth';
+import { MfaChallengeResponse } from 'teleport/services/mfa';
+
+export interface MfaContextValue {
+ getMfaChallengeResponse(
+ req: CreateAuthenticateChallengeRequest
+ ): Promise;
+}
+
+export const MfaContext = createContext(null);
+
+/**
+ * Provides a global MFA context to handle MFA prompts for methods outside
+ * of the React scope, such as admin action API calls in auth.ts or api.ts.
+ * This is intended as a workaround for such cases, and should not be used
+ * for methods with access to the React scope. Use useMfa directly instead.
+ */
+export const MfaContextProvider = ({ children }: PropsWithChildren) => {
+ const adminMfa = useMfa({});
+
+ const getMfaChallengeResponse = useCallback(
+ async (req: CreateAuthenticateChallengeRequest) => {
+ const chal = await auth.getMfaChallenge(req);
+
+ const res = await adminMfa.getChallengeResponse(chal);
+ if (!res) {
+ return {}; // return an empty challenge to prevent mfa retry.
+ }
+
+ return res;
+ },
+ [adminMfa]
+ );
+
+ const [mfaCtx] = useState(() => {
+ const mfaCtx = { getMfaChallengeResponse };
+ auth.setMfaContext(mfaCtx);
+ api.setMfaContext(mfaCtx);
+ return mfaCtx;
+ });
+
+ return (
+
+
+ {children}
+
+ );
+};
diff --git a/web/packages/teleport/src/Teleport.tsx b/web/packages/teleport/src/Teleport.tsx
index 0c1e35918c24e..8dce3f6460c0d 100644
--- a/web/packages/teleport/src/Teleport.tsx
+++ b/web/packages/teleport/src/Teleport.tsx
@@ -39,6 +39,7 @@ import { LoginFailedComponent as LoginFailed } from './Login/LoginFailed';
import { LoginSuccess } from './Login/LoginSuccess';
import { LoginTerminalRedirect } from './Login/LoginTerminalRedirect';
import { Main } from './Main';
+import { MfaContextProvider } from './MFAContext/MFAContext';
import { Player } from './Player';
import { SingleLogoutFailed } from './SingleLogoutFailed';
import TeleportContext from './teleportContext';
@@ -86,13 +87,15 @@ const Teleport: React.FC = props => {
-
-
- {createPrivateRoutes()}
-
+
+
+
+ {createPrivateRoutes()}
+
+
diff --git a/web/packages/teleport/src/Users/useUsers.ts b/web/packages/teleport/src/Users/useUsers.ts
index 48dc99dd11637..3716e859b9ca3 100644
--- a/web/packages/teleport/src/Users/useUsers.ts
+++ b/web/packages/teleport/src/Users/useUsers.ts
@@ -88,13 +88,18 @@ export default function useUsers({
}
async function onCreate(u: User) {
- const mfaResponse = await auth.getMfaChallengeResponseForAdminAction(true);
- return ctx.userService
- .createUser(u, ExcludeUserField.Traits, mfaResponse)
- .then(result => setUsers([result, ...users]))
- .then(() =>
- ctx.userService.createResetPasswordToken(u.name, 'invite', mfaResponse)
- );
+ const mfaResponse = await auth.getAdminActionMfaResponse(true);
+ const result = await ctx.userService.createUser(
+ u,
+ ExcludeUserField.Traits,
+ mfaResponse
+ );
+ setUsers([result, ...users]);
+ return ctx.userService.createResetPasswordToken(
+ u.name,
+ 'invite',
+ mfaResponse
+ );
}
function onInviteCollaboratorsClose() {
diff --git a/web/packages/teleport/src/lib/useMfa.test.tsx b/web/packages/teleport/src/lib/useMfa.test.tsx
index dad6b2645b70b..cfcb3d1baf555 100644
--- a/web/packages/teleport/src/lib/useMfa.test.tsx
+++ b/web/packages/teleport/src/lib/useMfa.test.tsx
@@ -234,9 +234,10 @@ describe('useMfa', () => {
mfa.current.cancelAttempt();
- await expect(respPromise).rejects.toThrow(
- new Error('User canceled MFA attempt')
+ const expectErr = new Error(
+ 'User cancelled MFA attempt. This is an admin-level API request and requires MFA verification. Please try again with a registered MFA device. If you do not have an MFA device registered, you can add one in the account settings page.'
);
+ await expect(respPromise).rejects.toThrow(expectErr);
// If the user cancels the MFA attempt and closes the dialog, the mfa status
// should be 'success', or else the dialog would remain open to display the error.
diff --git a/web/packages/teleport/src/lib/useMfa.ts b/web/packages/teleport/src/lib/useMfa.ts
index af1e3d531330e..ca5e97b020b16 100644
--- a/web/packages/teleport/src/lib/useMfa.ts
+++ b/web/packages/teleport/src/lib/useMfa.ts
@@ -108,7 +108,11 @@ export function useMfa({ req, isMfaRequired }: MfaProps): MfaState {
const cancelAttempt = () => {
if (mfaResponseRef.current) {
- mfaResponseRef.current.resolve(new Error('User canceled MFA attempt'));
+ mfaResponseRef.current.resolve(
+ new Error(
+ 'User cancelled MFA attempt. This is an admin-level API request and requires MFA verification. Please try again with a registered MFA device. If you do not have an MFA device registered, you can add one in the account settings page.'
+ )
+ );
}
};
diff --git a/web/packages/teleport/src/services/api/api.ts b/web/packages/teleport/src/services/api/api.ts
index 5b19aef0bf580..73e641accfb65 100644
--- a/web/packages/teleport/src/services/api/api.ts
+++ b/web/packages/teleport/src/services/api/api.ts
@@ -18,18 +18,33 @@
import 'whatwg-fetch';
-import auth, { MfaChallengeScope } from 'teleport/services/auth/auth';
+import { MfaContextValue } from 'teleport/MFAContext/MFAContext';
import websession from 'teleport/services/websession';
+import { MfaChallengeScope } from '../auth/auth';
import { MfaChallengeResponse } from '../mfa';
import { storageService } from '../storageService';
import parseError, { ApiError } from './parseError';
export const MFA_HEADER = 'Teleport-Mfa-Response';
+let mfaContext: MfaContextValue;
+
const api = {
- get(url: string, abortSignal?: AbortSignal) {
- return api.fetchJsonWithMfaAuthnRetry(url, { signal: abortSignal });
+ setMfaContext(mfa: MfaContextValue) {
+ mfaContext = mfa;
+ },
+
+ get(
+ url: string,
+ abortSignal?: AbortSignal,
+ mfaResponse?: MfaChallengeResponse
+ ) {
+ return api.fetchJsonWithMfaAuthnRetry(
+ url,
+ { signal: abortSignal },
+ mfaResponse
+ );
},
post(url, data?, abortSignal?, mfaResponse?: MfaChallengeResponse) {
@@ -179,23 +194,40 @@ const api = {
throw new ApiError(parseError(json), response, undefined, json.messages);
}
- let mfaResponseForRetry;
+ const mfaResponseForRetry = await api.getAdminActionMfaResponse();
+
+ return api.fetchJsonWithMfaAuthnRetry(
+ url,
+ customOptions,
+ mfaResponseForRetry
+ );
+ },
+
+ getAdminActionMfaResponse(allowReuse?: boolean) {
+ // mfaContext is set once the react-scoped MFA Context Provider is initialized.
+ // Since this is a global object outside of the react scope, there is a marginal
+ // chance for a race condition here (the react scope should generally be initialized
+ // before this has a chance of being called). This conditional is not expected to
+ // be hit, but will catch any major issues that could arise from this solution.
+ if (!mfaContext) {
+ throw new Error(
+ 'Failed to set up MFA prompt for admin action. Please try refreshing the page to try again. If the issue persists, contact support as this is likely a bug.'
+ );
+ }
+
try {
- const challenge = await auth.getMfaChallenge({
+ return mfaContext.getMfaChallengeResponse({
scope: MfaChallengeScope.ADMIN_ACTION,
+ allowReuse,
+ isMfaRequiredRequest: {
+ admin_action: {},
+ },
});
- mfaResponseForRetry = await auth.getMfaChallengeResponse(challenge);
} catch {
throw new Error(
- 'Failed to fetch MFA challenge. Please connect a registered hardware key and try again. If you do not have a hardware key registered, you can add one from your account settings page.'
+ 'This is an admin-level API request and requires MFA verification. Please try again with a registered MFA device. If you do not have an MFA device registered, you can add one in the account settings page.'
);
}
-
- return api.fetchJsonWithMfaAuthnRetry(
- url,
- customOptions,
- mfaResponseForRetry
- );
},
/**
diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts
index 100259d6dfc20..3f2af5aecf88e 100644
--- a/web/packages/teleport/src/services/auth/auth.ts
+++ b/web/packages/teleport/src/services/auth/auth.ts
@@ -17,6 +17,7 @@
*/
import cfg from 'teleport/config';
+import { MfaContextValue } from 'teleport/MFAContext/MFAContext';
import api from 'teleport/services/api';
import {
DeviceType,
@@ -44,7 +45,13 @@ import {
UserCredentials,
} from './types';
+let mfaContext: MfaContextValue;
+
const auth = {
+ setMfaContext(mfa: MfaContextValue) {
+ mfaContext = mfa;
+ },
+
checkWebauthnSupport() {
if (window.PublicKeyCredential) {
return Promise.resolve();
@@ -277,24 +284,9 @@ const auth = {
// If is_mfa_required_req is provided and it is found that MFA is not required, returns null instead.
async getMfaChallengeResponse(
challenge: MfaAuthenticateChallenge,
- mfaType?: DeviceType,
+ mfaType: DeviceType,
totpCode?: string
): Promise {
- if (!challenge) return;
-
- // TODO(Joerger): If mfaType is not provided by a parent component, use some global context
- // to display a component, similar to the one used in useMfa. For now we just default to
- // whichever method we can succeed with first.
- if (!mfaType) {
- if (totpCode) {
- mfaType = 'totp';
- } else if (challenge.webauthnPublicKey) {
- mfaType = 'webauthn';
- } else if (challenge.ssoChallenge) {
- mfaType = 'sso';
- }
- }
-
if (mfaType === 'webauthn') {
return auth.getWebAuthnChallengeResponse(challenge.webauthnPublicKey);
}
@@ -389,7 +381,7 @@ const auth = {
createPrivilegeTokenWithWebauthn() {
return auth
.getMfaChallenge({ scope: MfaChallengeScope.MANAGE_DEVICES })
- .then(auth.getMfaChallengeResponse)
+ .then(chal => auth.getMfaChallengeResponse(chal, 'webauthn'))
.then(mfaResp => auth.createPrivilegeToken(mfaResp));
},
@@ -442,27 +434,36 @@ const auth = {
.then(res => res?.webauthn_response);
},
- getMfaChallengeResponseForAdminAction(allowReuse?: boolean) {
- // If the client is checking if MFA is required for an admin action,
- // but we know admin action MFA is not enforced, return early.
- if (!cfg.isAdminActionMfaEnforced()) {
- return;
+ getAdminActionMfaResponse(allowReuse?: boolean) {
+ // mfaContext is set once the react-scoped MFA Context Provider is initialized.
+ // Since this is a global object outside of the react scope, there is a marginal
+ // chance for a race condition here (the react scope should generally be initialized
+ // before this has a chance of being called). This conditional is not expected to
+ // be hit, but will catch any major issues that could arise from this solution.
+ if (!mfaContext) {
+ throw new Error(
+ 'Failed to set up MFA prompt for admin action. Please try refreshing the page to try again. If the issue persists, contact support as this is likely a bug.'
+ );
}
- return auth
- .getMfaChallenge({
+ try {
+ return mfaContext.getMfaChallengeResponse({
scope: MfaChallengeScope.ADMIN_ACTION,
- allowReuse: allowReuse,
+ allowReuse,
isMfaRequiredRequest: {
admin_action: {},
},
- })
- .then(auth.getMfaChallengeResponse);
+ });
+ } catch {
+ throw new Error(
+ 'This is an admin-level API request and requires MFA verification. Please try again with a registered MFA device. If you do not have an MFA device registered, you can add one in the account settings page.'
+ );
+ }
},
- // TODO(Joerger): Delete in favor of getMfaChallengeResponseForAdminAction once /e is updated.
+ // TODO(Joerger): Delete in favor of getAdminActionMfaResponse once /e is updated.
getWebauthnResponseForAdminAction(allowReuse?: boolean) {
- return auth.getMfaChallengeResponseForAdminAction(allowReuse);
+ return auth.getAdminActionMfaResponse(allowReuse);
},
};
diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts
index 7b1ffa0b1724d..8ae24bfb44192 100644
--- a/web/packages/teleport/src/services/integrations/integrations.ts
+++ b/web/packages/teleport/src/services/integrations/integrations.ts
@@ -21,7 +21,7 @@ import api from 'teleport/services/api';
import { App } from '../apps';
import makeApp from '../apps/makeApps';
-import auth, { MfaChallengeScope } from '../auth/auth';
+import auth from '../auth/auth';
import makeNode from '../nodes/makeNode';
import {
AwsDatabaseVpcsResponse,
@@ -271,24 +271,14 @@ export const integrationService = {
integrationName,
req: AwsOidcDeployServiceRequest
): Promise {
- const challenge = await auth.getMfaChallenge({
- scope: MfaChallengeScope.ADMIN_ACTION,
- allowReuse: true,
- isMfaRequiredRequest: {
- admin_action: {},
- },
- });
-
- const response = await auth.getMfaChallengeResponse(challenge);
-
- return api
- .post(
- cfg.getAwsDeployTeleportServiceUrl(integrationName),
- req,
- null,
- response
- )
- .then(resp => resp.serviceDashboardUrl);
+ const mfaResp = await auth.getAdminActionMfaResponse(true);
+ const resp = await api.post(
+ cfg.getAwsDeployTeleportServiceUrl(integrationName),
+ req,
+ null,
+ mfaResp
+ );
+ return resp.serviceDashboardUrl;
},
async createAwsAppAccess(integrationName): Promise {
@@ -301,23 +291,21 @@ export const integrationService = {
integrationName,
req: AwsOidcDeployDatabaseServicesRequest
): Promise {
- const mfaResponse = await auth.getMfaChallengeResponseForAdminAction(true);
-
- return api
- .post(
- cfg.getAwsRdsDbsDeployServicesUrl(integrationName),
- req,
- null,
- mfaResponse
- )
- .then(resp => resp.clusterDashboardUrl);
+ const mfaResponse = await auth.getAdminActionMfaResponse(true);
+ const resp = await api.post(
+ cfg.getAwsRdsDbsDeployServicesUrl(integrationName),
+ req,
+ null,
+ mfaResponse
+ );
+ return resp.clusterDashboardUrl;
},
async enrollEksClusters(
integrationName: string,
req: EnrollEksClustersRequest
): Promise {
- const mfaResponse = await auth.getMfaChallengeResponseForAdminAction(true);
+ const mfaResponse = await auth.getAdminActionMfaResponse(true);
return api.post(
cfg.getEnrollEksClusterUrl(integrationName),
diff --git a/web/packages/teleport/src/services/joinToken/joinToken.test.ts b/web/packages/teleport/src/services/joinToken/joinToken.test.ts
index 1f941345c1006..a748a55cbbcdd 100644
--- a/web/packages/teleport/src/services/joinToken/joinToken.test.ts
+++ b/web/packages/teleport/src/services/joinToken/joinToken.test.ts
@@ -19,15 +19,17 @@
import cfg from 'teleport/config';
import api from 'teleport/services/api';
+import auth from '../auth/auth';
import JoinTokenService from './joinToken';
import type { JoinTokenRequest } from './types';
-test('fetchJoinToken with an empty request properly sets defaults', () => {
+test('fetchJoinToken with an empty request properly sets defaults', async () => {
const svc = new JoinTokenService();
jest.spyOn(api, 'post').mockResolvedValue(null);
+ jest.spyOn(auth, 'getAdminActionMfaResponse').mockResolvedValue(null);
// Test with all empty fields.
- svc.fetchJoinToken({} as any);
+ await svc.fetchJoinToken({} as any);
expect(api.post).toHaveBeenCalledWith(
cfg.getJoinTokenUrl(),
{
@@ -36,13 +38,15 @@ test('fetchJoinToken with an empty request properly sets defaults', () => {
allow: [],
suggested_agent_matcher_labels: {},
},
+ null,
null
);
});
-test('fetchJoinToken request fields are set as requested', () => {
+test('fetchJoinToken request fields are set as requested', async () => {
const svc = new JoinTokenService();
jest.spyOn(api, 'post').mockResolvedValue(null);
+ jest.spyOn(auth, 'getAdminActionMfaResponse').mockResolvedValue(null);
const mock: JoinTokenRequest = {
roles: ['Node'],
@@ -50,7 +54,8 @@ test('fetchJoinToken request fields are set as requested', () => {
method: 'iam',
suggestedAgentMatcherLabels: [{ name: 'env', value: 'dev' }],
};
- svc.fetchJoinToken(mock);
+ await svc.fetchJoinToken(mock);
+
expect(api.post).toHaveBeenCalledWith(
cfg.getJoinTokenUrl(),
{
@@ -59,6 +64,7 @@ test('fetchJoinToken request fields are set as requested', () => {
allow: [{ aws_account: '1234', aws_arn: 'xxxx' }],
suggested_agent_matcher_labels: { env: ['dev'] },
},
+ null,
null
);
});
diff --git a/web/packages/teleport/src/services/joinToken/joinToken.ts b/web/packages/teleport/src/services/joinToken/joinToken.ts
index fe564b4440dae..298742acec108 100644
--- a/web/packages/teleport/src/services/joinToken/joinToken.ts
+++ b/web/packages/teleport/src/services/joinToken/joinToken.ts
@@ -20,6 +20,7 @@ import cfg from 'teleport/config';
import api from 'teleport/services/api';
import { makeLabelMapOfStrArrs } from '../agents/make';
+import auth from '../auth/auth';
import makeJoinToken from './makeJoinToken';
import { JoinRule, JoinToken, JoinTokenRequest } from './types';
@@ -27,61 +28,70 @@ const TeleportTokenNameHeader = 'X-Teleport-TokenName';
class JoinTokenService {
// TODO (avatus) refactor this code to eventually use `createJoinToken`
- fetchJoinToken(
+ async fetchJoinToken(
req: JoinTokenRequest,
signal: AbortSignal = null
): Promise {
- return api
- .post(
- cfg.getJoinTokenUrl(),
- {
- roles: req.roles,
- join_method: req.method || 'token',
- allow: makeAllowField(req.rules || []),
- suggested_agent_matcher_labels: makeLabelMapOfStrArrs(
- req.suggestedAgentMatcherLabels
- ),
- },
- signal
- )
- .then(makeJoinToken);
+ const mfaResponse = await auth.getAdminActionMfaResponse();
+ const resp = await api.post(
+ cfg.getJoinTokenUrl(),
+ {
+ roles: req.roles,
+ join_method: req.method || 'token',
+ allow: makeAllowField(req.rules || []),
+ suggested_agent_matcher_labels: makeLabelMapOfStrArrs(
+ req.suggestedAgentMatcherLabels
+ ),
+ },
+ signal,
+ mfaResponse
+ );
+ return makeJoinToken(resp);
}
- upsertJoinTokenYAML(
+ async upsertJoinTokenYAML(
req: JoinTokenRequest,
tokenName: string
): Promise {
- return api
- .putWithHeaders(
- cfg.getJoinTokenYamlUrl(),
- {
- content: req.content,
- },
- {
- [TeleportTokenNameHeader]: tokenName,
- 'Content-Type': 'application/json',
- }
- )
- .then(makeJoinToken);
+ const mfaResponse = await auth.getAdminActionMfaResponse();
+ const resp = await api.putWithHeaders(
+ cfg.getJoinTokenYamlUrl(),
+ {
+ content: req.content,
+ },
+ {
+ [TeleportTokenNameHeader]: tokenName,
+ 'Content-Type': 'application/json',
+ },
+ mfaResponse
+ );
+ return makeJoinToken(resp);
}
- createJoinToken(req: JoinTokenRequest): Promise {
- return api.post(cfg.getJoinTokensUrl(), req).then(makeJoinToken);
+ async createJoinToken(req: JoinTokenRequest): Promise {
+ const mfaResponse = await auth.getAdminActionMfaResponse();
+ const resp = api.post(cfg.getJoinTokensUrl(), req, mfaResponse);
+ return makeJoinToken(resp);
}
- fetchJoinTokens(signal: AbortSignal = null): Promise<{ items: JoinToken[] }> {
- return api.get(cfg.getJoinTokensUrl(), signal).then(resp => {
+ async fetchJoinTokens(
+ signal: AbortSignal = null
+ ): Promise<{ items: JoinToken[] }> {
+ const mfaResponse = await auth.getAdminActionMfaResponse();
+ return api.get(cfg.getJoinTokensUrl(), signal, mfaResponse).then(resp => {
return {
items: resp.items?.map(makeJoinToken) || [],
};
});
}
- deleteJoinToken(id: string, signal: AbortSignal = null) {
+ async deleteJoinToken(id: string, signal: AbortSignal = null) {
+ const mfaResponse = await auth.getAdminActionMfaResponse();
return api.deleteWithHeaders(
cfg.getJoinTokensUrl(),
{ [TeleportTokenNameHeader]: id },
- signal
+ signal,
+ mfaResponse
);
}
}