diff --git a/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx b/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx index b05e68d858991..1e649105cb0ef 100644 --- a/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx +++ b/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx @@ -39,6 +39,7 @@ import { requiredField } from 'shared/components/Validation/rules'; import { useAsync } from 'shared/hooks/useAsync'; import { useTeleport } from 'teleport'; +import auth from 'teleport/services/auth'; import { AWSRules, CreateJoinTokenRequest, @@ -130,7 +131,13 @@ export const UpsertJoinTokenDialog = ({ const [createTokenAttempt, runCreateTokenAttempt] = useAsync( async (req: CreateJoinTokenRequest) => { - const token = await ctx.joinTokenService.createJoinToken(req); + const mfaResponse = await auth.getWebauthnResponseForAdminAction( + true /* allow re-use */ + ); + const token = await ctx.joinTokenService.createJoinToken( + req, + mfaResponse + ); updateTokenList(token); onClose(); } diff --git a/web/packages/teleport/src/services/api/api.ts b/web/packages/teleport/src/services/api/api.ts index f1d466908859a..9411e27ef2f1b 100644 --- a/web/packages/teleport/src/services/api/api.ts +++ b/web/packages/teleport/src/services/api/api.ts @@ -131,11 +131,37 @@ const api = { * It returns the JSON data if it is a valid JSON and * there were no response errors. * - * If a response had an error and it contained a MFA authn - * required message, then a retry is attempted after a user - * successfully re-authenticates with an MFA device. + * The field "mfaResponse" accepts a pre-made response to an MFA challenge + * for an admin action with allowReuse set to true. * - * All other errors will be thrown. + * The cluster requires the user to re-authenticate before performing certain + * admin actions (e.g., creating join tokens or deleting users) when + * second_factor is set to webauthn. + * + * Generally, mfaResponse is not needed because this func will first attempt + * a fetch without it and if the response had an error and it contained a MFA + * authn required message, then this func will fetch a challenge and prompt + * the user to re-authn. After successfully re-authenticating, a retry fetch + * with the original URL is attempted. + * + * There are a few cases where providing a mfaResponse is required and it + * starts by creating a MFA challenge with `allowReuse` set to true + * (see services/auth/auth.ts > getMfaChallengeResponseForAdminAction). + * + * This allow users to require re-authenticating ONCE for the following: + * - In the web app, we can be calling multiple endpoints back to back, + * and some or all endpoints require re-authenticating. If a non reusable + * mfaResponse was provided, or mfaResponse wasn't provided then each + * fetch will ask the user to re-authenticate. + * - We can make a single fetch without a mfaResponse, re-authenticate + * successfully as required, but the retry attempt still fails with a + * vague "access denied" error. This is because there are some endpoints + * where it's in the backend that are calling multiple endpoints that + * require re-authenticating. Providing a reusable mfaResponse resolves + * this issue. + * + * The mfaResponse lasts for WebauthnChallengeTimeout defined in: + * https://github.com/gravitational/teleport/blob/b8a65486844b4125ea1cf1f08ae17e2fc5a4db5a/lib/defaults/defaults.go#L598 */ async fetchJsonWithMfaAuthnRetry( url: string, diff --git a/web/packages/teleport/src/services/joinToken/joinToken.ts b/web/packages/teleport/src/services/joinToken/joinToken.ts index fe564b4440dae..14f29c10ecb5f 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 { WebauthnAssertionResponse } from '../auth'; import makeJoinToken from './makeJoinToken'; import { JoinRule, JoinToken, JoinTokenRequest } from './types'; @@ -65,8 +66,13 @@ class JoinTokenService { .then(makeJoinToken); } - createJoinToken(req: JoinTokenRequest): Promise { - return api.post(cfg.getJoinTokensUrl(), req).then(makeJoinToken); + createJoinToken( + req: JoinTokenRequest, + mfaResponse: WebauthnAssertionResponse + ): Promise { + return api + .post(cfg.getJoinTokensUrl(), req, null /* abortSignal */, mfaResponse) + .then(makeJoinToken); } fetchJoinTokens(signal: AbortSignal = null): Promise<{ items: JoinToken[] }> {