Skip to content

Commit

Permalink
Refactor Reauthenticate components to handle generic MFA challenges. (
Browse files Browse the repository at this point in the history
#49680) (#50354)

* Handle unified mfa response to create privileged token.

* Refactor useReauthenticate and Reauthenticate component.

* Refactor ChangePasswordWizard to use useReauthenticate.

* Fix mfa challenge option preference order; Fix reauthenticate component initial mfa option state.

* Remove useReAuthenticate's onAuthenticated parameter.

* Fix lint.

* Fix flaky test.

* Remove extra createPrivilegeToken call from the account page; Add new tokenless mfa endpoints to register/delete mfa devices; add TODOs to use new endpoints in v19+.

* Fix tests.

* Fix error handling in Web MFA flow.

* Update e ref.

* Fix stories.

* Fix lint.
  • Loading branch information
Joerger authored Dec 18, 2024
1 parent dc15625 commit a44e495
Show file tree
Hide file tree
Showing 32 changed files with 1,002 additions and 1,033 deletions.
2 changes: 1 addition & 1 deletion e
Submodule e updated from 37c824 to c49f24
3 changes: 3 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,10 @@ func (h *Handler) bindDefaultEndpoints() {

// MFA private endpoints.
h.GET("/webapi/mfa/devices", h.WithAuth(h.getMFADevicesHandle))
h.DELETE("/webapi/mfa/devices", h.WithAuth(h.deleteMFADeviceHandle))
h.POST("/webapi/mfa/authenticatechallenge", h.WithAuth(h.createAuthenticateChallengeHandle))
h.POST("/webapi/mfa/registerchallenge", h.WithAuth(h.createRegisterChallengeHandle))

h.POST("/webapi/mfa/devices", h.WithAuth(h.addMFADeviceHandle))
// DEPRECATED in favor of mfa/authenticatechallenge.
// TODO(bl-nero): DELETE IN 17.0.0
Expand Down
8 changes: 4 additions & 4 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5251,24 +5251,24 @@ func TestCreateRegisterChallenge(t *testing.T) {

tests := []struct {
name string
req *createRegisterChallengeRequest
req *createRegisterChallengeWithTokenRequest
assertChallenge func(t *testing.T, c *client.MFARegisterChallenge)
}{
{
name: "totp",
req: &createRegisterChallengeRequest{
req: &createRegisterChallengeWithTokenRequest{
DeviceType: "totp",
},
},
{
name: "webauthn",
req: &createRegisterChallengeRequest{
req: &createRegisterChallengeWithTokenRequest{
DeviceType: "webauthn",
},
},
{
name: "passwordless",
req: &createRegisterChallengeRequest{
req: &createRegisterChallengeWithTokenRequest{
DeviceType: "webauthn",
DeviceUsage: "passwordless",
},
Expand Down
105 changes: 103 additions & 2 deletions lib/web/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,41 @@ func (h *Handler) deleteMFADeviceWithTokenHandle(w http.ResponseWriter, r *http.
return OK(), nil
}

type deleteMfaDeviceRequest struct {
// DeviceName is the name of the device to delete.
DeviceName string `json:"deviceName"`
// ExistingMFAResponse is an MFA challenge response from an existing device.
// Not required if the user has no existing devices.
ExistingMFAResponse *client.MFAChallengeResponse `json:"existingMfaResponse"`
}

// deleteMFADeviceHandle deletes an mfa device for the user defined in the `token`, given as a query parameter.
func (h *Handler) deleteMFADeviceHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext) (interface{}, error) {
var req deleteMfaDeviceRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}

mfaResponse, err := req.ExistingMFAResponse.GetOptionalMFAResponseProtoReq()
if err != nil {
return nil, trace.Wrap(err)
}

clt, err := c.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}

if err := clt.DeleteMFADeviceSync(r.Context(), &proto.DeleteMFADeviceSyncRequest{
DeviceName: req.DeviceName,
ExistingMFAResponse: mfaResponse,
}); err != nil {
return nil, trace.Wrap(err)
}

return OK(), nil
}

type addMFADeviceRequest struct {
// PrivilegeTokenID is privilege token id.
PrivilegeTokenID string `json:"tokenId"`
Expand Down Expand Up @@ -203,7 +238,7 @@ func (h *Handler) createAuthenticateChallengeWithTokenHandle(w http.ResponseWrit
return makeAuthenticateChallenge(chal), nil
}

type createRegisterChallengeRequest struct {
type createRegisterChallengeWithTokenRequest struct {
// DeviceType is the type of MFA device to get a register challenge for.
DeviceType string `json:"deviceType"`
// DeviceUsage is the intended usage of the device (MFA, Passwordless, etc).
Expand All @@ -214,7 +249,7 @@ type createRegisterChallengeRequest struct {

// createRegisterChallengeWithTokenHandle creates and returns MFA register challenges for a new device for the specified device type.
func (h *Handler) createRegisterChallengeWithTokenHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req createRegisterChallengeRequest
var req createRegisterChallengeWithTokenRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -256,6 +291,72 @@ func (h *Handler) createRegisterChallengeWithTokenHandle(w http.ResponseWriter,
return resp, nil
}

type createRegisterChallengeRequest struct {
// DeviceType is the type of MFA device to get a register challenge for.
DeviceType string `json:"deviceType"`
// DeviceUsage is the intended usage of the device (MFA, Passwordless, etc).
// It mimics the proto.DeviceUsage enum.
// Defaults to MFA.
DeviceUsage string `json:"deviceUsage"`
// ExistingMFAResponse is an MFA challenge response from an existing device.
// Not required if the user has no existing devices.
ExistingMFAResponse *client.MFAChallengeResponse `json:"existingMfaResponse"`
}

// createRegisterChallengeHandle creates and returns MFA register challenges for a new device for the specified device type.
func (h *Handler) createRegisterChallengeHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params, c *SessionContext) (interface{}, error) {
var req createRegisterChallengeRequest
if err := httplib.ReadJSON(r, &req); err != nil {
return nil, trace.Wrap(err)
}

var deviceType proto.DeviceType
switch req.DeviceType {
case "totp":
deviceType = proto.DeviceType_DEVICE_TYPE_TOTP
case "webauthn":
deviceType = proto.DeviceType_DEVICE_TYPE_WEBAUTHN
default:
return nil, trace.BadParameter("MFA device type %q unsupported", req.DeviceType)
}

deviceUsage, err := getDeviceUsage(req.DeviceUsage)
if err != nil {
return nil, trace.Wrap(err)
}

mfaResponse, err := req.ExistingMFAResponse.GetOptionalMFAResponseProtoReq()
if err != nil {
return nil, trace.Wrap(err)
}

clt, err := c.GetClient()
if err != nil {
return nil, trace.Wrap(err)
}

chal, err := clt.CreateRegisterChallenge(r.Context(), &proto.CreateRegisterChallengeRequest{
DeviceType: deviceType,
DeviceUsage: deviceUsage,
ExistingMFAResponse: mfaResponse,
})
if err != nil {
return nil, trace.Wrap(err)
}

resp := &client.MFARegisterChallenge{}
switch chal.GetRequest().(type) {
case *proto.MFARegisterChallenge_TOTP:
resp.TOTP = &client.TOTPRegisterChallenge{
QRCode: chal.GetTOTP().GetQRCode(),
}
case *proto.MFARegisterChallenge_Webauthn:
resp.Webauthn = wantypes.CredentialCreationFromProto(chal.GetWebauthn())
}

return resp, nil
}

func getDeviceUsage(reqUsage string) (proto.DeviceUsage, error) {
var deviceUsage proto.DeviceUsage
switch strings.ToLower(reqUsage) {
Expand Down
12 changes: 12 additions & 0 deletions lib/web/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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.
Expand All @@ -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.
}
Expand Down
4 changes: 1 addition & 3 deletions web/packages/teleport/src/Account/Account.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const LoadingDevicesFailed = () => (
);

export const RemoveDialog = () => (
<Account {...props} token="123" deviceToRemove={props.devices[0]} />
<Account {...props} deviceToRemove={props.devices[0]} />
);

export const RestrictedTokenCreateProcessing = () => (
Expand All @@ -110,7 +110,6 @@ export const RestrictedTokenCreateFailed = () => (
);

const props: AccountProps = {
token: '',
onAddDevice: () => null,
fetchDevicesAttempt: { status: 'success' },
createRestrictedTokenAttempt: { status: '' },
Expand Down Expand Up @@ -179,7 +178,6 @@ const props: AccountProps = {
},
],
onDeviceAdded: () => {},
isReauthenticationRequired: false,
addDeviceWizardVisible: false,
closeAddDeviceWizard: () => {},
passwordState: PasswordState.PASSWORD_STATE_SET,
Expand Down
Loading

0 comments on commit a44e495

Please sign in to comment.