mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 17:23:22 +00:00
Add passwordless option with user reset/invite (#11748)
* Allow register passwordless on invite/reset * Specify the type of device usage when registering new device * Minor refactoring to reduce number of parameters to funcs * Check if device register requested pwdless, response is pwdless capable
This commit is contained in:
parent
b4eec0d3c5
commit
833848594f
File diff suppressed because it is too large
Load diff
|
@ -1021,6 +1021,9 @@ message AddMFADeviceSyncRequest {
|
|||
string NewDeviceName = 2 [ (gogoproto.jsontag) = "new_device_name,omitempty" ];
|
||||
// NewMFAResponse is a user's new mfa response to a mfa register challenge.
|
||||
MFARegisterResponse NewMFAResponse = 3 [ (gogoproto.jsontag) = "new_mfa_response,omitempty" ];
|
||||
// DeviceUsage is the requested usage for the device.
|
||||
// Defaults to DEVICE_USAGE_MFA.
|
||||
DeviceUsage DeviceUsage = 4 [ (gogoproto.jsontag) = "device_usage,omitempty" ];
|
||||
}
|
||||
|
||||
// AddMFADeviceSyncResponse is a response to AddMFADeviceSyncRequest.
|
||||
|
@ -1279,10 +1282,15 @@ message CRL {
|
|||
}
|
||||
|
||||
// ChangeUserAuthenticationRequest defines a request to change a password and if enabled
|
||||
// also adds a new MFA device from a user reset or from a new user invite. After successful changing
|
||||
// of authentications a new web session is created. Users may also receive new recovery codes if the
|
||||
// user meets the requirement to receive recovery codes. If a user previously had recovery codes,
|
||||
// the previous codes become invalid as it is replaced with newly generated ones.
|
||||
// also adds a new MFA device from a user reset or from a new user invite. User can also skip
|
||||
// setting a new password if passwordless is enabled and just provide a new webauthn register
|
||||
// response.
|
||||
//
|
||||
// After a successful request a new web session is created.
|
||||
//
|
||||
// Users may also receive new recovery codes if they meet the necessary requirements. If a user
|
||||
// previously had recovery codes, the previous codes become invalid as it is replaced with newly
|
||||
// generated ones.
|
||||
message ChangeUserAuthenticationRequest {
|
||||
// TokenID is the ID of a reset or invite token.
|
||||
// The token allows the user to change their credentials without being logged
|
||||
|
|
|
@ -416,10 +416,11 @@ func (s *Server) CompleteAccountRecovery(ctx context.Context, req *proto.Complet
|
|||
return trace.AccessDenied(completeRecoveryGenericErrMsg)
|
||||
}
|
||||
|
||||
_, err = s.verifyMFARespAndAddDevice(ctx, req.GetNewMFAResponse(), &newMFADeviceFields{
|
||||
_, err = s.verifyMFARespAndAddDevice(ctx, &newMFADeviceFields{
|
||||
username: approvedToken.GetUser(),
|
||||
newDeviceName: req.GetNewDeviceName(),
|
||||
tokenID: approvedToken.GetName(),
|
||||
deviceResp: req.GetNewMFAResponse(),
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
|
|
@ -684,7 +684,7 @@ func TestCompleteAccountRecovery(t *testing.T) {
|
|||
{
|
||||
name: "add new WEBAUTHN device",
|
||||
getRequest: func() *proto.CompleteAccountRecoveryRequest {
|
||||
_, webauthnRegRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), approvedToken.GetName())
|
||||
_, webauthnRegRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), approvedToken.GetName(), proto.DeviceUsage_DEVICE_USAGE_MFA)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &proto.CompleteAccountRecoveryRequest{
|
||||
|
@ -812,7 +812,7 @@ func TestCompleteAccountRecovery_WithErrors(t *testing.T) {
|
|||
require.NotEmpty(t, devs)
|
||||
|
||||
// New register response.
|
||||
_, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), approvedToken.GetName())
|
||||
_, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), approvedToken.GetName(), proto.DeviceUsage_DEVICE_USAGE_MFA)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &proto.CompleteAccountRecoveryRequest{
|
||||
|
@ -989,7 +989,7 @@ func TestAccountRecoveryFlow(t *testing.T) {
|
|||
}
|
||||
},
|
||||
getCompleteRequest: func(u *userAuthCreds, approvedTokenID string) *proto.CompleteAccountRecoveryRequest {
|
||||
_, webauthnRegRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), approvedTokenID)
|
||||
_, webauthnRegRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), approvedTokenID, proto.DeviceUsage_DEVICE_USAGE_MFA)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &proto.CompleteAccountRecoveryRequest{
|
||||
|
@ -1350,7 +1350,7 @@ func createUserWithSecondFactors(srv *TestTLSServer) (*userAuthCreds, error) {
|
|||
}
|
||||
|
||||
// Insert a password, device, and recovery codes.
|
||||
webDev, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetToken.GetName())
|
||||
webDev, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetToken.GetName(), proto.DeviceUsage_DEVICE_USAGE_MFA)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -1383,15 +1383,24 @@ func createUserWithSecondFactors(srv *TestTLSServer) (*userAuthCreds, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func getMockedWebauthnAndRegisterRes(authSrv *Server, tokenID string) (*TestDevice, *proto.MFARegisterResponse, error) {
|
||||
func getMockedWebauthnAndRegisterRes(authSrv *Server, tokenID string, usage proto.DeviceUsage) (*TestDevice, *proto.MFARegisterResponse, error) {
|
||||
res, err := authSrv.CreateRegisterChallenge(context.Background(), &proto.CreateRegisterChallengeRequest{
|
||||
TokenID: tokenID,
|
||||
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
|
||||
TokenID: tokenID,
|
||||
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
|
||||
DeviceUsage: usage,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
dev, regRes, err := NewTestDeviceFromChallenge(res)
|
||||
var dev *TestDevice
|
||||
var regRes *proto.MFARegisterResponse
|
||||
|
||||
if usage == proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS {
|
||||
dev, regRes, err = NewTestDeviceFromChallenge(res, WithPasswordless())
|
||||
} else {
|
||||
dev, regRes, err = NewTestDeviceFromChallenge(res)
|
||||
}
|
||||
|
||||
return dev, regRes, trace.Wrap(err)
|
||||
}
|
||||
|
|
|
@ -1628,10 +1628,12 @@ func (a *Server) AddMFADeviceSync(ctx context.Context, req *proto.AddMFADeviceSy
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
dev, err := a.verifyMFARespAndAddDevice(ctx, req.GetNewMFAResponse(), &newMFADeviceFields{
|
||||
dev, err := a.verifyMFARespAndAddDevice(ctx, &newMFADeviceFields{
|
||||
username: privilegeToken.GetUser(),
|
||||
newDeviceName: req.GetNewDeviceName(),
|
||||
tokenID: privilegeToken.GetName(),
|
||||
deviceResp: req.GetNewMFAResponse(),
|
||||
deviceUsage: req.DeviceUsage,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1660,10 +1662,14 @@ type newMFADeviceFields struct {
|
|||
// Identity with an in-memory SessionData storage.
|
||||
// Defaults to the Server's IdentityService.
|
||||
webIdentityOverride wanlib.RegistrationIdentity
|
||||
// deviceResp is the register response from the new device.
|
||||
deviceResp *proto.MFARegisterResponse
|
||||
// deviceUsage describes the intended usage of the new device.
|
||||
deviceUsage proto.DeviceUsage
|
||||
}
|
||||
|
||||
// verifyMFARespAndAddDevice validates MFA register response and on success adds the new MFA device.
|
||||
func (a *Server) verifyMFARespAndAddDevice(ctx context.Context, regResp *proto.MFARegisterResponse, req *newMFADeviceFields) (*types.MFADevice, error) {
|
||||
func (a *Server) verifyMFARespAndAddDevice(ctx context.Context, req *newMFADeviceFields) (*types.MFADevice, error) {
|
||||
cap, err := a.GetAuthPreference(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
@ -1674,19 +1680,19 @@ func (a *Server) verifyMFARespAndAddDevice(ctx context.Context, regResp *proto.M
|
|||
}
|
||||
|
||||
var dev *types.MFADevice
|
||||
switch regResp.GetResponse().(type) {
|
||||
switch req.deviceResp.GetResponse().(type) {
|
||||
case *proto.MFARegisterResponse_TOTP:
|
||||
dev, err = a.registerTOTPDevice(ctx, regResp, req)
|
||||
dev, err = a.registerTOTPDevice(ctx, req.deviceResp, req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
case *proto.MFARegisterResponse_Webauthn:
|
||||
dev, err = a.registerWebauthnDevice(ctx, regResp, req)
|
||||
dev, err = a.registerWebauthnDevice(ctx, req.deviceResp, req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
default:
|
||||
return nil, trace.BadParameter("MFARegisterResponse is an unknown response type %T", regResp.Response)
|
||||
return nil, trace.BadParameter("MFARegisterResponse is an unknown response type %T", req.deviceResp.Response)
|
||||
}
|
||||
|
||||
clusterName, err := a.GetClusterName()
|
||||
|
@ -1768,8 +1774,12 @@ func (a *Server) registerWebauthnDevice(ctx context.Context, regResp *proto.MFAR
|
|||
Identity: identity,
|
||||
}
|
||||
// Finish upserts the device on success.
|
||||
dev, err := webRegistration.Finish(
|
||||
ctx, req.username, req.newDeviceName, wanlib.CredentialCreationResponseFromProto(regResp.GetWebauthn()))
|
||||
dev, err := webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: req.username,
|
||||
DeviceName: req.newDeviceName,
|
||||
CreationResponse: wanlib.CredentialCreationResponseFromProto(regResp.GetWebauthn()),
|
||||
Passwordless: req.deviceUsage == proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS,
|
||||
})
|
||||
return dev, trace.Wrap(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1609,7 +1609,7 @@ func TestAddMFADeviceSync(t *testing.T) {
|
|||
privExToken, err := srv.Auth().createPrivilegeToken(ctx, u.username, UserTokenTypePrivilegeException)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, webauthnRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), privExToken.GetName())
|
||||
_, webauthnRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), privExToken.GetName(), proto.DeviceUsage_DEVICE_USAGE_MFA)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &proto.AddMFADeviceSyncRequest{
|
||||
|
|
|
@ -1902,11 +1902,13 @@ func addMFADeviceRegisterChallenge(gctx *grpcContext, stream proto.AuthService_A
|
|||
}
|
||||
|
||||
// Validate MFARegisterResponse and upsert the new device on success.
|
||||
dev, err := auth.verifyMFARespAndAddDevice(ctx, regResp, &newMFADeviceFields{
|
||||
dev, err := auth.verifyMFARespAndAddDevice(ctx, &newMFADeviceFields{
|
||||
username: user,
|
||||
newDeviceName: initReq.DeviceName,
|
||||
totpSecret: regChallenge.GetTOTP().GetSecret(),
|
||||
webIdentityOverride: webIdentity,
|
||||
deviceResp: regResp,
|
||||
deviceUsage: initReq.DeviceUsage,
|
||||
})
|
||||
|
||||
return dev, trace.Wrap(err)
|
||||
|
|
|
@ -36,8 +36,9 @@ type TestDevice struct {
|
|||
TOTPSecret string
|
||||
Key *mocku2f.Key
|
||||
|
||||
clock clockwork.Clock
|
||||
origin string
|
||||
clock clockwork.Clock
|
||||
origin string
|
||||
passwordless bool
|
||||
}
|
||||
|
||||
// TestDeviceOpt is a creation option for TestDevice.
|
||||
|
@ -49,6 +50,12 @@ func WithTestDeviceClock(clock clockwork.Clock) TestDeviceOpt {
|
|||
}
|
||||
}
|
||||
|
||||
func WithPasswordless() TestDeviceOpt {
|
||||
return func(d *TestDevice) {
|
||||
d.passwordless = true
|
||||
}
|
||||
}
|
||||
|
||||
func NewTestDeviceFromChallenge(c *proto.MFARegisterChallenge, opts ...TestDeviceOpt) (*TestDevice, *proto.MFARegisterResponse, error) {
|
||||
dev := &TestDevice{}
|
||||
for _, opt := range opts {
|
||||
|
@ -225,6 +232,11 @@ func (d *TestDevice) solveRegisterWebauthn(c *proto.MFARegisterChallenge) (*prot
|
|||
}
|
||||
d.Key.PreferRPID = true
|
||||
|
||||
if d.passwordless {
|
||||
d.Key.AllowResidentKey = true
|
||||
d.Key.SetUV = true
|
||||
}
|
||||
|
||||
resp, err := d.Key.SignCredentialCreation(d.Origin(), wanlib.CredentialCreationFromProto(c.GetWebauthn()))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
|
|
@ -315,9 +315,16 @@ func (s *Server) changeUserAuthentication(ctx context.Context, req *proto.Change
|
|||
return nil, trace.AccessDenied(noLocalAuth)
|
||||
}
|
||||
|
||||
err = services.VerifyPassword(req.GetNewPassword())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
reqPasswordless := len(req.GetNewPassword()) == 0 && authPref.GetAllowPasswordless()
|
||||
switch {
|
||||
case reqPasswordless:
|
||||
if req.GetNewMFARegisterResponse() == nil || req.NewMFARegisterResponse.GetWebauthn() == nil {
|
||||
return nil, trace.BadParameter("passwordless: missing webauthn credentials")
|
||||
}
|
||||
default:
|
||||
if err := services.VerifyPassword(req.GetNewPassword()); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if token exists.
|
||||
|
@ -343,10 +350,10 @@ func (s *Server) changeUserAuthentication(ctx context.Context, req *proto.Change
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Set a new password.
|
||||
err = s.UpsertPassword(username, req.GetNewPassword())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
if !reqPasswordless {
|
||||
if err := s.UpsertPassword(username, req.GetNewPassword()); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := s.GetUser(username, false)
|
||||
|
@ -397,10 +404,17 @@ func (s *Server) changeUserSecondFactor(ctx context.Context, req *proto.ChangeUs
|
|||
}
|
||||
}
|
||||
|
||||
_, err = s.verifyMFARespAndAddDevice(ctx, req.GetNewMFARegisterResponse(), &newMFADeviceFields{
|
||||
deviceUsage := proto.DeviceUsage_DEVICE_USAGE_MFA
|
||||
if len(req.GetNewPassword()) == 0 {
|
||||
deviceUsage = proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS
|
||||
}
|
||||
|
||||
_, err = s.verifyMFARespAndAddDevice(ctx, &newMFADeviceFields{
|
||||
username: token.GetUser(),
|
||||
newDeviceName: deviceName,
|
||||
tokenID: token.GetName(),
|
||||
deviceResp: req.GetNewMFARegisterResponse(),
|
||||
deviceUsage: deviceUsage,
|
||||
})
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
|
|
@ -384,7 +384,7 @@ func TestChangeUserAuthentication(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
},
|
||||
getReq: func(resetTokenID string) *proto.ChangeUserAuthenticationRequest {
|
||||
_, webauthnRegRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetTokenID)
|
||||
_, webauthnRegRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetTokenID, proto.DeviceUsage_DEVICE_USAGE_MFA)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &proto.ChangeUserAuthenticationRequest{
|
||||
|
@ -402,6 +402,37 @@ func TestChangeUserAuthentication(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with passwordless",
|
||||
setAuthPreference: func() {
|
||||
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
|
||||
Type: constants.Local,
|
||||
SecondFactor: constants.SecondFactorWebauthn,
|
||||
Webauthn: &types.Webauthn{
|
||||
RPID: "localhost",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = srv.Auth().SetAuthPreference(ctx, authPreference)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
getReq: func(resetTokenID string) *proto.ChangeUserAuthenticationRequest {
|
||||
_, webauthnRes, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetTokenID, proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &proto.ChangeUserAuthenticationRequest{
|
||||
TokenID: resetTokenID,
|
||||
NewMFARegisterResponse: webauthnRes,
|
||||
}
|
||||
},
|
||||
// Missing webauthn for passwordless.
|
||||
getInvalidReq: func(resetTokenID string) *proto.ChangeUserAuthenticationRequest {
|
||||
return &proto.ChangeUserAuthenticationRequest{
|
||||
TokenID: resetTokenID,
|
||||
NewMFARegisterResponse: &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_TOTP{}},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with second factor on",
|
||||
setAuthPreference: func() {
|
||||
|
@ -417,7 +448,7 @@ func TestChangeUserAuthentication(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
},
|
||||
getReq: func(resetTokenID string) *proto.ChangeUserAuthenticationRequest {
|
||||
_, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetTokenID)
|
||||
_, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetTokenID, proto.DeviceUsage_DEVICE_USAGE_MFA)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &proto.ChangeUserAuthenticationRequest{
|
||||
|
@ -473,7 +504,7 @@ func TestChangeUserAuthentication(t *testing.T) {
|
|||
|
||||
if c.getInvalidReq != nil {
|
||||
invalidReq := c.getInvalidReq(token.GetName())
|
||||
_, err = srv.Auth().changeUserAuthentication(ctx, invalidReq)
|
||||
_, err := srv.Auth().changeUserAuthentication(ctx, invalidReq)
|
||||
require.True(t, trace.IsBadParameter(err))
|
||||
}
|
||||
|
||||
|
@ -482,8 +513,10 @@ func TestChangeUserAuthentication(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Test password is updated.
|
||||
err = srv.Auth().checkPasswordWOToken(username, validReq.NewPassword)
|
||||
require.NoError(t, err)
|
||||
if len(validReq.NewPassword) != 0 {
|
||||
err := srv.Auth().checkPasswordWOToken(username, validReq.NewPassword)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test device was registered.
|
||||
if validReq.NewMFARegisterResponse != nil {
|
||||
|
|
|
@ -74,7 +74,11 @@ func TestLoginFlow_BeginFinish(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
ccr, err := webKey.SignCredentialCreation(webOrigin, cc)
|
||||
require.NoError(t, err)
|
||||
_, err = webRegistration.Finish(ctx, webUser, "webauthn1" /* deviceName */, ccr)
|
||||
_, err = webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: webUser,
|
||||
DeviceName: "webauthn1",
|
||||
CreationResponse: ccr,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
|
@ -240,7 +244,11 @@ func TestLoginFlow_Finish_errors(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
ccr, err := key.SignCredentialCreation(webOrigin, cc)
|
||||
require.NoError(t, err)
|
||||
_, err = webRegistration.Finish(ctx, user, "webauthn1" /* deviceName */, ccr)
|
||||
_, err = webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: user,
|
||||
DeviceName: "webauthn1",
|
||||
CreationResponse: ccr,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
webLogin := wanlib.LoginFlow{
|
||||
|
@ -362,7 +370,12 @@ func TestPasswordlessFlow_BeginAndFinish(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
ccr, err := webKey.SignCredentialCreation(webOrigin, cc)
|
||||
require.NoError(t, err)
|
||||
_, err = webRegistration.Finish(ctx, user, "webauthn1" /* deviceName */, ccr)
|
||||
_, err = webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: user,
|
||||
DeviceName: "webauthn1",
|
||||
CreationResponse: ccr,
|
||||
Passwordless: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
webLogin := &wanlib.PasswordlessFlow{
|
||||
|
|
|
@ -228,20 +228,36 @@ func upsertOrGetWebID(ctx context.Context, user string, identity RegistrationIde
|
|||
return wla.UserID, nil
|
||||
}
|
||||
|
||||
// RegisterResponse represents fields needed to finish registering a new webautn device.
|
||||
type RegisterResponse struct {
|
||||
// User is the device owner.
|
||||
User string
|
||||
// DeviceName is the name for the new device.
|
||||
DeviceName string
|
||||
// CreationResponse is the response from the new device.
|
||||
CreationResponse *CredentialCreationResponse
|
||||
// Passwordless is true if this is expected to be a passwordless registration.
|
||||
// Callers may make certain concessions when processing passwordless
|
||||
// registration (such as skipping password validation), this flag reflects that.
|
||||
// The data stored in the Begin SessionData must match the passwordless flag,
|
||||
// otherwise the registration is denied.
|
||||
Passwordless bool
|
||||
}
|
||||
|
||||
// Finish is the second and last step of the registration ceremony.
|
||||
// If successful, it returns the created MFADevice. Finish has the side effect
|
||||
// or writing the device to storage (using its Identity interface).
|
||||
func (f *RegistrationFlow) Finish(ctx context.Context, user, deviceName string, resp *CredentialCreationResponse) (*types.MFADevice, error) {
|
||||
func (f *RegistrationFlow) Finish(ctx context.Context, req RegisterResponse) (*types.MFADevice, error) {
|
||||
switch {
|
||||
case user == "":
|
||||
case req.User == "":
|
||||
return nil, trace.BadParameter("user required")
|
||||
case deviceName == "":
|
||||
case req.DeviceName == "":
|
||||
return nil, trace.BadParameter("device name required")
|
||||
case resp == nil:
|
||||
case req.CreationResponse == nil:
|
||||
return nil, trace.BadParameter("credential creation response required")
|
||||
}
|
||||
|
||||
parsedResp, err := parseCredentialCreationResponse(resp)
|
||||
parsedResp, err := parseCredentialCreationResponse(req.CreationResponse)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -255,13 +271,13 @@ func (f *RegistrationFlow) Finish(ctx context.Context, user, deviceName string,
|
|||
// TODO(codingllama): Verify that the public key matches the allowed
|
||||
// credential params? It doesn't look like duo-labs/webauthn does that.
|
||||
|
||||
wla, err := f.Identity.GetWebauthnLocalAuth(ctx, user)
|
||||
wla, err := f.Identity.GetWebauthnLocalAuth(ctx, req.User)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
u := newWebUser(user, wla.UserID, true /* credentialIDOnly */, nil /* devices */)
|
||||
u := newWebUser(req.User, wla.UserID, true /* credentialIDOnly */, nil /* devices */)
|
||||
|
||||
sessionDataPB, err := f.Identity.GetWebauthnSessionData(ctx, user, scopeSession)
|
||||
sessionDataPB, err := f.Identity.GetWebauthnSessionData(ctx, req.User, scopeSession)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -270,6 +286,9 @@ func (f *RegistrationFlow) Finish(ctx context.Context, user, deviceName string,
|
|||
// Activate passwordless switches (resident key, user verification) if we
|
||||
// required verification in the begin step.
|
||||
passwordless := sessionData.UserVerification == protocol.VerificationRequired
|
||||
if req.Passwordless && !passwordless {
|
||||
return nil, trace.BadParameter("passwordless registration failed, requested CredentialCreation was for an MFA registration")
|
||||
}
|
||||
|
||||
web, err := newWebAuthn(webAuthnParams{
|
||||
cfg: f.Webauthn,
|
||||
|
@ -293,7 +312,7 @@ func (f *RegistrationFlow) Finish(ctx context.Context, user, deviceName string,
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
newDevice := types.NewMFADevice(deviceName, uuid.NewString() /* id */, time.Now() /* addedAt */)
|
||||
newDevice := types.NewMFADevice(req.DeviceName, uuid.NewString() /* id */, time.Now() /* addedAt */)
|
||||
newDevice.Device = &types.MFADevice_Webauthn{
|
||||
Webauthn: &types.WebauthnDevice{
|
||||
CredentialId: credential.ID,
|
||||
|
@ -301,21 +320,21 @@ func (f *RegistrationFlow) Finish(ctx context.Context, user, deviceName string,
|
|||
AttestationType: credential.AttestationType,
|
||||
Aaguid: credential.Authenticator.AAGUID,
|
||||
SignatureCounter: credential.Authenticator.SignCount,
|
||||
AttestationObject: resp.AttestationResponse.AttestationObject,
|
||||
ResidentKey: passwordless,
|
||||
AttestationObject: req.CreationResponse.AttestationResponse.AttestationObject,
|
||||
ResidentKey: req.Passwordless,
|
||||
},
|
||||
}
|
||||
// We delegate a few checks to identity, including:
|
||||
// * The validity of the created MFADevice
|
||||
// * Uniqueness validation of the deviceName
|
||||
// * Uniqueness validation of the Webauthn credential ID.
|
||||
if err := f.Identity.UpsertMFADevice(ctx, user, newDevice); err != nil {
|
||||
if err := f.Identity.UpsertMFADevice(ctx, req.User, newDevice); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Registration complete, remove the registration challenge we just used.
|
||||
if err := f.Identity.DeleteWebauthnSessionData(ctx, user, scopeSession); err != nil {
|
||||
log.Warnf("WebAuthn: failed to delete registration SessionData for user %v", user)
|
||||
if err := f.Identity.DeleteWebauthnSessionData(ctx, req.User, scopeSession); err != nil {
|
||||
log.Warnf("WebAuthn: failed to delete registration SessionData for user %v", req.User)
|
||||
}
|
||||
|
||||
return newDevice, nil
|
||||
|
|
|
@ -94,7 +94,12 @@ func TestRegistrationFlow_BeginFinish(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Finish is the final step in registration.
|
||||
newDevice, err := webRegistration.Finish(ctx, user, test.deviceName, ccr)
|
||||
newDevice, err := webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: user,
|
||||
DeviceName: test.deviceName,
|
||||
CreationResponse: ccr,
|
||||
Passwordless: test.passwordless,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.deviceName, newDevice.GetName())
|
||||
// Did we get a proper WebauthnDevice?
|
||||
|
@ -297,6 +302,7 @@ func TestRegistrationFlow_Finish_errors(t *testing.T) {
|
|||
user, deviceName string
|
||||
createResp func() *wanlib.CredentialCreationResponse
|
||||
wantErr string
|
||||
passwordless bool
|
||||
}{
|
||||
{
|
||||
name: "NOK user empty",
|
||||
|
@ -362,10 +368,29 @@ func TestRegistrationFlow_Finish_errors(t *testing.T) {
|
|||
},
|
||||
wantErr: "validating challenge",
|
||||
},
|
||||
{
|
||||
name: "NOK passwordless on Finish but not on Begin",
|
||||
user: user,
|
||||
deviceName: "webauthn2",
|
||||
passwordless: true,
|
||||
createResp: func() *wanlib.CredentialCreationResponse {
|
||||
cc, err := webRegistration.Begin(ctx, user, false /* passwordless */)
|
||||
require.NoError(t, err)
|
||||
resp, err := key.SignCredentialCreation(webOrigin, cc)
|
||||
require.NoError(t, err)
|
||||
return resp
|
||||
},
|
||||
wantErr: "passwordless registration failed",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := webRegistration.Finish(ctx, test.user, test.deviceName, test.createResp())
|
||||
_, err := webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: test.user,
|
||||
DeviceName: test.deviceName,
|
||||
CreationResponse: test.createResp(),
|
||||
Passwordless: test.passwordless,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), test.wantErr)
|
||||
})
|
||||
|
@ -450,7 +475,11 @@ func TestRegistrationFlow_Finish_attestation(t *testing.T) {
|
|||
ccr, err := dev.SignCredentialCreation(origin, cc)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = webRegistration.Finish(ctx, user, devName, ccr)
|
||||
_, err = webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: user,
|
||||
DeviceName: devName,
|
||||
CreationResponse: ccr,
|
||||
})
|
||||
if ok := err == nil; ok != test.wantOK {
|
||||
t.Errorf("Finish returned err = %v, wantOK = %v", err, test.wantOK)
|
||||
}
|
||||
|
|
|
@ -110,7 +110,11 @@ func TestRegister(t *testing.T) {
|
|||
}
|
||||
require.Equal(t, test.wantRawID, resp.GetWebauthn().RawId)
|
||||
|
||||
_, err = webRegistration.Finish(ctx, user, u2fKey.name, wanlib.CredentialCreationResponseFromProto(resp.GetWebauthn()))
|
||||
_, err = webRegistration.Finish(ctx, wanlib.RegisterResponse{
|
||||
User: user,
|
||||
DeviceName: u2fKey.name,
|
||||
CreationResponse: wanlib.CredentialCreationResponseFromProto(resp.GetWebauthn()),
|
||||
})
|
||||
require.NoError(t, err, "server-side registration failed")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -77,6 +77,10 @@ type addMFADeviceRequest struct {
|
|||
SecondFactorToken string `json:"secondFactorToken"`
|
||||
// WebauthnRegisterResponse is a WebAuthn registration challenge response.
|
||||
WebauthnRegisterResponse *webauthn.CredentialCreationResponse `json:"webauthnRegisterResponse"`
|
||||
// DeviceUsage is the intended usage of the device (MFA, Passwordless, etc).
|
||||
// It mimics the proto.DeviceUsage enum.
|
||||
// Defaults to MFA.
|
||||
DeviceUsage string `json:"deviceUsage"`
|
||||
}
|
||||
|
||||
// addMFADeviceHandle adds a new mfa device for the user defined in the token.
|
||||
|
@ -86,9 +90,15 @@ func (h *Handler) addMFADeviceHandle(w http.ResponseWriter, r *http.Request, par
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
deviceUsage, err := getDeviceUsage(req.DeviceUsage)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
protoReq := &proto.AddMFADeviceSyncRequest{
|
||||
TokenID: req.PrivilegeTokenID,
|
||||
NewDeviceName: req.DeviceName,
|
||||
DeviceUsage: deviceUsage,
|
||||
}
|
||||
|
||||
switch {
|
||||
|
@ -170,14 +180,9 @@ func (h *Handler) createRegisterChallengeWithTokenHandle(w http.ResponseWriter,
|
|||
return nil, trace.BadParameter("MFA device type %q unsupported", req.DeviceType)
|
||||
}
|
||||
|
||||
var deviceUsage proto.DeviceUsage
|
||||
switch strings.ToLower(req.DeviceUsage) {
|
||||
case "", "mfa":
|
||||
deviceUsage = proto.DeviceUsage_DEVICE_USAGE_MFA
|
||||
case "passwordless":
|
||||
deviceUsage = proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS
|
||||
default:
|
||||
return nil, trace.BadParameter("device usage %q unsupported", req.DeviceUsage)
|
||||
deviceUsage, err := getDeviceUsage(req.DeviceUsage)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
chal, err := h.cfg.ProxyClient.CreateRegisterChallenge(r.Context(), &proto.CreateRegisterChallengeRequest{
|
||||
|
@ -191,3 +196,17 @@ func (h *Handler) createRegisterChallengeWithTokenHandle(w http.ResponseWriter,
|
|||
|
||||
return client.MakeRegisterChallenge(chal), nil
|
||||
}
|
||||
|
||||
func getDeviceUsage(reqUsage string) (proto.DeviceUsage, error) {
|
||||
var deviceUsage proto.DeviceUsage
|
||||
switch strings.ToLower(reqUsage) {
|
||||
case "", "mfa":
|
||||
deviceUsage = proto.DeviceUsage_DEVICE_USAGE_MFA
|
||||
case "passwordless":
|
||||
deviceUsage = proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS
|
||||
default:
|
||||
return proto.DeviceUsage_DEVICE_USAGE_UNSPECIFIED, trace.BadParameter("device usage %q unsupported", reqUsage)
|
||||
}
|
||||
|
||||
return deviceUsage, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue