Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
Expand Down
34 changes: 30 additions & 4 deletions web/packages/teleport/src/services/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions web/packages/teleport/src/services/joinToken/joinToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -65,8 +66,13 @@ class JoinTokenService {
.then(makeJoinToken);
}

createJoinToken(req: JoinTokenRequest): Promise<JoinToken> {
return api.post(cfg.getJoinTokensUrl(), req).then(makeJoinToken);
createJoinToken(
req: JoinTokenRequest,
mfaResponse: WebauthnAssertionResponse
): Promise<JoinToken> {
return api
.post(cfg.getJoinTokensUrl(), req, null /* abortSignal */, mfaResponse)
.then(makeJoinToken);
}

fetchJoinTokens(signal: AbortSignal = null): Promise<{ items: JoinToken[] }> {
Expand Down
Loading