diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 67fa7130f93a3..6a4c83dc81706 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -7589,10 +7589,11 @@ func (a *ServerWithRoles) UpdateHeadlessAuthenticationState(ctx context.Context, } // Only WebAuthn is supported in headless login flow for superior phishing prevention. - if _, ok := mfaResp.Response.(*proto.MFAAuthenticateResponse_Webauthn); !ok { - err = trace.BadParameter("expected WebAuthn challenge response, but got %T", mfaResp.Response) + switch mfaResp.Response.(type) { + case *proto.MFAAuthenticateResponse_Webauthn, *proto.MFAAuthenticateResponse_SSO: + default: + err = trace.BadParameter("MFA response of type %T is not supported for headless authentication", mfaResp.Response) emitHeadlessLoginEvent(ctx, events.UserHeadlessLoginApprovedFailureCode, a.authServer.emitter, headlessAuthn, err) - return err } requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_HEADLESS_LOGIN} diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index 0aa7f07ea6503..8785202cb6476 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -388,7 +388,10 @@ type AuthenticateWebUserRequest struct { type HeadlessRequest struct { // Actions can be either accept or deny. Action string `json:"action"` + // MFAResponse is an MFA response used to authenticate the headless request. + MFAResponse *MFAChallengeResponse `json:"mfaResponse"` // WebauthnAssertionResponse is a signed WebAuthn credential assertion. + // TODO(Joerger): DELETE IN v19.0.0, new clients send mfaResponse WebauthnAssertionResponse *wantypes.CredentialAssertionResponse `json:"webauthnAssertionResponse,omitempty"` } diff --git a/lib/web/headless.go b/lib/web/headless.go index 4542a4dd0522b..2d8e4f3e41e3a 100644 --- a/lib/web/headless.go +++ b/lib/web/headless.go @@ -24,9 +24,7 @@ import ( "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" - "github.com/gravitational/teleport/api/client/proto" "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" ) @@ -67,17 +65,21 @@ func (h *Handler) putHeadlessState(_ http.ResponseWriter, r *http.Request, param return nil, trace.Wrap(err) } - var action types.HeadlessAuthenticationState - var resp = &proto.MFAAuthenticateResponse{} + if req.MFAResponse == nil && req.WebauthnAssertionResponse != nil { + req.MFAResponse = &client.MFAChallengeResponse{ + WebauthnResponse: req.WebauthnAssertionResponse, + } + } + + mfaResp, err := req.MFAResponse.GetOptionalMFAResponseProtoReq() + if err != nil { + return nil, trace.Wrap(err) + } + var action types.HeadlessAuthenticationState switch req.Action { case "accept": action = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED - resp = &proto.MFAAuthenticateResponse{ - Response: &proto.MFAAuthenticateResponse_Webauthn{ - Webauthn: wantypes.CredentialAssertionResponseToProto(req.WebauthnAssertionResponse), - }, - } case "denied": action = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED default: @@ -89,9 +91,7 @@ func (h *Handler) putHeadlessState(_ http.ResponseWriter, r *http.Request, param return nil, trace.Wrap(err) } - err = authClient.UpdateHeadlessAuthenticationState(r.Context(), headlessAuthenticationID, - action, resp) - if err != nil { + if err = authClient.UpdateHeadlessAuthenticationState(r.Context(), headlessAuthenticationID, action, mfaResp); err != nil { return nil, trace.Wrap(err) } diff --git a/rfd/0180-sso-mfa.md b/rfd/0180-sso-mfa.md index d331f07ee3c04..0bea5239b7384 100644 --- a/rfd/0180-sso-mfa.md +++ b/rfd/0180-sso-mfa.md @@ -850,3 +850,15 @@ From what I can tell from the [documentation](https://docs.github.com/en/apps/oa you can only do the basic github 2fa login flow. There is no support for MFA acr values like okta, or customized web flows with MFA like Auth0. Therefore Github connectors will be left out of the initial implementation. + +#### Headless + +Originally, Webauthn was the only supported MFA type for Headless, as legacy +OTP is especially at risk to phishing attacks. However, a securely configured +SSO MFA connector with Webauthn enforced by the provider is also sufficiently +phishing proof. Therefore, SSO MFA will be supported as an MFA method for +headless approvals. + +Note: as stated elsewhere in this RFD, it is the onus the administrator to +head warnings in the documentation and ensure the secure setup of SSO MFA. +Admins should not enable headless authentication with insecure SSO MFA setups. diff --git a/web/packages/teleport/src/services/auth/auth.ts b/web/packages/teleport/src/services/auth/auth.ts index 88ceedabf4f75..afaaaefae9cbd 100644 --- a/web/packages/teleport/src/services/auth/auth.ts +++ b/web/packages/teleport/src/services/auth/auth.ts @@ -219,9 +219,7 @@ const auth = { }, headlessSSOGet(transactionId: string) { - return auth - .checkWebauthnSupport() - .then(() => api.get(cfg.getHeadlessSsoPath(transactionId))) + return api.get(cfg.getHeadlessSsoPath(transactionId)) .then((json: any) => { json = json || {}; @@ -234,10 +232,12 @@ const auth = { headlessSSOAccept(transactionId: string) { return auth .getMfaChallenge({ scope: MfaChallengeScope.HEADLESS_LOGIN }) - .then(challenge => auth.getMfaChallengeResponse(challenge, 'webauthn')) + .then(challenge => auth.getMfaChallengeResponse(challenge)) .then(res => { const request = { action: 'accept', + mfaResponse: res, + // TODO(Joerger): DELETE IN v19.0.0, new clients send mfaResponse. webauthnAssertionResponse: res.webauthn_response, };