Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Reauthenticate components to handle generic MFA challenges. #49680

Open
wants to merge 13 commits into from
2 changes: 1 addition & 1 deletion e
Submodule e updated from a76c72 to cd63b2
3 changes: 3 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,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 @@ -5250,24 +5250,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
Loading