diff --git a/lib/web/mfa.go b/lib/web/mfa.go index 8a6dbf76b9ca7..6d36a4df8c1c6 100644 --- a/lib/web/mfa.go +++ b/lib/web/mfa.go @@ -132,7 +132,9 @@ func (h *Handler) addMFADeviceHandle(w http.ResponseWriter, r *http.Request, par } type createAuthenticateChallengeRequest struct { - IsMFARequired *isMFARequiredRequest `json:"is_mfa_required"` + IsMFARequiredRequest *isMFARequiredRequest `json:"is_mfa_required_req"` + ChallengeScope int `json:"challenge_scope"` + ChallengeAllowReuse bool `json:"challenge_allow_reuse"` } // createAuthenticateChallengeHandle creates and returns MFA authentication challenges for the user in context (logged in user). @@ -148,23 +150,27 @@ func (h *Handler) createAuthenticateChallengeHandle(w http.ResponseWriter, r *ht return nil, trace.Wrap(err) } - var isMFARequiredProtoReq *proto.IsMFARequiredRequest - if req.IsMFARequired != nil { - isMFARequiredProtoReq, err = req.IsMFARequired.checkAndGetProtoRequest() + var mfaRequiredCheckProto *proto.IsMFARequiredRequest + if req.IsMFARequiredRequest != nil { + mfaRequiredCheckProto, err = req.IsMFARequiredRequest.checkAndGetProtoRequest() if err != nil { return nil, trace.Wrap(err) } } + allowReuse := mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO + if req.ChallengeAllowReuse { + allowReuse = mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES + } + chal, err := clt.CreateAuthenticateChallenge(r.Context(), &proto.CreateAuthenticateChallengeRequest{ Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{ ContextUser: &proto.ContextUser{}, }, - MFARequiredCheck: isMFARequiredProtoReq, + MFARequiredCheck: mfaRequiredCheckProto, ChallengeExtensions: &mfav1.ChallengeExtensions{ - // TODO(Joerger): Web client needs to provide scope and allow reuse - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_UNSPECIFIED, - AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_UNSPECIFIED, + Scope: mfav1.ChallengeScope(req.ChallengeScope), + AllowReuse: allowReuse, }, }) if err != nil { diff --git a/web/packages/teleport/src/Account/AccountNew.tsx b/web/packages/teleport/src/Account/AccountNew.tsx index b4f2e9aa5237d..26eec9ec2c7c6 100644 --- a/web/packages/teleport/src/Account/AccountNew.tsx +++ b/web/packages/teleport/src/Account/AccountNew.tsx @@ -28,6 +28,8 @@ import { FeatureBox } from 'teleport/components/Layout'; import ReAuthenticate from 'teleport/components/ReAuthenticate'; import { RemoveDialog } from 'teleport/components/MfaDeviceList'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; + import { AuthDeviceList } from './ManageDevices/AuthDeviceList/AuthDeviceList'; import useManageDevices, { State as ManageDevicesState, @@ -214,6 +216,7 @@ export function Account({ onAuthenticated={setToken} onClose={hideReAuthenticate} actionText="registering a new device" + challengeScope={MFAChallengeScope.USER_SESSION} /> )} {isAddDeviceVisible && ( diff --git a/web/packages/teleport/src/Account/ManageDevices/ManageDevices.tsx b/web/packages/teleport/src/Account/ManageDevices/ManageDevices.tsx index df23b323f9359..b6ed2de99921c 100644 --- a/web/packages/teleport/src/Account/ManageDevices/ManageDevices.tsx +++ b/web/packages/teleport/src/Account/ManageDevices/ManageDevices.tsx @@ -30,6 +30,8 @@ import MfaDeviceList, { RemoveDialog } from 'teleport/components/MfaDeviceList'; import ReAuthenticate from 'teleport/components/ReAuthenticate'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; + import AddDevice from './AddDevice'; import useManageDevices, { State } from './useManageDevices'; @@ -104,6 +106,7 @@ export function ManageDevices({ onAuthenticated={setToken} onClose={hideReAuthenticate} actionText="registering a new device" + challengeScope={MFAChallengeScope.USER_SESSION} /> )} {isAddDeviceVisible && ( diff --git a/web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts b/web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts index 0757e16ce77ef..456e662def85f 100644 --- a/web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts +++ b/web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts @@ -20,7 +20,7 @@ import { useCallback } from 'react'; import useAttempt from 'shared/hooks/useAttemptNext'; import cfg, { UrlScpParams } from 'teleport/config'; -import auth from 'teleport/services/auth/auth'; +import auth, { MFAChallengeScope } from 'teleport/services/auth/auth'; export default function useGetScpUrl(addMfaToScpUrls: boolean) { const { setAttempt, attempt, handleError } = useAttempt(''); @@ -35,7 +35,9 @@ export default function useGetScpUrl(addMfaToScpUrls: boolean) { return cfg.getScpUrl(params); } try { - let webauthn = await auth.getWebauthnResponse(); + let webauthn = await auth.getWebauthnResponse( + MFAChallengeScope.USER_SESSION + ); setAttempt({ status: 'success', statusText: '', diff --git a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx index f9a85457ad23a..7aab6ab442223 100644 --- a/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/ConnectMyComputer/TestConnection/TestConnection.tsx @@ -47,6 +47,8 @@ import { import { sortNodeLogins } from 'teleport/services/nodes'; import { ApiError } from 'teleport/services/api/parseError'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; + import { NodeMeta } from '../../useDiscover'; import type { Option } from 'shared/components/Select'; @@ -172,6 +174,7 @@ export function TestConnection(props: AgentStepProps) { }) } 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 10cb0d6b50a68..4ff8ee5219dd7 100644 --- a/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/Database/TestConnection/TestConnection.tsx @@ -25,6 +25,8 @@ import TextSelectCopy from 'teleport/components/TextSelectCopy'; import { generateTshLoginCommand } from 'teleport/lib/util'; import ReAuthenticate from 'teleport/components/ReAuthenticate'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; + import { ActionButtons, HeaderSubtitle, @@ -89,6 +91,7 @@ export function TestConnectionView({ testConnection(makeTestConnRequest(), res)} onClose={cancelMfaDialog} + challengeScope={MFAChallengeScope.USER_SESSION} /> )}
Test Connection
diff --git a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx index 5edf97a0dccff..23976922b1f6c 100644 --- a/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/Kubernetes/TestConnection/TestConnection.tsx @@ -28,6 +28,8 @@ import TextSelectCopy from 'teleport/components/TextSelectCopy'; import { generateTshLoginCommand } from 'teleport/lib/util'; import ReAuthenticate from 'teleport/components/ReAuthenticate'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; + import { ActionButtons, HeaderSubtitle, @@ -101,6 +103,7 @@ export function TestConnection({ testConnection(makeTestConnRequest(), res)} onClose={cancelMfaDialog} + challengeScope={MFAChallengeScope.USER_SESSION} /> )}
Test Connection
diff --git a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx index 5a596ab99fade..734b20bacdc9b 100644 --- a/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx +++ b/web/packages/teleport/src/Discover/Server/TestConnection/TestConnection.tsx @@ -33,6 +33,8 @@ import { } from 'teleport/Discover/Shared'; import { sortNodeLogins } from 'teleport/services/nodes'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; + import { NodeMeta } from '../../useDiscover'; import type { Option } from 'shared/components/Select'; @@ -87,6 +89,7 @@ export function TestConnection(props: AgentStepProps) { testConnection(selectedOpt.value, res)} onClose={cancelMfaDialog} + challengeScope={MFAChallengeScope.USER_SESSION} /> )}
Test Connection
diff --git a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx index 088bedae810a2..e55511d75404c 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx +++ b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.story.tsx @@ -18,6 +18,8 @@ import React from 'react'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; + import { State } from './useReAuthenticate'; import { ReAuthenticate } from './ReAuthenticate'; @@ -47,4 +49,5 @@ const props: State = { onClose: () => null, auth2faType: 'on', actionText: 'performing this action', + challengeScope: MFAChallengeScope.UNSPECIFIED, }; diff --git a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx index e6d363d94c8cd..fdd5dd756af3b 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx +++ b/web/packages/teleport/src/components/ReAuthenticate/ReAuthenticate.tsx @@ -47,6 +47,7 @@ export function ReAuthenticate({ auth2faType, preferredMfaType, actionText, + challengeScope, }: State) { const [otpToken, setOtpToken] = useState(''); const mfaOptions = createMfaOptions({ @@ -60,7 +61,7 @@ export function ReAuthenticate({ e.preventDefault(); if (mfaOption?.value === 'webauthn') { - submitWithWebauthn(); + submitWithWebauthn(challengeScope); } if (mfaOption?.value === 'otp') { submitWithTotp(otpToken); diff --git a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts index 3ad3116768fff..28d7921768e44 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts +++ b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts @@ -20,6 +20,7 @@ import useAttempt from 'shared/hooks/useAttemptNext'; import cfg from 'teleport/config'; import auth from 'teleport/services/auth'; +import { MFAChallengeScope } from 'teleport/services/auth/auth'; import type { MfaAuthnResponse } from 'teleport/services/mfa'; @@ -31,7 +32,7 @@ import type { MfaAuthnResponse } from 'teleport/services/mfa'; // 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; + const { onClose, actionText = defaultActionText, challengeScope } = props; // Note that attempt state "success" is not used or required. // After the user submits, the control is passed back @@ -52,12 +53,12 @@ export default function useReAuthenticate(props: Props) { .catch(handleError); } - function submitWithWebauthn() { + function submitWithWebauthn(scope: MFAChallengeScope) { setAttempt({ status: 'processing' }); if ('onMfaResponse' in props) { auth - .getWebauthnResponse() + .getWebauthnResponse(scope) .then(webauthnResponse => props.onMfaResponse({ webauthn_response: webauthnResponse }) ) @@ -66,7 +67,7 @@ export default function useReAuthenticate(props: Props) { } auth - .createPrivilegeTokenWithWebauthn() + .createPrivilegeTokenWithWebauthn(scope) .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. @@ -96,6 +97,7 @@ export default function useReAuthenticate(props: Props) { auth2faType: cfg.getAuth2faType(), preferredMfaType: cfg.getPreferredMfaType(), actionText, + challengeScope, onClose, }; } @@ -114,6 +116,10 @@ type BaseProps = { * * */ actionText?: string; + /** + * The MFA challenge scope of the action to perform, as defined in webauthn.proto. + */ + challengeScope: MFAChallengeScope; }; // MfaResponseProps defines a function diff --git a/web/packages/teleport/src/services/api/api.ts b/web/packages/teleport/src/services/api/api.ts index 09107e3d3a70a..c8d62fade3428 100644 --- a/web/packages/teleport/src/services/api/api.ts +++ b/web/packages/teleport/src/services/api/api.ts @@ -17,7 +17,7 @@ */ import 'whatwg-fetch'; -import auth from 'teleport/services/auth/auth'; +import auth, { MFAChallengeScope } from 'teleport/services/auth/auth'; import { storageService } from '../storageService'; import { WebauthnAssertionResponse } from '../auth'; @@ -117,7 +117,9 @@ const api = { let webauthnResponseForRetry; try { - webauthnResponseForRetry = await auth.getWebauthnResponse(); + webauthnResponseForRetry = await auth.getWebauthnResponse( + MFAChallengeScope.ADMIN_ACTION + ); } catch (err) { throw new Error( 'Failed to fetch webauthn credentials, 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 d2af51fe042a8..b1bde79e5d01e 100644 --- a/web/packages/teleport/src/services/auth/auth.ts +++ b/web/packages/teleport/src/services/auth/auth.ts @@ -240,13 +240,7 @@ const auth = { headlessSSOAccept(transactionId: string) { return auth - .checkWebauthnSupport() - .then(() => api.post(cfg.api.mfaAuthnChallengePath)) - .then(res => - navigator.credentials.get({ - publicKey: makeMfaAuthenticateChallenge(res).webauthnPublicKey, - }) - ) + .fetchWebauthnChallenge(MFAChallengeScope.HEADLESS_LOGIN) .then(res => { const request = { action: 'accept', @@ -269,7 +263,11 @@ const auth = { return api.post(cfg.api.createPrivilegeTokenPath, { secondFactorToken }); }, - async fetchWebauthnChallenge(isMFARequiredRequest?: IsMfaRequiredRequest) { + async fetchWebauthnChallenge( + scope: MFAChallengeScope, + allowReuse?: boolean, + isMFARequiredRequest?: IsMfaRequiredRequest + ) { // TODO(Joerger): DELETE IN 16.0.0 // the create mfa challenge endpoint below supports // MFARequired requests without the extra roundtrip. @@ -292,7 +290,9 @@ const auth = { .then(() => api .post(cfg.api.mfaAuthnChallengePath, { - is_mfa_required: isMFARequiredRequest, + is_mfa_required_req: isMFARequiredRequest, + challenge_scope: scope, + challenge_allow_reuse: allowReuse, }) .then(makeMfaAuthenticateChallenge) ) @@ -303,8 +303,8 @@ const auth = { ); }, - createPrivilegeTokenWithWebauthn() { - return auth.fetchWebauthnChallenge().then(res => + createPrivilegeTokenWithWebauthn(scope: MFAChallengeScope) { + return auth.fetchWebauthnChallenge(scope).then(res => api.post(cfg.api.createPrivilegeTokenPath, { webauthnAssertionResponse: makeWebauthnAssertionResponse(res), }) @@ -315,9 +315,13 @@ const auth = { return api.post(cfg.api.createPrivilegeTokenPath, {}); }, - getWebauthnResponse(isMFARequiredRequest?: IsMfaRequiredRequest) { + getWebauthnResponse( + scope: MFAChallengeScope, + allowReuse?: boolean, + isMFARequiredRequest?: IsMfaRequiredRequest + ) { return auth - .fetchWebauthnChallenge(isMFARequiredRequest) + .fetchWebauthnChallenge(scope, allowReuse, isMFARequiredRequest) .then(res => makeWebauthnAssertionResponse(res)); }, }; @@ -394,3 +398,15 @@ export type IsMFARequiredAdminAction = { name: string; }; }; + +// MFAChallengeScope is an mfa challenge scope. Possible values are defined in mfa.proto +export enum MFAChallengeScope { + UNSPECIFIED = 0, + LOGIN = 1, + PASSWORDLESS_LOGIN = 2, + HEADLESS_LOGIN = 3, + MANAGE_DEVICES = 4, + ACCOUNT_RECOVERY = 5, + USER_SESSION = 6, + ADMIN_ACTION = 7, +}