diff --git a/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.story.tsx b/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.story.tsx index d2fdf750a6f4e..29a80a907c7ab 100644 --- a/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.story.tsx +++ b/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.story.tsx @@ -63,4 +63,6 @@ const props: State = { hostname: 'db-hostname', }, dbEngine: DatabaseEngine.MySQL, + showMfaDialog: false, + cancelMfaDialog: () => null, }; diff --git a/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx index 9a5821c92ed4a..42a6b8634411f 100644 --- a/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx @@ -22,6 +22,7 @@ import Select, { Option } from 'shared/components/Select'; import TextSelectCopy from 'teleport/components/TextSelectCopy'; import { generateTshLoginCommand } from 'teleport/lib/util'; +import ReAuthenticate from 'teleport/components/ReAuthenticate'; import { ActionButtons, @@ -53,6 +54,8 @@ export function TestConnectionView({ username, clusterId, dbEngine, + showMfaDialog, + cancelMfaDialog, }: State) { const userOpts = db.users.map(l => ({ value: l, label: l })); const nameOpts = db.names.map(l => ({ value: l, label: l })); @@ -67,8 +70,21 @@ export function TestConnectionView({ tshDbCmd += ` --db-name=${selectedName.value}`; } + function makeTestConnRequest() { + return { + name: selectedName?.value, + user: selectedUser?.value, + }; + } + return ( + {showMfaDialog && ( + testConnection(makeTestConnRequest(), res)} + onClose={cancelMfaDialog} + /> + )} Test Connection Optionally verify that you can successfully connect to the Database you @@ -121,12 +137,7 @@ export function TestConnectionView({ attempt={attempt} diagnosis={diagnosis} canTestConnection={canTestConnection} - testConnection={() => - testConnection({ - name: selectedName?.value, - user: selectedUser?.value, - }) - } + testConnection={() => testConnection(makeTestConnRequest())} stepNumber={2} stepDescription="Verify that your database is accessible" /> diff --git a/web/packages/teleport/src/Discover/Database/TestConnection/useTestConnection.ts b/web/packages/teleport/src/Discover/Database/TestConnection/useTestConnection.ts index 10160c2b1072c..fd428ba657d07 100644 --- a/web/packages/teleport/src/Discover/Database/TestConnection/useTestConnection.ts +++ b/web/packages/teleport/src/Discover/Database/TestConnection/useTestConnection.ts @@ -20,20 +20,27 @@ import { DbMeta } from '../../useDiscover'; import type { AgentStepProps } from '../../types'; import type { Database } from '../resources'; +import type { MfaAuthnResponse } from 'teleport/services/mfa'; export function useTestConnection(props: AgentStepProps) { const { runConnectionDiagnostic, ...connectionDiagnostic } = - useConnectionDiagnostic(props); + useConnectionDiagnostic(); - function testConnection({ name, user }: { name: string; user: string }) { - runConnectionDiagnostic({ - resourceKind: 'db', - resourceName: props.agentMeta.resourceName, - dbTester: { - name, - user, + function testConnection( + { name, user }: { name: string; user: string }, + mfaResponse?: MfaAuthnResponse + ) { + runConnectionDiagnostic( + { + resourceKind: 'db', + resourceName: props.agentMeta.resourceName, + dbTester: { + name, + user, + }, }, - }); + mfaResponse + ); } const { engine } = props.resourceState as Database; diff --git a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.story.tsx b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.story.tsx index 0501e95c5302d..7c420b961b421 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.story.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.story.tsx @@ -98,4 +98,6 @@ const props: State = { username: 'teleport-username', authType: 'local', clusterId: 'some-cluster-id', + showMfaDialog: false, + cancelMfaDialog: () => null, }; diff --git a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx index 3c61471bf5b6a..62252d8f6fcd6 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx @@ -25,6 +25,7 @@ import { Option } from 'shared/components/Select'; import TextSelectCopy from 'teleport/components/TextSelectCopy'; import { generateTshLoginCommand } from 'teleport/lib/util'; +import ReAuthenticate from 'teleport/components/ReAuthenticate'; import { ActionButtons, @@ -36,6 +37,7 @@ import { import { useTestConnection, State } from './useTestConnection'; import type { AgentStepProps } from '../../types'; +import type { KubeImpersonation } from 'teleport/services/agents'; export default function Container(props: AgentStepProps) { const state = useTestConnection(props); @@ -54,6 +56,8 @@ export function TestConnection({ authType, username, clusterId, + showMfaDialog, + cancelMfaDialog, }: State) { const userOpts = kube.users.map(l => ({ value: l, label: l })); const groupOpts = kube.groups.map(l => ({ value: l, label: l })); @@ -72,17 +76,27 @@ export function TestConnection({ return; } - testConnection({ + testConnection(makeTestConnRequest()); + } + + function makeTestConnRequest(): KubeImpersonation { + return { namespace, user: selectedUser?.value, groups: selectedGroups?.map(g => g.value), - }); + }; } return ( {({ validator }) => ( + {showMfaDialog && ( + testConnection(makeTestConnRequest(), res)} + onClose={cancelMfaDialog} + /> + )} Test Connection diff --git a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/useTestConnection.ts b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/useTestConnection.ts index d6a7c268a6bce..c05ce85173f71 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/useTestConnection.ts +++ b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/useTestConnection.ts @@ -20,17 +20,24 @@ import { KubeMeta } from '../../useDiscover'; import type { KubeImpersonation } from 'teleport/services/agents'; import type { AgentStepProps } from '../../types'; +import type { MfaAuthnResponse } from 'teleport/services/mfa'; export function useTestConnection(props: AgentStepProps) { const { runConnectionDiagnostic, ...connectionDiagnostic } = - useConnectionDiagnostic(props); + useConnectionDiagnostic(); - function testConnection(impersonate: KubeImpersonation) { - runConnectionDiagnostic({ - resourceKind: 'kube_cluster', - resourceName: props.agentMeta.resourceName, - kubeImpersonation: impersonate, - }); + function testConnection( + impersonate: KubeImpersonation, + mfaResponse?: MfaAuthnResponse + ) { + runConnectionDiagnostic( + { + resourceKind: 'kube_cluster', + resourceName: props.agentMeta.resourceName, + kubeImpersonation: impersonate, + }, + mfaResponse + ); } return { diff --git a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.story.tsx b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.story.tsx index d4907a0a0b8f2..f8f571e949580 100644 --- a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.story.tsx +++ b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.story.tsx @@ -46,4 +46,6 @@ const props: State = { username: 'teleport-username', authType: 'local', clusterId: 'some-cluster-id', + showMfaDialog: false, + cancelMfaDialog: () => null, }; diff --git a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx index c23f8f348b42e..0c3e3b7e9b509 100644 --- a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx @@ -19,6 +19,8 @@ import styled from 'styled-components'; import { ButtonSecondary, Text, Box, LabelInput } from 'design'; import Select from 'shared/components/Select'; +import ReAuthenticate from 'teleport/components/ReAuthenticate'; + import { HeaderWithBackBtn, ActionButtons, @@ -46,6 +48,8 @@ export function TestConnection({ nextStep, prevStep, canTestConnection, + showMfaDialog, + cancelMfaDialog, }: State) { const [usernameOpts] = useState(() => logins.map(l => ({ value: l, label: l })) @@ -56,6 +60,12 @@ export function TestConnection({ return ( + {showMfaDialog && ( + testConnection(selectedOpt.value, res)} + onClose={cancelMfaDialog} + /> + )} Test Connection Optionally verify that you can successfully connect to the server you diff --git a/web/packages/teleport/src/Discover/Server/TestConnection/useTestConnection.ts b/web/packages/teleport/src/Discover/Server/TestConnection/useTestConnection.ts index 5ba385f6bea1c..08b16365c3c71 100644 --- a/web/packages/teleport/src/Discover/Server/TestConnection/useTestConnection.ts +++ b/web/packages/teleport/src/Discover/Server/TestConnection/useTestConnection.ts @@ -21,10 +21,11 @@ import { useConnectionDiagnostic } from 'teleport/Discover/Shared'; import { NodeMeta } from '../../useDiscover'; import type { AgentStepProps } from '../../types'; +import type { MfaAuthnResponse } from 'teleport/services/mfa'; export function useTestConnection(props: AgentStepProps) { const { runConnectionDiagnostic, ...connectionDiagnostic } = - useConnectionDiagnostic(props); + useConnectionDiagnostic(); function startSshSession(login: string) { const meta = props.agentMeta as NodeMeta; @@ -37,12 +38,15 @@ export function useTestConnection(props: AgentStepProps) { openNewTab(url); } - function testConnection(login: string) { - runConnectionDiagnostic({ - resourceKind: 'node', - resourceName: props.agentMeta.resourceName, - sshPrincipal: login, - }); + function testConnection(login: string, mfaResponse?: MfaAuthnResponse) { + runConnectionDiagnostic( + { + resourceKind: 'node', + resourceName: props.agentMeta.resourceName, + sshPrincipal: login, + }, + mfaResponse + ); } return { diff --git a/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts b/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts index 6c4d367735aef..99052c0f1888c 100644 --- a/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts +++ b/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts @@ -20,55 +20,87 @@ import useAttempt from 'shared/hooks/useAttemptNext'; import useTeleport from 'teleport/useTeleport'; import { useDiscover } from 'teleport/Discover/useDiscover'; import { DiscoverEventStatus } from 'teleport/services/userEvent'; +import { getDatabaseProtocol } from 'teleport/Discover/Database/resources'; import type { ConnectionDiagnostic, ConnectionDiagnosticRequest, } from 'teleport/services/agents'; -import type { AgentStepProps } from '../../types'; +import type { MfaAuthnResponse } from 'teleport/services/mfa'; +import type { Database } from 'teleport/Discover/Database/resources'; -export function useConnectionDiagnostic(props: AgentStepProps) { +export function useConnectionDiagnostic() { const ctx = useTeleport(); - const { attempt, run } = useAttempt(''); + const { attempt, setAttempt, handleError } = useAttempt(''); const [diagnosis, setDiagnosis] = useState(); const [ranDiagnosis, setRanDiagnosis] = useState(false); - const { emitErrorEvent, emitEvent } = useDiscover(); + const { emitErrorEvent, emitEvent, prevStep, nextStep, resourceState } = + useDiscover(); const access = ctx.storeUser.getConnectionDiagnosticAccess(); const canTestConnection = access.create && access.edit && access.read; - function runConnectionDiagnostic(req: ConnectionDiagnosticRequest) { + const [showMfaDialog, setShowMfaDialog] = useState(false); + + // runConnectionDiagnostic depending on the value of `mfaAuthnResponse` does the following: + // 1) If param `mfaAuthnResponse` is undefined or null, it will check if MFA is required. + // - If MFA is required, it sets a flag that indicates a users + // MFA credentials are required, and skips the request to test connection. + // - If MFA is NOT required, it makes the request to test connection. + // 2) If param `mfaAuthnResponse` is defined, it skips checking if MFA is required, + // and makes the request to test connection. + async function runConnectionDiagnostic( + req: ConnectionDiagnosticRequest, + mfaAuthnResponse?: MfaAuthnResponse + ) { setDiagnosis(null); // reset since user's can re-test connection. setRanDiagnosis(true); - run(() => - ctx.agentService - .createConnectionDiagnostic(req) - .then(diag => { - setDiagnosis(diag); - - // The request may succeed, but the connection - // test itself can fail: - if (!diag.success) { - // Append all possible errors: - const errors: string[] = []; - diag.traces.forEach(trace => { - if (trace.status === 'failed') { - errors.push( - `[${trace.traceType}] ${trace.error} (${trace.details})` - ); - } - }); - emitErrorEvent(`testing failed: ${errors.join('\n')}`); - } else { - emitEvent({ stepStatus: DiscoverEventStatus.Success }); - } - }) - .catch((error: Error) => { - emitErrorEvent(error.message); - throw error; - }) - ); + setShowMfaDialog(false); + + setAttempt({ status: 'processing' }); + + try { + if (!mfaAuthnResponse) { + const mfaReq = getMfaRequest(req, resourceState); + const sessionMfa = await ctx.mfaService.isMfaRequired(mfaReq); + if (sessionMfa.required) { + setShowMfaDialog(true); + return; + } + } + + const diag = await ctx.agentService.createConnectionDiagnostic({ + ...req, + mfaAuthnResponse, + }); + + setAttempt({ status: 'success' }); + setDiagnosis(diag); + + // The request may succeed, but the connection + // test itself can fail: + if (!diag.success) { + // Append all possible errors: + const errors = diag.traces + .filter(trace => trace.status === 'failed') + .map( + trace => `[${trace.traceType}] ${trace.error} (${trace.details})` + ) + .join('\n'); + emitErrorEvent(`diagnosis returned with errors: ${errors}`); + } else { + emitEvent({ stepStatus: DiscoverEventStatus.Success }); + } + } catch (err) { + handleError(err); + emitErrorEvent(err.message); + } + } + + function cancelMfaDialog() { + setAttempt({ status: '' }); + setShowMfaDialog(false); } const { username, authType } = ctx.storeUser.state; @@ -84,14 +116,46 @@ export function useConnectionDiagnostic(props: AgentStepProps) { // else either a failed or success event would've been // already sent for each test connection, so we don't need // to send anything here. - props.nextStep(); + nextStep(); }, - prevStep: props.prevStep, + prevStep, canTestConnection, username, authType, clusterId: ctx.storeUser.getClusterId(), + showMfaDialog, + cancelMfaDialog, }; } +function getMfaRequest(req: ConnectionDiagnosticRequest, resourceState: any) { + switch (req.resourceKind) { + case 'node': + return { + node: { + login: req.sshPrincipal, + node_name: req.resourceName, + }, + }; + + case 'db': + const state = resourceState as Database; + return { + database: { + service_name: req.resourceName, + protocol: getDatabaseProtocol(state.engine), + name: req.dbTester?.name, + username: req.dbTester?.user, + }, + }; + + case 'kube_cluster': + return { + kube: { + cluster_name: req.resourceName, + }, + }; + } +} + export type State = ReturnType; diff --git a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts index 4d4109ea2fc3d..ba0f309f07706 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts +++ b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts @@ -14,33 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; - import useAttempt from 'shared/hooks/useAttemptNext'; import cfg from 'teleport/config'; import auth from 'teleport/services/auth'; -export default function useReAuthenticate({ - onAuthenticated, - onClose, - actionText = defaultActionText, -}: Props) { +import type { MfaAuthnResponse } from 'teleport/services/mfa'; + +// useReAuthenticate will have different "submit" behaviors depending on: +// - If prop field `onMfaResponse` is defined, after a user submits, the +// function `onMfaResponse` is called with the user's MFA response. +// - If prop field `onAuthenticated` is defined, after a user submits, the +// user's MFA response are submitted with the request to get a privilege +// token, and after successfully obtaining the token, the function +// `onAuthenticated` will be called with this token. +export default function useReAuthenticate(props: Props) { + const { onClose, actionText = defaultActionText } = props; + + // Note that attempt state "success" is not used or required. + // After the user submits, the control is passed back + // to the caller who is reponsible for rendering the `ReAuthenticate` + // component. const { attempt, setAttempt, handleError } = useAttempt(''); function submitWithTotp(secondFactorToken: string) { + if ('onMfaResponse' in props) { + props.onMfaResponse({ totp_code: secondFactorToken }); + return; + } + setAttempt({ status: 'processing' }); auth .createPrivilegeTokenWithTotp(secondFactorToken) - .then(onAuthenticated) + .then(props.onAuthenticated) .catch(handleError); } function submitWithWebauthn() { setAttempt({ status: 'processing' }); + + if ('onMfaResponse' in props) { + auth + .getWebauthnResponse() + .then(webauthnResponse => + props.onMfaResponse({ webauthn_response: webauthnResponse }) + ) + .catch(handleError); + return; + } + auth .createPrivilegeTokenWithWebauthn() - .then(onAuthenticated) + .then(props.onAuthenticated) .catch((err: Error) => { // This catches a webauthn frontend error that occurs on Firefox and replaces it with a more helpful error message. if ( @@ -75,8 +100,7 @@ export default function useReAuthenticate({ const defaultActionText = 'performing this action'; -export type Props = { - onAuthenticated: React.Dispatch>; +type BaseProps = { onClose: () => void; /** * The text that will be appended to the text in the re-authentication dialog. @@ -90,4 +114,23 @@ export type Props = { actionText?: string; }; +// MfaResponseProps defines a function +// that accepts a MFA response. No +// authentication has been done at this point. +type MfaResponseProps = BaseProps & { + onMfaResponse(res: MfaAuthnResponse): void; + onAuthenticated?: never; +}; + +// DefaultProps defines a function that +// accepts a privilegeTokenId that is only +// obtained after MFA response has been +// validated. +type DefaultProps = BaseProps & { + onAuthenticated(privilegeTokenId: string): void; + onMfaResponse?: never; +}; + +export type Props = MfaResponseProps | DefaultProps; + export type State = ReturnType; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index 78d43da1cf745..507ad198f108d 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -172,6 +172,7 @@ const cfg = { nodeScriptPath: '/scripts/:token/install-node.sh', appNodeScriptPath: '/scripts/:token/install-app.sh?name=:name&uri=:uri', + mfaRequired: '/v1/webapi/sites/:clusterId/mfa/required', mfaLoginBegin: '/v1/webapi/mfa/login/begin', // creates authnenticate challenge with user and password mfaLoginFinish: '/v1/webapi/mfa/login/finishsession', // creates a web session mfaChangePasswordBegin: '/v1/webapi/mfa/authenticatechallenge/password', @@ -397,6 +398,11 @@ const cfg = { return generatePath(cfg.api.connectionDiagnostic, { clusterId }); }, + getMfaRequiredUrl() { + const clusterId = cfg.proxyCluster; + return generatePath(cfg.api.mfaRequired, { clusterId }); + }, + getCheckAccessToRegisteredResourceUrl() { const clusterId = cfg.proxyCluster; return generatePath(cfg.api.checkAccessToRegisteredResource, { diff --git a/web/packages/teleport/src/services/agents/agents.ts b/web/packages/teleport/src/services/agents/agents.ts index 5ed5803b66b9e..600720ce7b41b 100644 --- a/web/packages/teleport/src/services/agents/agents.ts +++ b/web/packages/teleport/src/services/agents/agents.ts @@ -40,6 +40,7 @@ export const agentService = { }, database_name: req.dbTester?.name, database_user: req.dbTester?.user, + mfa_response: req.mfaAuthnResponse, }) .then(makeConnectionDiagnostic); }, diff --git a/web/packages/teleport/src/services/agents/types.ts b/web/packages/teleport/src/services/agents/types.ts index 2f06887953bce..fdded6207c257 100644 --- a/web/packages/teleport/src/services/agents/types.ts +++ b/web/packages/teleport/src/services/agents/types.ts @@ -20,6 +20,8 @@ import { Node } from 'teleport/services/nodes'; import { Kube } from 'teleport/services/kube'; import { Desktop, WindowsDesktopService } from 'teleport/services/desktops'; +import type { MfaAuthnResponse } from '../mfa'; + export type AgentKind = | App | Database @@ -99,6 +101,7 @@ export type ConnectionDiagnosticRequest = { sshPrincipal?: string; //`json:"ssh_principal"` kubeImpersonation?: KubeImpersonation; // `json:"kubernetes_impersonation` dbTester?: DatabaseTester; + mfaAuthnResponse?: MfaAuthnResponse; }; export type KubeImpersonation = { diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts index cfcda7a74ef66..d5852b02edfc5 100644 --- a/web/packages/teleport/src/services/auth/auth.ts +++ b/web/packages/teleport/src/services/auth/auth.ts @@ -195,7 +195,7 @@ const auth = { return api.post(cfg.api.createPrivilegeTokenPath, { secondFactorToken }); }, - createPrivilegeTokenWithWebauthn() { + fetchWebauthnChallenge() { return auth .checkWebauthnSupport() .then(() => @@ -207,17 +207,26 @@ const auth = { navigator.credentials.get({ publicKey: res.webauthnPublicKey, }) - ) - .then(res => - api.post(cfg.api.createPrivilegeTokenPath, { - webauthnAssertionResponse: makeWebauthnAssertionResponse(res), - }) ); }, + createPrivilegeTokenWithWebauthn() { + return auth.fetchWebauthnChallenge().then(res => + api.post(cfg.api.createPrivilegeTokenPath, { + webauthnAssertionResponse: makeWebauthnAssertionResponse(res), + }) + ); + }, + createRestrictedPrivilegeToken() { return api.post(cfg.api.createPrivilegeTokenPath, {}); }, + + getWebauthnResponse() { + return auth + .fetchWebauthnChallenge() + .then(res => makeWebauthnAssertionResponse(res)); + }, }; function base64EncodeUnicode(str: string) { diff --git a/web/packages/teleport/src/services/mfa/mfa.ts b/web/packages/teleport/src/services/mfa/mfa.ts index a09939700db4c..2d9c61643e0c1 100644 --- a/web/packages/teleport/src/services/mfa/mfa.ts +++ b/web/packages/teleport/src/services/mfa/mfa.ts @@ -22,6 +22,7 @@ import { MfaDevice, AddNewTotpDeviceRequest, AddNewHardwareDeviceRequest, + IsMfaRequiredRequest, } from './types'; import makeMfaDevice from './makeMfaDevice'; @@ -42,6 +43,10 @@ class MfaService { .then(devices => devices.map(makeMfaDevice)); } + isMfaRequired(req: IsMfaRequiredRequest): Promise<{ required: boolean }> { + return api.post(cfg.getMfaRequiredUrl(), req); + } + addNewTotpDevice(req: AddNewTotpDeviceRequest) { return api.post(cfg.api.mfaDevicesPath, req); } diff --git a/web/packages/teleport/src/services/mfa/types.ts b/web/packages/teleport/src/services/mfa/types.ts index dbbd4d08ee019..be960ec52c7bc 100644 --- a/web/packages/teleport/src/services/mfa/types.ts +++ b/web/packages/teleport/src/services/mfa/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { WebauthnAssertionResponse } from '../auth'; + export type MfaDevice = { id: string; name: string; @@ -38,3 +40,52 @@ export type DeviceType = 'totp' | 'webauthn'; // DeviceUsage is the intended usage of the device (MFA, Passwordless, etc). export type DeviceUsage = 'passwordless' | 'mfa'; + +// MfaAuthnResponse is a response to a MFA device challenge. +export type MfaAuthnResponse = + | { totp_code: string } + | { webauthn_response: WebauthnAssertionResponse }; + +export type IsMfaRequiredDatabase = { + database: { + // service_name is the database service name. + service_name: string; + // protocol is the type of the database protocol. + protocol: string; + // username is an optional database username. + username?: string; + // database_name is an optional database name. + database_name?: string; + }; +}; + +export type IsMfaRequiredNode = { + node: { + // node_name can be node's hostname or UUID. + node_name: string; + // login is the OS login name. + login: string; + }; +}; + +export type IsMfaRequiredWindowsDesktop = { + windows_desktop: { + // desktop_name is the Windows Desktop server name. + desktop_name: string; + // login is the Windows desktop user login. + login: string; + }; +}; + +export type IsMfaRequiredKube = { + kube: { + // cluster_name is the name of the kube cluster. + cluster_name: string; + }; +}; + +export type IsMfaRequiredRequest = + | IsMfaRequiredDatabase + | IsMfaRequiredNode + | IsMfaRequiredKube + | IsMfaRequiredWindowsDesktop;