Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we still be returning this error?

}

requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_HEADLESS_LOGIN}
Expand Down
3 changes: 3 additions & 0 deletions lib/client/weblogin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
24 changes: 12 additions & 12 deletions lib/web/headless.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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:
Expand All @@ -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)
}

Expand Down
12 changes: 12 additions & 0 deletions rfd/0180-sso-mfa.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 4 additions & 4 deletions web/packages/teleport/src/services/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,7 @@ const auth = {
},

headlessSSOGet(transactionId: string) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're here, it should be headlessSsoGet.

return auth
.checkWebauthnSupport()
.then(() => api.get(cfg.getHeadlessSsoPath(transactionId)))
return api.get(cfg.getHeadlessSsoPath(transactionId))
.then((json: any) => {
json = json || {};

Expand All @@ -234,10 +232,12 @@ const auth = {
headlessSSOAccept(transactionId: string) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're changing headlessSSOGet then we should also rename this to headlessSsoAccept.

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,
};

Expand Down
Loading