From 7a41d14c73ece767f04e0de4c91f5918fe2d5bf7 Mon Sep 17 00:00:00 2001 From: joerger Date: Fri, 22 Nov 2024 12:30:00 -0800 Subject: [PATCH] Handle unified mfa response to create privileged token. --- lib/web/users.go | 12 +++++++ .../ReAuthenticate/useReAuthenticate.ts | 7 +++-- .../teleport/src/services/auth/auth.ts | 31 ++++++++++--------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/web/users.go b/lib/web/users.go index 88fa8907bafce..f96d0b4405e10 100644 --- a/lib/web/users.go +++ b/lib/web/users.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" + "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/web/ui" ) @@ -251,10 +252,15 @@ func deleteUser(r *http.Request, params httprouter.Params, m userAPIGetter, user } type privilegeTokenRequest struct { + // TODO(Joerger): DELETE IN v19.0.0 in favor of ExistingMFAResponse // SecondFactorToken is the totp code. SecondFactorToken string `json:"secondFactorToken"` + // TODO(Joerger): DELETE IN v19.0.0 in favor of ExistingMFAResponse // WebauthnResponse is the response from authenticators. WebauthnResponse *wantypes.CredentialAssertionResponse `json:"webauthnAssertionResponse"` + // ExistingMFAResponse is an MFA challenge response from an existing device. + // Not required if the user has no existing devices. + ExistingMFAResponse *client.MFAChallengeResponse `json:"existingMfaResponse"` } // createPrivilegeTokenHandle creates and returns a privilege token. @@ -275,6 +281,12 @@ func (h *Handler) createPrivilegeTokenHandle(w http.ResponseWriter, r *http.Requ protoReq.ExistingMFAResponse = &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_Webauthn{ Webauthn: wantypes.CredentialAssertionResponseToProto(req.WebauthnResponse), }} + case req.ExistingMFAResponse != nil: + var err error + protoReq.ExistingMFAResponse, err = req.ExistingMFAResponse.GetOptionalMFAResponseProtoReq() + if err != nil { + return nil, trace.Wrap(err) + } default: // Can be empty, which means user did not have a second factor registered. } diff --git a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts index 8bb9e13a7661d..fe41801f49387 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts +++ b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts @@ -48,7 +48,7 @@ export default function useReAuthenticate(props: Props) { setAttempt({ status: 'processing' }); auth - .createPrivilegeTokenWithTotp(secondFactorToken) + .createPrivilegeToken({ totp_code: secondFactorToken }) .then(props.onAuthenticated) .catch(handleError); } @@ -65,8 +65,11 @@ export default function useReAuthenticate(props: Props) { return; } + // Creating privilege tokens always expects the MANAGE_DEVICES webauthn scope. auth - .createPrivilegeTokenWithWebauthn() + .getMfaChallenge({ scope: MfaChallengeScope.MANAGE_DEVICES }) + .then(auth.getMfaChallengeResponse) + .then(auth.createPrivilegeToken) .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. diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts index 410b926d859ba..4e0854d8c1edc 100644 --- a/web/packages/teleport/src/services/auth/auth.ts +++ b/web/packages/teleport/src/services/auth/auth.ts @@ -260,10 +260,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( @@ -333,20 +329,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: makeWebauthnAssertionResponse( - 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() {