Skip to content
Merged
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
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