{
act(() => {
createStep = within(screen.getByTestId('create-step'));
});
- await user.click(createStep.getByLabelText('Hardware Device'));
+ await user.click(createStep.getByLabelText('Security Key'));
await user.click(
createStep.getByRole('button', { name: 'Create an MFA method' })
);
diff --git a/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.tsx b/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.tsx
index 847197e2c12c7..396c594e86f6e 100644
--- a/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.tsx
+++ b/web/packages/teleport/src/Account/ManageDevices/wizards/AddAuthDeviceWizard.tsx
@@ -268,7 +268,7 @@ function CreateMfaBox({
}) {
// Be more specific about the WebAuthn device type (it's not a passkey).
mfaRegisterOptions = mfaRegisterOptions.map((o: MfaOption) =>
- o.value === 'webauthn' ? { ...o, label: 'Hardware Device' } : o
+ o.value === 'webauthn' ? { ...o, label: 'Security Key' } : o
);
return (
diff --git a/web/packages/teleport/src/Account/ManageDevices/wizards/ReauthenticateStep.tsx b/web/packages/teleport/src/Account/ManageDevices/wizards/ReauthenticateStep.tsx
index b7e703daef72c..c36e133e73c7b 100644
--- a/web/packages/teleport/src/Account/ManageDevices/wizards/ReauthenticateStep.tsx
+++ b/web/packages/teleport/src/Account/ManageDevices/wizards/ReauthenticateStep.tsx
@@ -46,9 +46,9 @@ export function ReauthenticateStep({
refCallback,
stepIndex,
flowLength,
- reauthAttempt: attempt,
mfaChallengeOptions,
- clearReauthAttempt: clearAttempt,
+ reauthAttempt,
+ clearReauthAttempt,
submitWithMfa,
onClose,
}: ReauthenticateStepProps) {
@@ -68,7 +68,7 @@ export function ReauthenticateStep({
submitWithMfa(mfaOption, otpCode).then(next);
};
- const errorMessage = getReauthenticationErrorMessage(attempt);
+ const errorMessage = getReauthenticationErrorMessage(reauthAttempt);
return (
@@ -94,7 +94,7 @@ export function ReauthenticateStep({
mb={4}
onChange={o => {
setMfaOption(o as DeviceType);
- clearAttempt();
+ clearReauthAttempt();
}}
/>
{mfaOption === 'totp' && (
@@ -106,7 +106,7 @@ export function ReauthenticateStep({
value={otpCode}
placeholder="123 456"
onChange={onOtpCodeChanged}
- readonly={attempt.status === 'processing'}
+ readonly={reauthAttempt.status === 'processing'}
/>
)}
diff --git a/web/packages/teleport/src/Account/PasswordBox.tsx b/web/packages/teleport/src/Account/PasswordBox.tsx
index f4ce622668517..bef6f8220745a 100644
--- a/web/packages/teleport/src/Account/PasswordBox.tsx
+++ b/web/packages/teleport/src/Account/PasswordBox.tsx
@@ -78,9 +78,10 @@ export function PasswordBox({
{dialogOpen && (
dev.usage === 'passwordless')
+ }
onClose={() => setDialogOpen(false)}
onSuccess={onSuccess}
/>
diff --git a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx
index e3e784e691c44..bf107c81164ea 100644
--- a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx
+++ b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx
@@ -52,5 +52,6 @@ const props: State = {
MFA_OPTION_SSO_DEFAULT,
],
submitWithMfa: () => null,
+ submitWithPasswordless: () => null,
onClose: () => null,
};
diff --git a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts
index 4d9e991277827..1e7d1337db3f2 100644
--- a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts
+++ b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts
@@ -102,6 +102,21 @@ export default function useReAuthenticate(props: ReauthProps): ReauthState {
.catch(handleError);
}
+ function submitWithPasswordless() {
+ setAttempt({ status: 'processing' });
+ // Always get a new passwordless challenge, the challenge stored in state is for mfa
+ // and will also be overwritten in the backend by the passwordless challenge.
+ return auth
+ .getMfaChallenge({
+ scope: props.challengeScope,
+ userVerificationRequirement: 'required',
+ })
+ .then(chal => auth.getMfaChallengeResponse(chal, 'webauthn'))
+ .then(props.onMfaResponse)
+ .finally(clearMfaChallenge)
+ .catch(handleError);
+ }
+
function clearAttempt() {
setAttempt({ status: '' });
}
@@ -112,6 +127,7 @@ export default function useReAuthenticate(props: ReauthProps): ReauthState {
getMfaChallenge,
getMfaChallengeOptions,
submitWithMfa,
+ submitWithPasswordless,
};
}
@@ -128,4 +144,5 @@ export type ReauthState = {
getMfaChallenge: () => Promise;
getMfaChallengeOptions: () => Promise;
submitWithMfa: (mfaType?: DeviceType, totp_code?: string) => Promise;
+ submitWithPasswordless: () => Promise;
};
diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts
index c26a5fe0641f4..a818eda3e1f5b 100644
--- a/web/packages/teleport/src/services/auth/auth.ts
+++ b/web/packages/teleport/src/services/auth/auth.ts
@@ -210,17 +210,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);
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.ts b/web/packages/teleport/src/services/mfa/mfaOptions.ts
index 5f63b7ecdb12c..4137c3562d5f4 100644
--- a/web/packages/teleport/src/services/mfa/mfaOptions.ts
+++ b/web/packages/teleport/src/services/mfa/mfaOptions.ts
@@ -32,7 +32,7 @@ export function getMfaChallengeOptions(mfaChallenge: MfaAuthenticateChallenge) {
}
if (mfaChallenge?.ssoChallenge) {
- mfaOptions.push(getSsoOption(mfaChallenge.ssoChallenge));
+ mfaOptions.push(getSsoMfaOption(mfaChallenge.ssoChallenge));
}
return mfaOptions;
@@ -67,18 +67,18 @@ export const MFA_OPTION_TOTP: MfaOption = {
label: 'Authenticator App',
};
-// used in tests, returned by getSsoOptions(null).
+// SSO MFA option used in tests.
export const MFA_OPTION_SSO_DEFAULT: MfaOption = {
value: 'sso',
label: 'SSO',
};
-const getSsoOption = (ssoChallenge: SSOChallenge): MfaOption => {
+const getSsoMfaOption = (ssoChallenge: SSOChallenge): MfaOption => {
return {
value: 'sso',
label:
- ssoChallenge.device?.displayName ||
- ssoChallenge.device?.connectorId ||
+ ssoChallenge?.device?.displayName ||
+ ssoChallenge?.device?.connectorId ||
'SSO',
};
};