diff --git a/web/packages/teleport/src/Account/Account.test.tsx b/web/packages/teleport/src/Account/Account.test.tsx index 7dcf86f471adb..3ca45002ffaee 100644 --- a/web/packages/teleport/src/Account/Account.test.tsx +++ b/web/packages/teleport/src/Account/Account.test.tsx @@ -243,7 +243,7 @@ test('adding an MFA device', async () => { const user = userEvent.setup(); const ctx = createTeleportContext(); jest.spyOn(ctx.mfaService, 'fetchDevices').mockResolvedValue([testPasskey]); - jest.spyOn(auth, 'getChallenge').mockResolvedValue({ + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValue({ webauthnPublicKey: null, totpChallenge: true, ssoChallenge: null, @@ -327,7 +327,7 @@ test('removing an MFA method', async () => { const user = userEvent.setup(); const ctx = createTeleportContext(); jest.spyOn(ctx.mfaService, 'fetchDevices').mockResolvedValue([testMfaMethod]); - jest.spyOn(auth, 'getChallenge').mockResolvedValue({ + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValue({ webauthnPublicKey: null, totpChallenge: false, ssoChallenge: null, diff --git a/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.test.tsx b/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.test.tsx index 492685b5e1597..d23469ead98fd 100644 --- a/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.test.tsx +++ b/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.test.tsx @@ -86,6 +86,7 @@ beforeEach(() => { user = userEvent.setup(); onSuccess = jest.fn(); + jest.spyOn(auth, 'getMfaChallenge').mockResolvedValueOnce(undefined); jest .spyOn(auth, 'getMfaChallengeResponse') .mockResolvedValueOnce(dummyChallengeResponse); @@ -107,6 +108,7 @@ describe('with passwordless reauthentication', () => { scope: MfaChallengeScope.CHANGE_PASSWORD, userVerificationRequirement: 'required', }); + expect(auth.getMfaChallengeResponse).toHaveBeenCalled(); } it('changes password', async () => { @@ -127,7 +129,7 @@ describe('with passwordless reauthentication', () => { oldPassword: '', newPassword: 'new-pass1234', secondFactorToken: '', - credential: dummyChallengeResponse.webauthn_response, + webauthnResponse: dummyChallengeResponse.webauthn_response, }); expect(onSuccess).toHaveBeenCalled(); }); @@ -194,7 +196,7 @@ describe('with WebAuthn MFA reauthentication', () => { ); await user.click(reauthenticateStep.getByText('MFA Device')); await user.click(reauthenticateStep.getByText('Next')); - expect(auth.getMfaChallengeResponse).toHaveBeenCalledWith({ + expect(auth.getMfaChallenge).toHaveBeenCalledWith({ scope: MfaChallengeScope.CHANGE_PASSWORD, userVerificationRequirement: 'discouraged', }); @@ -222,7 +224,7 @@ describe('with WebAuthn MFA reauthentication', () => { oldPassword: 'current-pass', newPassword: 'new-pass1234', secondFactorToken: '', - credential: dummyChallengeResponse.webauthn_response, + webauthnResponse: dummyChallengeResponse.webauthn_response, }); expect(onSuccess).toHaveBeenCalled(); }); @@ -296,7 +298,7 @@ describe('with OTP MFA reauthentication', () => { ); await user.click(reauthenticateStep.getByText('Authenticator App')); await user.click(reauthenticateStep.getByText('Next')); - expect(auth.getMfaChallengeResponse).not.toHaveBeenCalled(); + expect(auth.getMfaChallenge).not.toHaveBeenCalled(); } it('changes password', async () => { @@ -420,11 +422,11 @@ describe('without reauthentication', () => { 'new-pass1234' ); await user.click(changePasswordStep.getByText('Save Changes')); - expect(auth.getMfaChallengeResponse).not.toHaveBeenCalled(); + expect(auth.getMfaChallenge).not.toHaveBeenCalled(); expect(auth.changePassword).toHaveBeenCalledWith({ oldPassword: 'current-pass', newPassword: 'new-pass1234', - credential: undefined, + webauthnResponse: undefined, secondFactorToken: '', }); expect(onSuccess).toHaveBeenCalled(); diff --git a/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.tsx b/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.tsx index 1a152f2ddcfbf..18c5445e01bb7 100644 --- a/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.tsx +++ b/web/packages/teleport/src/Account/ChangePasswordWizard/ChangePasswordWizard.tsx @@ -38,7 +38,7 @@ import Box from 'design/Box'; import { ChangePasswordReq } from 'teleport/services/auth'; import auth, { MfaChallengeScope } from 'teleport/services/auth/auth'; -import { MfaDevice } from 'teleport/services/mfa'; +import { MfaDevice, WebauthnAssertionResponse } from 'teleport/services/mfa'; export interface ChangePasswordWizardProps { /** MFA type setting, as configured in the cluster's configuration. */ @@ -66,7 +66,8 @@ export function ChangePasswordWizard({ const [reauthMethod, setReauthMethod] = useState( reauthOptions[0]?.value ); - const [credential, setCredential] = useState(); + const [webauthnResponse, setWebauthnResponse] = + useState(); const reauthRequired = reauthOptions.length > 0; return ( @@ -84,9 +85,9 @@ export function ChangePasswordWizard({ // Step properties reauthOptions={reauthOptions} reauthMethod={reauthMethod} - credential={credential} onReauthMethodChange={setReauthMethod} - onAuthenticated={setCredential} + webauthnResponse={webauthnResponse} + onWebauthnResponse={setWebauthnResponse} onClose={onClose} onSuccess={onSuccess} /> @@ -154,7 +155,7 @@ interface ReauthenticateStepProps { reauthOptions: ReauthenticationOption[]; reauthMethod: ReauthenticationMethod; onReauthMethodChange(method: ReauthenticationMethod): void; - onAuthenticated(res: Credential): void; + onWebauthnResponse(res: WebauthnAssertionResponse): void; onClose(): void; } @@ -166,7 +167,7 @@ export function ReauthenticateStep({ reauthOptions, reauthMethod, onReauthMethodChange, - onAuthenticated, + onWebauthnResponse, onClose, }: ChangePasswordWizardStepProps) { const [reauthenticateAttempt, reauthenticate] = useAsync( @@ -181,7 +182,7 @@ export function ReauthenticateStep({ const response = await auth.getMfaChallengeResponse(challenge); // TODO(Joerger): handle non-webauthn response. - onAuthenticated(response.webauthn_response); + onWebauthnResponse(response.webauthn_response); } next(); } @@ -229,7 +230,7 @@ export function ReauthenticateStep({ } interface ChangePasswordStepProps { - credential: Credential; + webauthnResponse: WebauthnAssertionResponse; reauthMethod: ReauthenticationMethod; onClose(): void; onSuccess(): void; @@ -240,7 +241,7 @@ export function ChangePasswordStep({ prev, stepIndex, flowLength, - credential, + webauthnResponse, reauthMethod, onClose, onSuccess, @@ -279,7 +280,7 @@ export function ChangePasswordStep({ oldPassword, newPassword, secondFactorToken: authCode, - credential, + webauthnResponse, }); } diff --git a/web/packages/teleport/src/lib/useMfa.ts b/web/packages/teleport/src/lib/useMfa.ts index d5c82e678d3b9..664016790e002 100644 --- a/web/packages/teleport/src/lib/useMfa.ts +++ b/web/packages/teleport/src/lib/useMfa.ts @@ -20,14 +20,12 @@ import { useState, useEffect, useCallback } from 'react'; import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender'; import { TermEvent } from 'teleport/lib/term/enums'; -import { - parseMfaChallengeJson as parseMfaChallenge, - makeWebauthnAssertionResponse, -} from 'teleport/services/mfa/makeMfa'; +import { parseMfaChallengeJson as parseMfaChallenge } from 'teleport/services/mfa/makeMfa'; import { MfaAuthenticateChallengeJson, SSOChallenge, } from 'teleport/services/mfa'; +import auth from 'teleport/services/auth/auth'; export function useMfa(emitterSender: EventEmitterMfaSender): MfaState { const [state, setState] = useState<{ @@ -86,16 +84,17 @@ export function useMfa(emitterSender: EventEmitterMfaSender): MfaState { return; } - navigator.credentials - .get({ publicKey: state.webauthnPublicKey }) + auth + .getMfaChallengeResponse({ + webauthnPublicKey: state.webauthnPublicKey, + }) .then(res => { setState(prevState => ({ ...prevState, errorText: '', webauthnPublicKey: null, })); - const credential = makeWebauthnAssertionResponse(res); - emitterSender.sendWebAuthn(credential); + emitterSender.sendWebAuthn(res.webauthn_response); }) .catch((err: Error) => { setErrorText(err.message); diff --git a/web/packages/teleport/src/services/api/api.test.ts b/web/packages/teleport/src/services/api/api.test.ts index b38849561f087..deccdfd1a1867 100644 --- a/web/packages/teleport/src/services/api/api.test.ts +++ b/web/packages/teleport/src/services/api/api.test.ts @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import { MfaChallengeResponse } from '../mfa'; import api, { MFA_HEADER, defaultRequestOptions, @@ -26,18 +27,20 @@ import api, { describe('api.fetch', () => { const mockedFetch = jest.spyOn(global, 'fetch').mockResolvedValue({} as any); // we don't care about response - const webauthnResp = { - id: 'some-id', - type: 'some-type', - extensions: { - appid: false, - }, - rawId: 'some-raw-id', - response: { - authenticatorData: 'authen-data', - clientDataJSON: 'client-data-json', - signature: 'signature', - userHandle: 'user-handle', + const mfaResp: MfaChallengeResponse = { + webauthn_response: { + id: 'some-id', + type: 'some-type', + extensions: { + appid: false, + }, + rawId: 'some-raw-id', + response: { + authenticatorData: 'authen-data', + clientDataJSON: 'client-data-json', + signature: 'signature', + userHandle: 'user-handle', + }, }, }; @@ -88,7 +91,7 @@ describe('api.fetch', () => { }); test('with webauthnResponse', async () => { - await api.fetch('/something', undefined, webauthnResp); + await api.fetch('/something', undefined, mfaResp); expect(mockedFetch).toHaveBeenCalledTimes(1); const firstCall = mockedFetch.mock.calls[0]; @@ -100,14 +103,14 @@ describe('api.fetch', () => { ...defaultRequestOptions.headers, ...getAuthHeaders(), [MFA_HEADER]: JSON.stringify({ - webauthnAssertionResponse: webauthnResp, + webauthnAssertionResponse: mfaResp.webauthn_response, }), }, }); }); test('with customOptions and webauthnResponse', async () => { - await api.fetch('/something', customOpts, webauthnResp); + await api.fetch('/something', customOpts, mfaResp); expect(mockedFetch).toHaveBeenCalledTimes(1); const firstCall = mockedFetch.mock.calls[0]; @@ -120,7 +123,7 @@ describe('api.fetch', () => { ...customOpts.headers, ...getAuthHeaders(), [MFA_HEADER]: JSON.stringify({ - webauthnAssertionResponse: webauthnResp, + webauthnAssertionResponse: mfaResp.webauthn_response, }), }, }); diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts index 410b926d859ba..aaa7541a1257d 100644 --- a/web/packages/teleport/src/services/auth/auth.ts +++ b/web/packages/teleport/src/services/auth/auth.ts @@ -212,14 +212,13 @@ const auth = { oldPassword, newPassword, secondFactorToken, - credential, + webauthnResponse, }: ChangePasswordReq) { const data = { old_password: base64EncodeUnicode(oldPassword), new_password: base64EncodeUnicode(newPassword), second_factor_token: secondFactorToken, - webauthnAssertionResponse: - credential && makeWebauthnAssertionResponse(credential), + webauthnAssertionResponse: webauthnResponse, }; return api.put(cfg.api.changeUserPasswordPath, data); @@ -342,9 +341,7 @@ const auth = { .then(res => api.post(cfg.api.createPrivilegeTokenPath, { // TODO(Joerger): Handle non-webauthn challenges. - webauthnAssertionResponse: makeWebauthnAssertionResponse( - res.webauthn_response - ), + webauthnAssertionResponse: res.webauthn_response, }) ); }, @@ -390,7 +387,7 @@ const auth = { return auth .getMfaChallenge({ scope, allowReuse, isMfaRequiredRequest }, abortSignal) .then(challenge => auth.getMfaChallengeResponse(challenge)) - .then(res => makeWebauthnAssertionResponse(res.webauthn_response)); + .then(res => res.webauthn_response); }, getMfaChallengeResponseForAdminAction(allowReuse?: boolean) { diff --git a/web/packages/teleport/src/services/auth/types.ts b/web/packages/teleport/src/services/auth/types.ts index ae9818ef2ebb7..7c74f666d9db1 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 } from '../mfa'; +import { DeviceUsage, WebauthnAssertionResponse } from '../mfa'; import { IsMfaRequiredRequest, MfaChallengeScope } from './auth'; @@ -74,7 +74,7 @@ export type ChangePasswordReq = { oldPassword: string; newPassword: string; secondFactorToken: string; - credential?: Credential; + webauthnResponse?: WebauthnAssertionResponse; }; export type CreateNewHardwareDeviceRequest = {