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:
Lisa Kim 2022-05-12 10:31:10 -07:00 committed by GitHub
parent b4eec0d3c5
commit 833848594f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 897 additions and 682 deletions

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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{

View file

@ -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)

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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{

View file

@ -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

View file

@ -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)
}

View file

@ -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")
})
}

View file

@ -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
}