From 709f5af10a752c9cfc93ea9d54ba79926679aa5d Mon Sep 17 00:00:00 2001 From: joerger Date: Wed, 18 Dec 2024 11:31:58 -0800 Subject: [PATCH] Use global mfa context for all admin actions with explicit MFA checks. --- .../teleport/src/MFAContext/MFAContext.tsx | 15 +++--- web/packages/teleport/src/Users/useUsers.ts | 2 +- web/packages/teleport/src/services/api/api.ts | 11 +---- .../teleport/src/services/auth/auth.ts | 48 ++++++------------- .../src/services/integrations/integrations.ts | 19 ++------ .../src/services/joinToken/joinToken.ts | 18 +++---- 6 files changed, 34 insertions(+), 79 deletions(-) diff --git a/web/packages/teleport/src/MFAContext/MFAContext.tsx b/web/packages/teleport/src/MFAContext/MFAContext.tsx index ddd478f207fba..daac6287bad00 100644 --- a/web/packages/teleport/src/MFAContext/MFAContext.tsx +++ b/web/packages/teleport/src/MFAContext/MFAContext.tsx @@ -1,12 +1,9 @@ import { PropsWithChildren, createContext, useCallback, useRef } from 'react'; import AuthnDialog from 'teleport/components/AuthnDialog'; import { useMfa } from 'teleport/lib/useMfa'; -import api from 'teleport/services/api'; -import { MfaChallengeScope } from 'teleport/services/auth/auth'; +import auth, { MfaChallengeScope } from 'teleport/services/auth/auth'; import { MfaChallengeResponse } from 'teleport/services/mfa'; -import { useTeleport } from '..'; - export interface MfaContextValue { getAdminActionMfaResponse(reusable?: boolean): Promise; } @@ -33,11 +30,11 @@ export const MfaContextProvider = ({ children }: PropsWithChildren) => { [adminMfa, allowReuse] ); - const mfaCtx = { getAdminActionMfaResponse }; - - const ctx = useTeleport(); - ctx.joinTokenService.setMfaContext(mfaCtx); - api.setMfaContext(mfaCtx); + const mfaCtx = { + getAdminActionMfaResponse, + useMfa, + }; + auth.setMfaContext(mfaCtx); return ( diff --git a/web/packages/teleport/src/Users/useUsers.ts b/web/packages/teleport/src/Users/useUsers.ts index 48dc99dd11637..404ea58fe252b 100644 --- a/web/packages/teleport/src/Users/useUsers.ts +++ b/web/packages/teleport/src/Users/useUsers.ts @@ -88,7 +88,7 @@ export default function useUsers({ } async function onCreate(u: User) { - const mfaResponse = await auth.getMfaChallengeResponseForAdminAction(true); + const mfaResponse = await auth.getAdminActionMfaResponse(true); return ctx.userService .createUser(u, ExcludeUserField.Traits, mfaResponse) .then(result => setUsers([result, ...users])) diff --git a/web/packages/teleport/src/services/api/api.ts b/web/packages/teleport/src/services/api/api.ts index e13f07e2adece..07bb68f068905 100644 --- a/web/packages/teleport/src/services/api/api.ts +++ b/web/packages/teleport/src/services/api/api.ts @@ -21,22 +21,15 @@ import 'whatwg-fetch'; import websession from 'teleport/services/websession'; import 'whatwg-fetch'; -import { MfaContextValue } from 'teleport/MFAContext/MFAContext'; - import { MfaChallengeResponse } from '../mfa'; import { storageService } from '../storageService'; +import auth from '../auth/auth'; import parseError, { ApiError } from './parseError'; export const MFA_HEADER = 'Teleport-Mfa-Response'; -let mfaContext: MfaContextValue; - const api = { - setMfaContext(mfa: MfaContextValue) { - mfaContext = mfa; - }, - get( url: string, abortSignal?: AbortSignal, @@ -198,7 +191,7 @@ const api = { let mfaResponseForRetry; try { - mfaResponseForRetry = await mfaContext.getAdminActionMfaResponse(); + mfaResponseForRetry = await auth.getAdminActionMfaResponse(); } 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.' diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts index 100259d6dfc20..2b3f4d7a0cde1 100644 --- a/web/packages/teleport/src/services/auth/auth.ts +++ b/web/packages/teleport/src/services/auth/auth.ts @@ -25,6 +25,8 @@ import { MfaChallengeResponse, SsoChallenge, } from 'teleport/services/mfa'; + +import { MfaContextValue } from 'teleport/MFAContext/MFAContext'; import { CaptureEvent, userEventService } from 'teleport/services/userEvent'; import { @@ -33,6 +35,7 @@ import { parseMfaChallengeJson, parseMfaRegistrationChallengeJson, } from '../mfa/makeMfa'; + import { makeChangedUserAuthn } from './make'; import makePasswordToken from './makePasswordToken'; import { @@ -44,7 +47,13 @@ import { UserCredentials, } from './types'; +let mfaContext: MfaContextValue; + const auth = { + setMfaContext(mfa: MfaContextValue) { + mfaContext = mfa; + }, + checkWebauthnSupport() { if (window.PublicKeyCredential) { return Promise.resolve(); @@ -277,24 +286,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 +383,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 +436,13 @@ 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; - } - - return auth - .getMfaChallenge({ - scope: MfaChallengeScope.ADMIN_ACTION, - allowReuse: allowReuse, - isMfaRequiredRequest: { - admin_action: {}, - }, - }) - .then(auth.getMfaChallengeResponse); + getAdminActionMfaResponse(allowReuse?: boolean) { + return mfaContext.getAdminActionMfaResponse(allowReuse); }, // TODO(Joerger): Delete in favor of getMfaChallengeResponseForAdminAction once /e is updated. getWebauthnResponseForAdminAction(allowReuse?: boolean) { - return auth.getMfaChallengeResponseForAdminAction(allowReuse); + return mfaContext.getAdminActionMfaResponse(allowReuse); }, }; diff --git a/web/packages/teleport/src/services/integrations/integrations.ts b/web/packages/teleport/src/services/integrations/integrations.ts index 7b1ffa0b1724d..ff78b8a90f2d4 100644 --- a/web/packages/teleport/src/services/integrations/integrations.ts +++ b/web/packages/teleport/src/services/integrations/integrations.ts @@ -21,8 +21,9 @@ 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, AwsOidcDeployDatabaseServicesRequest, @@ -271,16 +272,7 @@ 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); - + const response = await auth.getAdminActionMfaResponse(true); return api .post( cfg.getAwsDeployTeleportServiceUrl(integrationName), @@ -301,8 +293,7 @@ export const integrationService = { integrationName, req: AwsOidcDeployDatabaseServicesRequest ): Promise { - const mfaResponse = await auth.getMfaChallengeResponseForAdminAction(true); - + const mfaResponse = await auth.getAdminActionMfaResponse(true); return api .post( cfg.getAwsRdsDbsDeployServicesUrl(integrationName), @@ -317,7 +308,7 @@ export const integrationService = { 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.ts b/web/packages/teleport/src/services/joinToken/joinToken.ts index e6bc2e34b126d..c575b5803d689 100644 --- a/web/packages/teleport/src/services/joinToken/joinToken.ts +++ b/web/packages/teleport/src/services/joinToken/joinToken.ts @@ -17,10 +17,10 @@ */ import cfg from 'teleport/config'; -import { MfaContextValue } from 'teleport/MFAContext/MFAContext'; 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'; @@ -28,18 +28,12 @@ import { JoinRule, JoinToken, JoinTokenRequest } from './types'; const TeleportTokenNameHeader = 'X-Teleport-TokenName'; class JoinTokenService { - // MFA context is set late by the MFA Context provider. - mfa: MfaContextValue; - setMfaContext(mfa: MfaContextValue) { - this.mfa = mfa; - } - // TODO (avatus) refactor this code to eventually use `createJoinToken` async fetchJoinToken( req: JoinTokenRequest, signal: AbortSignal = null ): Promise { - const mfaResponse = await this.mfa.getAdminActionMfaResponse(); + const mfaResponse = await auth.getAdminActionMfaResponse(); return api .post( cfg.getJoinTokenUrl(), @@ -61,7 +55,7 @@ class JoinTokenService { req: JoinTokenRequest, tokenName: string ): Promise { - const mfaResponse = await this.mfa.getAdminActionMfaResponse(); + const mfaResponse = await auth.getAdminActionMfaResponse(); return api .putWithHeaders( cfg.getJoinTokenYamlUrl(), @@ -78,7 +72,7 @@ class JoinTokenService { } async createJoinToken(req: JoinTokenRequest): Promise { - const mfaResponse = await this.mfa.getAdminActionMfaResponse(); + const mfaResponse = await auth.getAdminActionMfaResponse(); return api .post(cfg.getJoinTokensUrl(), req, mfaResponse) .then(makeJoinToken); @@ -87,7 +81,7 @@ class JoinTokenService { async fetchJoinTokens( signal: AbortSignal = null ): Promise<{ items: JoinToken[] }> { - const mfaResponse = await this.mfa.getAdminActionMfaResponse(); + const mfaResponse = await auth.getAdminActionMfaResponse(); return api.get(cfg.getJoinTokensUrl(), signal, mfaResponse).then(resp => { return { items: resp.items?.map(makeJoinToken) || [], @@ -96,7 +90,7 @@ class JoinTokenService { } async deleteJoinToken(id: string, signal: AbortSignal = null) { - const mfaResponse = await this.mfa.getAdminActionMfaResponse(); + const mfaResponse = await auth.getAdminActionMfaResponse(); return api.deleteWithHeaders( cfg.getJoinTokensUrl(), { [TeleportTokenNameHeader]: id },