) => {
+ setOtpCode(e.target.value);
};
const onReauthenticate = (
@@ -76,20 +57,11 @@ export function ReauthenticateStep({
) => {
e.preventDefault();
if (!validator.validate()) return;
- if (mfaOption === 'webauthn') {
- submitWithWebauthn();
- }
- if (mfaOption === 'otp') {
- submitWithTotp(authCode);
- }
+ submitWithMfa(mfaOption, 'mfa', otpCode).then(([, err]) => {
+ if (!err) next();
+ });
};
- const errorMessage = getReauthenticationErrorMessage(
- auth2faType,
- mfaOptions.length,
- attempt
- );
-
return (
@@ -99,7 +71,9 @@ export function ReauthenticateStep({
title="Verify Identity"
/>
- {errorMessage && {errorMessage}}
+ {submitAttempt.status === 'error' && (
+ {submitAttempt.statusText}
+ )}
{mfaOption && Multi-factor type}
{({ validator }) => (
@@ -113,20 +87,20 @@ export function ReauthenticateStep({
gap={3}
mb={4}
onChange={o => {
- setMfaOption(o as Auth2faType);
- clearAttempt();
+ setMfaOption(o as DeviceType);
+ clearSubmitAttempt();
}}
/>
- {mfaOption === 'otp' && (
+ {mfaOption === 'totp' && (
)}
@@ -150,64 +124,3 @@ export function ReauthenticateStep({
);
}
-function getReauthenticationErrorMessage(
- auth2faType: Auth2faType,
- numMfaOptions: number,
- attempt: Attempt
-): string {
- if (numMfaOptions === 0) {
- switch (auth2faType) {
- case 'on':
- return (
- "Identity verification is required, but you don't have any" +
- 'passkeys or MFA methods registered. This may mean that the' +
- 'server configuration has changed. Please contact your ' +
- 'administrator.'
- );
- case 'otp':
- return (
- 'Identity verification using authenticator app is required, but ' +
- "you don't have any authenticator apps registered. This may mean " +
- 'that the server configuration has changed. Please contact your ' +
- 'administrator.'
- );
- case 'webauthn':
- return (
- 'Identity verification using a passkey or security key is required, but ' +
- "you don't have any such devices registered. This may mean " +
- 'that the server configuration has changed. Please contact your ' +
- 'administrator.'
- );
- case 'optional':
- case 'off':
- // This error message is not useful, but this condition should never
- // happen, and if it does, it means something is broken, and we don't
- // have a clue anyway.
- return 'Unable to verify identity';
- default:
- auth2faType satisfies never;
- }
- }
-
- if (attempt.status === 'failed') {
- // This message relies on the status message produced by the auth server in
- // lib/auth/Server.checkOTP function. Please keep these in sync.
- if (attempt.statusText === 'invalid totp token') {
- return 'Invalid authenticator code';
- } else {
- return attempt.statusText;
- }
- }
-}
-
-export function createReauthOptions(
- auth2faType: Auth2faType,
- devices: MfaDevice[]
-): MfaOption[] {
- return createMfaOptions({ auth2faType, required: true }).filter(
- ({ value }) => {
- const deviceType = value === 'otp' ? 'totp' : value;
- return devices.some(({ type }) => type === deviceType);
- }
- );
-}
diff --git a/web/packages/teleport/src/Account/PasswordBox.tsx b/web/packages/teleport/src/Account/PasswordBox.tsx
index d8d99ea287075..ea136156d0120 100644
--- a/web/packages/teleport/src/Account/PasswordBox.tsx
+++ b/web/packages/teleport/src/Account/PasswordBox.tsx
@@ -33,14 +33,12 @@ import { ChangePasswordWizard } from './ChangePasswordWizard';
import { StatePill, AuthMethodState } from './StatePill';
export interface PasswordBoxProps {
- changeDisabled: boolean;
devices: MfaDevice[];
passwordState: PasswordState;
onPasswordChange: () => void;
}
export function PasswordBox({
- changeDisabled,
devices,
passwordState,
onPasswordChange,
@@ -67,10 +65,7 @@ export function PasswordBox({
}
icon={}
actions={
- setDialogOpen(true)}
- >
+ setDialogOpen(true)}>
Change Password
}
@@ -78,9 +73,10 @@ export function PasswordBox({
{dialogOpen && (
dev.usage === 'passwordless')
+ }
onClose={() => setDialogOpen(false)}
onSuccess={onSuccess}
/>
diff --git a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx
index 1b5ae9e6a780a..eddce1b82a260 100644
--- a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx
+++ b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx
@@ -170,13 +170,13 @@ export function TestConnection(props: AgentStepProps) {
{showMfaDialog && (
- testConnection({
+ onMfaResponse={async res => {
+ await testConnection({
login: selectedLoginOpt.value,
sshPrincipalSelectionMode,
mfaResponse: res,
- })
- }
+ });
+ }}
onClose={cancelMfaDialog}
challengeScope={MfaChallengeScope.USER_SESSION}
/>
diff --git a/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx
index d57a15037a21f..f9162dafd01be 100644
--- a/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx
+++ b/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx
@@ -117,7 +117,7 @@ export function TestConnection() {
{showMfaDialog && (
testConnection(validator, res)}
+ onMfaResponse={async res => testConnection(validator, res)}
onClose={cancelMfaDialog}
challengeScope={MfaChallengeScope.USER_SESSION}
/>
diff --git a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx
index 1c0807560a700..a43fddf1c22a4 100644
--- a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx
+++ b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx
@@ -101,7 +101,9 @@ export function TestConnection({
{showMfaDialog && (
testConnection(makeTestConnRequest(), res)}
+ onMfaResponse={async res =>
+ testConnection(makeTestConnRequest(), res)
+ }
onClose={cancelMfaDialog}
challengeScope={MfaChallengeScope.USER_SESSION}
/>
diff --git a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx
index f72d45081e132..7569906b05cb3 100644
--- a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx
+++ b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx
@@ -87,7 +87,7 @@ export function TestConnection(props: AgentStepProps) {
{showMfaDialog && (
testConnection(selectedOpt.value, res)}
+ onMfaResponse={async res => testConnection(selectedOpt.value, res)}
onClose={cancelMfaDialog}
challengeScope={MfaChallengeScope.USER_SESSION}
/>
diff --git a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx
index 088bedae810a2..80cee8f465ffb 100644
--- a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx
+++ b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx
@@ -16,10 +16,16 @@
* along with this program. If not, see .
*/
-import React from 'react';
+import { makeEmptyAttempt } from 'shared/hooks/useAsync';
-import { State } from './useReAuthenticate';
-import { ReAuthenticate } from './ReAuthenticate';
+import {
+ MFA_OPTION_SSO_DEFAULT,
+ MFA_OPTION_TOTP,
+ MFA_OPTION_WEBAUTHN,
+} from 'teleport/services/mfa';
+
+import { ReAuthenticate, State } from './ReAuthenticate';
+import { ReauthState } from './useReAuthenticate';
export default {
title: 'Teleport/ReAuthenticate',
@@ -28,23 +34,38 @@ export default {
export const Loaded = () => ;
export const Processing = () => (
-
+
);
export const Failed = () => (
);
const props: State = {
- attempt: { status: '' },
- clearAttempt: () => null,
- submitWithTotp: () => null,
- submitWithWebauthn: () => null,
- preferredMfaType: 'webauthn',
+ reauthState: {
+ initAttempt: { status: 'success' },
+ mfaOptions: [MFA_OPTION_WEBAUTHN, MFA_OPTION_TOTP, MFA_OPTION_SSO_DEFAULT],
+ submitWithMfa: async () => null,
+ submitAttempt: makeEmptyAttempt(),
+ clearSubmitAttempt: () => {},
+ } as ReauthState,
+
onClose: () => null,
- auth2faType: 'on',
- actionText: 'performing this action',
};
diff --git a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx
index c344262c7fd62..6dfc8a0f96f3c 100644
--- a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx
+++ b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx
@@ -16,55 +16,88 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
-import { Flex, Box, Text, ButtonPrimary, ButtonSecondary } from 'design';
+import {
+ Box,
+ ButtonPrimary,
+ ButtonSecondary,
+ Flex,
+ Indicator,
+ Text,
+} from 'design';
+import { Alert, Danger } from 'design/Alert';
import Dialog, {
- DialogHeader,
- DialogTitle,
DialogContent,
DialogFooter,
+ DialogHeader,
+ DialogTitle,
} from 'design/Dialog';
-import { Danger } from 'design/Alert';
-import Validation from 'shared/components/Validation';
-import { requiredToken } from 'shared/components/Validation/rules';
+import React, { useEffect, useState } from 'react';
import FieldInput from 'shared/components/FieldInput';
import FieldSelect from 'shared/components/FieldSelect';
-import createMfaOptions, { MfaOption } from 'shared/utils/createMfaOptions';
+import Validation, { Validator } from 'shared/components/Validation';
+import { requiredToken } from 'shared/components/Validation/rules';
+
+import { MfaOption } from 'teleport/services/mfa';
-import useReAuthenticate, { State, Props } from './useReAuthenticate';
+import useReAuthenticate, {
+ ReauthProps,
+ ReauthState,
+} from './useReAuthenticate';
+
+export type Props = ReauthProps & {
+ onClose: () => void;
+};
export default function Container(props: Props) {
const state = useReAuthenticate(props);
- return ;
+ return ;
}
+export type State = {
+ reauthState: ReauthState;
+ onClose: () => void;
+};
+
export function ReAuthenticate({
- attempt,
- clearAttempt,
- submitWithTotp,
- submitWithWebauthn,
onClose,
- auth2faType,
- preferredMfaType,
- actionText,
+ reauthState: {
+ initAttempt,
+ mfaOptions,
+ submitWithMfa,
+ submitAttempt,
+ clearSubmitAttempt,
+ },
}: State) {
- const [otpToken, setOtpToken] = useState('');
- const mfaOptions = createMfaOptions({
- auth2faType: auth2faType,
- preferredType: preferredMfaType,
- required: true,
- });
- const [mfaOption, setMfaOption] = useState(mfaOptions[0]);
+ const [otpCode, setOtpToken] = useState('');
+ const [mfaOption, setMfaOption] = useState();
- function onSubmit(e: React.MouseEvent) {
- e.preventDefault();
+ useEffect(() => {
+ if (mfaOptions?.length) setMfaOption(mfaOptions[0]);
+ }, [mfaOptions]);
- if (mfaOption?.value === 'webauthn') {
- submitWithWebauthn();
- }
- if (mfaOption?.value === 'otp') {
- submitWithTotp(otpToken);
- }
+ // Handle potential error states first.
+ switch (initAttempt.status) {
+ case 'processing':
+ return (
+
+
+
+ );
+ case 'error':
+ return ;
+ case 'success':
+ break;
+ default:
+ return null;
+ }
+
+ function onReauthenticate(
+ e: React.MouseEvent,
+ validator: Validator
+ ) {
+ e.preventDefault();
+ if (!validator.validate()) return;
+ submitWithMfa(mfaOption.value, 'mfa', otpCode);
}
return (
@@ -83,12 +116,12 @@ export function ReAuthenticate({
Verify your identity
You must verify your identity with one of your existing
- two-factor devices before {actionText}.
+ two-factor devices before performing this action.
- {attempt.status === 'failed' && (
+ {submitAttempt.status === 'error' && (
- {attempt.statusText}
+ {submitAttempt.statusText}
)}
@@ -100,25 +133,25 @@ export function ReAuthenticate({
options={mfaOptions}
onChange={(o: MfaOption) => {
setMfaOption(o);
- clearAttempt();
+ clearSubmitAttempt();
}}
data-testid="mfa-select"
mr={3}
mb={0}
- isDisabled={attempt.status === 'processing'}
+ isDisabled={submitAttempt.status === 'processing'}
elevated={true}
/>
- {mfaOption.value === 'otp' && (
+ {mfaOption?.value === 'totp' && (
setOtpToken(e.target.value)}
placeholder="123 456"
- readonly={attempt.status === 'processing'}
+ readonly={submitAttempt.status === 'processing'}
mb={0}
/>
)}
@@ -127,8 +160,8 @@ export function ReAuthenticate({
validator.validate() && onSubmit(e)}
- disabled={attempt.status === 'processing'}
+ onClick={e => onReauthenticate(e, validator)}
+ disabled={submitAttempt.status === 'processing'}
mr={3}
mt={3}
type="submit"
diff --git a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts
index 7b2746de0a25c..ed8c73f3fe6da 100644
--- a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts
+++ b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts
@@ -16,128 +16,141 @@
* along with this program. If not, see .
*/
-import useAttempt from 'shared/hooks/useAttemptNext';
+import { useCallback, useEffect, useState } from 'react';
+import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
-import cfg from 'teleport/config';
import auth from 'teleport/services/auth';
import { MfaChallengeScope } from 'teleport/services/auth/auth';
-import type { MfaChallengeResponse } 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(props.onAuthenticated)
- .catch(handleError);
- }
+import {
+ DeviceType,
+ DeviceUsage,
+ getMfaChallengeOptions,
+ MfaAuthenticateChallenge,
+ MfaChallengeResponse,
+ MfaOption,
+} from 'teleport/services/mfa';
+
+export default function useReAuthenticate({
+ challengeScope,
+ onMfaResponse,
+}: ReauthProps): ReauthState {
+ const [mfaOptions, setMfaOptions] = useState();
+ const [challengeState, setChallengeState] = useState();
+
+ const [initAttempt, init] = useAsync(async () => {
+ const challenge = await auth.getMfaChallenge({
+ scope: challengeScope,
+ });
+
+ setChallengeState({ challenge, deviceUsage: 'mfa' });
+ setMfaOptions(getMfaChallengeOptions(challenge));
+ });
+
+ useEffect(() => {
+ init();
+ }, []);
+
+ const getChallenge = useCallback(
+ async (deviceUsage: DeviceUsage = 'mfa') => {
+ if (challengeState?.deviceUsage === deviceUsage) {
+ return challengeState.challenge;
+ }
+
+ // If the challenge state is empty, used, or has different args,
+ // retrieve a new mfa challenge and set it in the state.
+ const challenge = await auth.getMfaChallenge({
+ scope: challengeScope,
+ userVerificationRequirement:
+ deviceUsage === 'passwordless' ? 'required' : 'discouraged',
+ });
+ setChallengeState({
+ challenge,
+ deviceUsage,
+ });
+ return challenge;
+ },
+ [challengeState, challengeScope]
+ );
+
+ const [submitAttempt, submitWithMfa, setSubmitAttempt] = useAsync(
+ useCallback(
+ async (
+ mfaType?: DeviceType,
+ deviceUsage?: DeviceUsage,
+ totpCode?: string
+ ) => {
+ const challenge = await getChallenge(deviceUsage);
+
+ let response: MfaChallengeResponse;
+ try {
+ response = await auth.getMfaChallengeResponse(
+ challenge,
+ mfaType,
+ totpCode
+ );
+ } catch (err) {
+ throw new Error(getReAuthenticationErrorMessage(err));
+ }
- function submitWithWebauthn() {
- setAttempt({ status: 'processing' });
-
- if ('onMfaResponse' in props) {
- auth
- .getMfaChallenge({ scope: props.challengeScope })
- .then(challenge => auth.getMfaChallengeResponse(challenge, 'webauthn'))
- .catch(handleError);
-
- return;
- }
-
- auth
- .createPrivilegeTokenWithWebauthn()
- .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 (
- err.message.includes('attempt was made to use an object that is not')
- ) {
- setAttempt({
- status: 'failed',
- statusText:
- 'The two-factor device you used is not registered on this account. You must verify using a device that has already been registered.',
- });
- } else {
- setAttempt({ status: 'failed', statusText: err.message });
+ try {
+ await onMfaResponse(response);
+ } finally {
+ // once onMfaResponse is called, assume the challenge
+ // has been consumed and clear the state.
+ setChallengeState(null);
}
- });
- }
+ },
+ [getChallenge, onMfaResponse]
+ )
+ );
- function clearAttempt() {
- setAttempt({ status: '' });
+ function clearSubmitAttempt() {
+ setSubmitAttempt(makeEmptyAttempt());
}
return {
- attempt,
- clearAttempt,
- submitWithTotp,
- submitWithWebauthn,
- auth2faType: cfg.getAuth2faType(),
- preferredMfaType: cfg.getPreferredMfaType(),
- actionText,
- onClose,
+ initAttempt,
+ mfaOptions,
+ submitWithMfa,
+ submitAttempt,
+ clearSubmitAttempt,
};
}
-const defaultActionText = 'performing this action';
-
-type BaseProps = {
- onClose?: () => void;
- /**
- * The text that will be appended to the text in the re-authentication dialog.
- *
- * Default value: "performing this action"
- *
- * Example: If `actionText` is set to "registering a new device" then the dialog will say
- * "You must verify your identity with one of your existing two-factor devices before registering a new device."
- *
- * */
- actionText?: string;
+export type ReauthProps = {
+ challengeScope: MfaChallengeScope;
+ onMfaResponse(res: MfaChallengeResponse): Promise;
};
-// MfaResponseProps defines a function
-// that accepts a MFA response. No
-// authentication has been done at this point.
-type MfaResponseProps = BaseProps & {
- onMfaResponse(res: MfaChallengeResponse): void;
- /**
- * The MFA challenge scope of the action to perform, as defined in webauthn.proto.
- */
- challengeScope: MfaChallengeScope;
- onAuthenticated?: never;
+export type ReauthState = {
+ initAttempt: Attempt;
+ mfaOptions: MfaOption[];
+ submitWithMfa: (
+ mfaType?: DeviceType,
+ deviceUsage?: DeviceUsage,
+ totpCode?: string
+ ) => Promise<[void, Error]>;
+ submitAttempt: Attempt;
+ clearSubmitAttempt: () => void;
};
-// 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;
- challengeScope?: never;
+type challengeState = {
+ challenge: MfaAuthenticateChallenge;
+ deviceUsage: DeviceUsage;
};
-export type Props = MfaResponseProps | DefaultProps;
+function getReAuthenticationErrorMessage(err: Error): string {
+ if (err.message.includes('attempt was made to use an object that is not')) {
+ // Catch a webauthn frontend error that occurs on Firefox and replace it with a more helpful error message.
+ return 'The two-factor device you used is not registered on this account. You must verify using a device that has already been registered.';
+ }
+
+ if (err.message === 'invalid totp token') {
+ // This message relies on the status message produced by the auth server in
+ // lib/auth/Server.checkOTP function. Please keep these in sync.
+ return 'Invalid authenticator code';
+ }
-export type State = ReturnType;
+ return err.message;
+}
diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts
index f0f30f7356b6d..3724f1dc8b056 100644
--- a/web/packages/teleport/src/services/auth/auth.ts
+++ b/web/packages/teleport/src/services/auth/auth.ts
@@ -208,17 +208,12 @@ const auth = {
});
},
- changePassword({
- oldPassword,
- newPassword,
- secondFactorToken,
- webauthnResponse,
- }: ChangePasswordReq) {
+ changePassword({ oldPassword, newPassword, mfaResponse }: ChangePasswordReq) {
const data = {
old_password: base64EncodeUnicode(oldPassword),
new_password: base64EncodeUnicode(newPassword),
- second_factor_token: secondFactorToken,
- webauthnAssertionResponse: webauthnResponse,
+ second_factor_token: mfaResponse.totp_code,
+ webauthnAssertionResponse: mfaResponse.webauthn_response,
};
return api.put(cfg.api.changeUserPasswordPath, data);
@@ -259,10 +254,6 @@ const auth = {
return api.put(cfg.getHeadlessSsoPath(transactionId), request);
},
- createPrivilegeTokenWithTotp(secondFactorToken: string) {
- return api.post(cfg.api.createPrivilegeTokenPath, { secondFactorToken });
- },
-
// getChallenge gets an MFA challenge for the provided parameters. If is_mfa_required_req
// is provided and it is found that MFA is not required, returns null instead.
async getMfaChallenge(
@@ -332,18 +323,27 @@ const auth = {
);
},
- // TODO(Joerger): Combine with otp endpoint.
+ createPrivilegeToken(existingMfaResponse?: MfaChallengeResponse) {
+ return api.post(cfg.api.createPrivilegeTokenPath, {
+ existingMfaResponse,
+ // TODO(Joerger): DELETE IN v19.0.0
+ // Also provide totp/webauthn response in backwards compatible format.
+ secondFactorToken: existingMfaResponse?.totp_code,
+ webauthnAssertionResponse: existingMfaResponse?.webauthn_response,
+ });
+ },
+
+ // TODO(Joerger): Delete once no longer used by /e
createPrivilegeTokenWithWebauthn() {
- // Creating privilege tokens always expects the MANAGE_DEVICES webauthn scope.
return auth
.getMfaChallenge({ scope: MfaChallengeScope.MANAGE_DEVICES })
.then(auth.getMfaChallengeResponse)
- .then(res =>
- api.post(cfg.api.createPrivilegeTokenPath, {
- // TODO(Joerger): Handle non-webauthn challenges.
- webauthnAssertionResponse: res.webauthn_response,
- })
- );
+ .then(mfaResp => auth.createPrivilegeToken(mfaResp));
+ },
+
+ // TODO(Joerger): Delete once no longer used by /e
+ createPrivilegeTokenWithTotp(secondFactorToken: string) {
+ return api.post(cfg.api.createPrivilegeTokenPath, { secondFactorToken });
},
createRestrictedPrivilegeToken() {
diff --git a/web/packages/teleport/src/services/auth/types.ts b/web/packages/teleport/src/services/auth/types.ts
index 7c74f666d9db1..57fb003ab7975 100644
--- a/web/packages/teleport/src/services/auth/types.ts
+++ b/web/packages/teleport/src/services/auth/types.ts
@@ -18,7 +18,7 @@
import { EventMeta } from 'teleport/services/userEvent';
-import { DeviceUsage, WebauthnAssertionResponse } from '../mfa';
+import { DeviceUsage, MfaChallengeResponse } from '../mfa';
import { IsMfaRequiredRequest, MfaChallengeScope } from './auth';
@@ -73,8 +73,7 @@ export type CreateAuthenticateChallengeRequest = {
export type ChangePasswordReq = {
oldPassword: string;
newPassword: string;
- secondFactorToken: string;
- webauthnResponse?: WebauthnAssertionResponse;
+ mfaResponse?: MfaChallengeResponse;
};
export type CreateNewHardwareDeviceRequest = {
diff --git a/web/packages/teleport/src/services/mfa/mfaOptions.test.ts b/web/packages/teleport/src/services/mfa/mfaOptions.test.ts
index 5c430a05be8a8..81eec4be87a80 100644
--- a/web/packages/teleport/src/services/mfa/mfaOptions.test.ts
+++ b/web/packages/teleport/src/services/mfa/mfaOptions.test.ts
@@ -94,7 +94,7 @@ describe('test retrieving mfa options from MFA Challenge', () => {
webauthnPublicKey: {} as PublicKeyCredentialRequestOptions,
ssoChallenge: Object.create(SSOChallenge),
},
- expect: ['webauthn', 'totp', 'sso'],
+ expect: ['webauthn', 'sso', 'totp'],
},
];
diff --git a/web/packages/teleport/src/services/mfa/mfaOptions.ts b/web/packages/teleport/src/services/mfa/mfaOptions.ts
index 4bbe1dceb65f1..96510d31e668f 100644
--- a/web/packages/teleport/src/services/mfa/mfaOptions.ts
+++ b/web/packages/teleport/src/services/mfa/mfaOptions.ts
@@ -20,6 +20,7 @@ import { Auth2faType } from 'shared/services';
import { DeviceType, MfaAuthenticateChallenge, SSOChallenge } from './types';
+// returns mfa challenge options in order of preferences: WebAuthn > SSO > TOTP.
export function getMfaChallengeOptions(mfaChallenge: MfaAuthenticateChallenge) {
const mfaOptions: MfaOption[] = [];
@@ -27,12 +28,12 @@ export function getMfaChallengeOptions(mfaChallenge: MfaAuthenticateChallenge) {
mfaOptions.push(MFA_OPTION_WEBAUTHN);
}
- if (mfaChallenge?.totpChallenge) {
- mfaOptions.push(MFA_OPTION_TOTP);
+ if (mfaChallenge?.ssoChallenge) {
+ mfaOptions.push(getSsoMfaOption(mfaChallenge.ssoChallenge));
}
- if (mfaChallenge?.ssoChallenge) {
- mfaOptions.push(getSsoOption(mfaChallenge.ssoChallenge));
+ if (mfaChallenge?.totpChallenge) {
+ mfaOptions.push(MFA_OPTION_TOTP);
}
return mfaOptions;
@@ -57,22 +58,28 @@ export type MfaOption = {
label: string;
};
-const MFA_OPTION_WEBAUTHN: MfaOption = {
+export const MFA_OPTION_WEBAUTHN: MfaOption = {
value: 'webauthn',
label: 'Passkey or Security Key',
};
-const MFA_OPTION_TOTP: MfaOption = {
+export const MFA_OPTION_TOTP: MfaOption = {
value: 'totp',
label: 'Authenticator App',
};
-const getSsoOption = (ssoChallenge: SSOChallenge): MfaOption => {
+// SSO MFA option used in tests.
+export const MFA_OPTION_SSO_DEFAULT: MfaOption = {
+ value: 'sso',
+ label: 'SSO',
+};
+
+const getSsoMfaOption = (ssoChallenge: SSOChallenge): MfaOption => {
return {
value: 'sso',
label:
- ssoChallenge.device?.displayName ||
- ssoChallenge.device?.connectorId ||
+ ssoChallenge?.device?.displayName ||
+ ssoChallenge?.device?.connectorId ||
'SSO',
};
};