Alias "u2f" to "webauthn" and partially cleanup (#10466)

Alias the "u2f" second factor mode to "webauthn", effectively sunsetting U2F in
favor of WebAuthn.

The change effectively disables "U2F mode" server-side, making Teleport use
WebAuthn instead. This is in line with our compatibility promise, as Teleport
8.x clients are already WebAuthn-capable (and thus have no problems talking to
the cluster).

I have cleaned up a good chunk of U2F references in lib/web and lib/client, plus
a few other places. Changes on lib/auth are just the necessary to get the tests
back to good standing. There is more work to be done, but this seems enough for
a single PR.

#10375

* Remove "Disabled" field from types.Webauthn
* Update generated protos
* Treat second_factor "u2f" as "webauthn"
* Remove references to Webauthn.Disabled
* Remove U2F from lib/web/
* Remove U2F from lib/client/
* Remove U2F from lib/auth/ (partially)
* Fix issues after rebase on master
* Fix typo
This commit is contained in:
Alan Parra 2022-02-23 11:10:13 -03:00 committed by GitHub
parent 5f1eb44af5
commit f8b7b330ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 979 additions and 2173 deletions

View file

@ -134,9 +134,10 @@ const (
SecondFactorOTP = SecondFactorType("otp")
// SecondFactorU2F means that only U2F is supported for 2FA and 2FA is
// required for all users.
// U2F is marked for removal. It currently works as an alias for "webauthn".
SecondFactorU2F = SecondFactorType("u2f")
// SecondFactorWebauthn means that only Webauthn is supported for 2FA and 2FA is
// required for all users.
// SecondFactorWebauthn means that only Webauthn is supported for 2FA and 2FA
// is required for all users.
SecondFactorWebauthn = SecondFactorType("webauthn")
// SecondFactorOn means that all 2FA protocols are supported and 2FA is
// required for all users.

View file

@ -233,23 +233,17 @@ func (c *AuthPreferenceV2) GetPreferredLocalMFA() constants.SecondFactorType {
switch sf := c.GetSecondFactor(); sf {
case constants.SecondFactorOff:
return "" // Nothing to suggest.
case constants.SecondFactorOTP, constants.SecondFactorU2F, constants.SecondFactorWebauthn:
return sf // If using a single method, then that is what it should be.
case constants.SecondFactorOTP:
return sf // Single method.
case constants.SecondFactorU2F, constants.SecondFactorWebauthn:
return constants.SecondFactorWebauthn // Always WebAuthn.
case constants.SecondFactorOn, constants.SecondFactorOptional:
// In order of preference:
// 1. WebAuthn (public-key based)
// 2. U2F (public-key based, deprecated by WebAuthn)
// 3. OTP
//
// Presently, some configurations here are impossible to reach (U2F is
// always required and WebAuthn always exists as a consequence).
// Nevertheless, we make an effort to gracefully handle those situations.
if w, err := c.GetWebauthn(); err == nil && !w.Disabled {
// 2. OTP
if _, err := c.GetWebauthn(); err == nil {
return constants.SecondFactorWebauthn
}
if _, err := c.GetU2F(); err == nil {
return constants.SecondFactorU2F
}
return constants.SecondFactorOTP
default:
log.Warnf("Unexpected second_factor setting: %v", sf)
@ -271,37 +265,24 @@ func (c *AuthPreferenceV2) IsSecondFactorTOTPAllowed() bool {
// IsSecondFactorU2FAllowed checks if users are allowed to register U2F devices.
func (c *AuthPreferenceV2) IsSecondFactorU2FAllowed() bool {
// Is U2F configured?
switch _, err := c.GetU2F(); {
case trace.IsNotFound(err): // OK, expected to happen in some cases.
return false
case err != nil:
log.WithError(err).Warnf("Got unexpected error when reading U2F config")
return false
}
// Are second factor settings in accordance?
return c.Spec.SecondFactor == constants.SecondFactorU2F ||
c.Spec.SecondFactor == constants.SecondFactorOptional ||
c.Spec.SecondFactor == constants.SecondFactorOn
return false // Never allowed, marked for removal.
}
// IsSecondFactorWebauthnAllowed checks if users are allowed to register
// Webauthn devices.
func (c *AuthPreferenceV2) IsSecondFactorWebauthnAllowed() bool {
// Is Webauthn configured and enabled?
switch webConfig, err := c.GetWebauthn(); {
switch _, err := c.GetWebauthn(); {
case trace.IsNotFound(err): // OK, expected to happen in some cases.
return false
case err != nil:
log.WithError(err).Warnf("Got unexpected error when reading Webauthn config")
return false
case webConfig.Disabled: // OK, fallback to U2F in use.
return false
}
// Are second factor settings in accordance?
return c.Spec.SecondFactor == constants.SecondFactorWebauthn ||
return c.Spec.SecondFactor == constants.SecondFactorU2F ||
c.Spec.SecondFactor == constants.SecondFactorWebauthn ||
c.Spec.SecondFactor == constants.SecondFactorOptional ||
c.Spec.SecondFactor == constants.SecondFactorOn
}
@ -428,16 +409,18 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
return trace.BadParameter("authentication type %q not supported", c.Spec.Type)
}
// DELETE IN 11.0, time to sunset U2F (codingllama).
if c.Spec.SecondFactor == constants.SecondFactorU2F {
log.Warnf(`` +
`Second Factor "u2f" is deprecated and marked for removal, using "webauthn" instead. ` +
`Please update your configuration to use WebAuthn. ` +
`Refer to https://goteleport.com/docs/access-controls/guides/webauthn/`)
c.Spec.SecondFactor = constants.SecondFactorWebauthn
}
// make sure second factor makes sense
switch sf := c.Spec.SecondFactor; sf {
case constants.SecondFactorOff, constants.SecondFactorOTP:
case constants.SecondFactorU2F:
if c.Spec.U2F == nil {
return trace.BadParameter("missing required U2F configuration for second factor type %q", sf)
}
if err := c.Spec.U2F.Check(); err != nil {
return trace.Wrap(err)
}
case constants.SecondFactorWebauthn:
// If U2F is present validate it, we can derive Webauthn from it.
if c.Spec.U2F != nil {
@ -449,11 +432,8 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
c.Spec.Webauthn = &Webauthn{}
}
}
switch {
case c.Spec.Webauthn == nil:
if c.Spec.Webauthn == nil {
return trace.BadParameter("missing required webauthn configuration for second factor type %q", sf)
case c.Spec.Webauthn.Disabled:
return trace.BadParameter("disabled webauthn configuration not allowed for second factor type %q", sf)
}
if err := c.Spec.Webauthn.CheckAndSetDefaults(c.Spec.U2F); err != nil {
return trace.Wrap(err)
@ -462,13 +442,9 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
// The following scenarios are allowed for "on" and "optional":
// - Webauthn is configured (preferred)
// - U2F is configured, Webauthn derived from it (U2F-compat mode)
// - U2F is configured, Webauthn is disabled (fallback mode)
switch {
case c.Spec.Webauthn == nil && c.Spec.U2F == nil:
if c.Spec.U2F == nil && c.Spec.Webauthn == nil {
return trace.BadParameter("missing required webauthn configuration for second factor type %q", sf)
case c.Spec.Webauthn != nil && c.Spec.Webauthn.Disabled && c.Spec.U2F == nil:
return trace.BadParameter("missing u2f configuration with disabled webauthn not allowed for second factor %q", sf)
}
// Is U2F configured?

View file

@ -75,6 +75,7 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_secondFactor(t *testing.T) {
constants.SecondFactorOptional,
}
secondFactorWebActive := []constants.SecondFactorType{
constants.SecondFactorU2F,
constants.SecondFactorWebauthn,
constants.SecondFactorOn,
constants.SecondFactorOptional,
@ -121,11 +122,16 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_secondFactor(t *testing.T) {
},
},
{
name: "NOK U2F missing U2F",
name: "OK U2F aliased to Webauthn",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorU2F, // only mode where U2F is mandatory
constants.SecondFactorU2F,
},
spec: types.AuthPreferenceSpecV2{
U2F: minimalU2F,
},
assertFn: func(t *testing.T, got *types.AuthPreferenceV2) {
require.Equal(t, constants.SecondFactorWebauthn, got.Spec.SecondFactor)
},
wantErr: "missing required U2F configuration",
},
// Webauthn tests.
{
@ -185,52 +191,6 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_secondFactor(t *testing.T) {
require.Empty(t, cmp.Diff(wantWeb, gotWeb))
},
},
{
name: "OK Webauthn disabled with fallback",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorOff,
constants.SecondFactorOTP,
constants.SecondFactorU2F,
// constants.SecondFactorWebauthn excluded
constants.SecondFactorOn,
constants.SecondFactorOptional,
},
spec: types.AuthPreferenceSpecV2{
U2F: minimalU2F,
Webauthn: &types.Webauthn{
Disabled: true,
},
},
assertFn: func(t *testing.T, got *types.AuthPreferenceV2) {
require.False(t, got.IsSecondFactorWebauthnAllowed(), "webauthn second factor allowed")
require.NotEqual(t, constants.SecondFactorWebauthn, got.GetPreferredLocalMFA(), "webauthn set as preferred MFA")
},
},
{
name: "NOK Webauthn disabled in own mode",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorWebauthn,
},
spec: types.AuthPreferenceSpecV2{
Webauthn: &types.Webauthn{
Disabled: true,
},
},
wantErr: "disabled webauthn configuration not allowed",
},
{
name: "NOK Webauthn disabled without fallback",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorOn,
constants.SecondFactorOptional,
},
spec: types.AuthPreferenceSpecV2{
Webauthn: &types.Webauthn{
Disabled: true,
},
},
wantErr: "missing u2f configuration",
},
{
name: "OK Webauthn with attestation CAs",
secondFactors: secondFactorWebActive,
@ -338,51 +298,26 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_secondFactor(t *testing.T) {
},
},
{
name: "OK U2F second factor allowed",
name: "OK U2F second factor never allowed",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorOff,
constants.SecondFactorOTP,
constants.SecondFactorU2F,
constants.SecondFactorWebauthn,
constants.SecondFactorOn,
constants.SecondFactorOptional,
},
spec: types.AuthPreferenceSpecV2{
U2F: minimalU2F,
},
assertFn: func(t *testing.T, got *types.AuthPreferenceV2) {
require.True(t, got.IsSecondFactorU2FAllowed(), "U2F not allowed")
},
},
{
name: "OK U2F second factor not allowed",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorOff,
constants.SecondFactorOTP,
constants.SecondFactorWebauthn,
},
spec: types.AuthPreferenceSpecV2{
U2F: minimalU2F,
Webauthn: minimalWeb,
},
assertFn: func(t *testing.T, got *types.AuthPreferenceV2) {
require.False(t, got.IsSecondFactorU2FAllowed(), "U2F allowed")
},
},
{
name: "OK U2F second factor not allowed when not configured",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorOn,
constants.SecondFactorOptional,
},
spec: types.AuthPreferenceSpecV2{
Webauthn: minimalWeb,
},
assertFn: func(t *testing.T, got *types.AuthPreferenceV2) {
require.False(t, got.IsSecondFactorU2FAllowed(), "U2F allowed")
},
},
{
name: "OK Webauthn second factor allowed",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorU2F,
constants.SecondFactorWebauthn,
constants.SecondFactorOn,
constants.SecondFactorOptional,
@ -399,7 +334,6 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_secondFactor(t *testing.T) {
secondFactors: []constants.SecondFactorType{
constants.SecondFactorOff,
constants.SecondFactorOTP,
constants.SecondFactorU2F,
},
spec: types.AuthPreferenceSpecV2{
U2F: minimalU2F,
@ -409,22 +343,6 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_secondFactor(t *testing.T) {
require.False(t, got.IsSecondFactorWebauthnAllowed(), "Webauthn allowed")
},
},
{
name: "OK Webauthn second factor not allowed when disabled",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorOn,
constants.SecondFactorOptional,
},
spec: types.AuthPreferenceSpecV2{
U2F: minimalU2F,
Webauthn: &types.Webauthn{
Disabled: true,
},
},
assertFn: func(t *testing.T, got *types.AuthPreferenceV2) {
require.False(t, got.IsSecondFactorWebauthnAllowed(), "Webauthn allowed")
},
},
// GetPreferredLocalMFA
{
name: "OK preferred local MFA empty",
@ -444,26 +362,10 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_secondFactor(t *testing.T) {
require.Equal(t, constants.SecondFactorOTP, got.GetPreferredLocalMFA())
},
},
{
name: "OK preferred local MFA = U2F",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorU2F,
constants.SecondFactorOn,
constants.SecondFactorOptional,
},
spec: types.AuthPreferenceSpecV2{
U2F: minimalU2F,
Webauthn: &types.Webauthn{
Disabled: true,
},
},
assertFn: func(t *testing.T, got *types.AuthPreferenceV2) {
require.Equal(t, constants.SecondFactorU2F, got.GetPreferredLocalMFA())
},
},
{
name: "OK preferred local MFA = Webauthn",
secondFactors: []constants.SecondFactorType{
constants.SecondFactorU2F,
constants.SecondFactorWebauthn,
constants.SecondFactorOn,
constants.SecondFactorOptional,

File diff suppressed because it is too large Load diff

View file

@ -1083,14 +1083,7 @@ message Webauthn {
// By default no devices are denied.
repeated string AttestationDeniedCAs = 3
[ (gogoproto.jsontag) = "attestation_denied_cas,omitempty" ];
// Disables Webauthn, regardless of other cluster settings.
// Allows fallback to pure U2F in clusters with second_factor:on or
// second_factor:optional.
// Must not be set for clusters with second_factor:webauthn.
// Temporary safety switch for Webauthn, to be removed in future versions of
// Teleport.
// DELETE IN 9.x, fallback not possible without U2F (codingllama).
bool Disabled = 4 [ (gogoproto.jsontag) = "disabled,omitempty" ];
reserved 4; // bool Disabled
}
// Namespace represents namespace resource specification

View file

@ -30,8 +30,6 @@ import (
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
wantypes "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/modules"
@ -682,21 +680,6 @@ func TestCompleteAccountRecovery(t *testing.T) {
}
},
},
{
name: "add new U2F device",
getRequest: func() *proto.CompleteAccountRecoveryRequest {
u2fRegResp, _, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), approvedToken.GetName())
require.NoError(t, err)
return &proto.CompleteAccountRecoveryRequest{
NewDeviceName: "new-u2f",
RecoveryApprovedTokenID: approvedToken.GetName(),
NewAuthnCred: &proto.CompleteAccountRecoveryRequest_NewMFAResponse{NewMFAResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_U2F{U2F: u2fRegResp},
}},
}
},
},
{
name: "add new WEBAUTHN device",
getRequest: func() *proto.CompleteAccountRecoveryRequest {
@ -827,16 +810,16 @@ func TestCompleteAccountRecovery_WithErrors(t *testing.T) {
require.NoError(t, err)
require.NotEmpty(t, devs)
// New u2f register response.
u2fRegResp, _, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), approvedToken.GetName())
// New register response.
_, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), approvedToken.GetName())
require.NoError(t, err)
return &proto.CompleteAccountRecoveryRequest{
RecoveryApprovedTokenID: approvedToken.GetName(),
NewDeviceName: devs[0].GetName(),
NewAuthnCred: &proto.CompleteAccountRecoveryRequest_NewMFAResponse{NewMFAResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_U2F{U2F: u2fRegResp},
}},
NewAuthnCred: &proto.CompleteAccountRecoveryRequest_NewMFAResponse{
NewMFAResponse: mfaResp,
},
}
},
},
@ -1015,35 +998,6 @@ func TestAccountRecoveryFlow(t *testing.T) {
}
},
},
{
name: "recover u2f with password",
getStartRequest: func(u *userAuthCreds) *proto.StartAccountRecoveryRequest {
return &proto.StartAccountRecoveryRequest{
Username: u.username,
RecoverType: types.UserTokenUsage_USER_TOKEN_RECOVER_MFA,
RecoveryCode: []byte(u.recoveryCodes[0]),
}
},
getApproveRequest: func(u *userAuthCreds, c *proto.MFAAuthenticateChallenge, startTokenID string) *proto.VerifyAccountRecoveryRequest {
return &proto.VerifyAccountRecoveryRequest{
RecoveryStartTokenID: startTokenID,
Username: u.username,
AuthnCred: &proto.VerifyAccountRecoveryRequest_Password{Password: u.password},
}
},
getCompleteRequest: func(u *userAuthCreds, approvedTokenID string) *proto.CompleteAccountRecoveryRequest {
u2fRegResp, _, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), approvedTokenID)
require.NoError(t, err)
return &proto.CompleteAccountRecoveryRequest{
NewDeviceName: "new-u2f",
RecoveryApprovedTokenID: approvedTokenID,
NewAuthnCred: &proto.CompleteAccountRecoveryRequest_NewMFAResponse{NewMFAResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_U2F{U2F: u2fRegResp},
}},
}
},
},
{
name: "recover otp with password",
getStartRequest: func(u *userAuthCreds) *proto.StartAccountRecoveryRequest {
@ -1370,9 +1324,8 @@ func createUserWithSecondFactors(srv *TestTLSServer) (*userAuthCreds, error) {
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOn,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
// Use default Webauthn config.
})
@ -1396,17 +1349,16 @@ func createUserWithSecondFactors(srv *TestTLSServer) (*userAuthCreds, error) {
return nil, trace.Wrap(err)
}
// Insert a password, u2f device, and recovery codes.
u2fRegResp, u2fKey, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), resetToken.GetName())
// Insert a password, device, and recovery codes.
webDev, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetToken.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
res, err := srv.Auth().ChangeUserAuthentication(ctx, &proto.ChangeUserAuthenticationRequest{
TokenID: resetToken.GetName(),
NewPassword: password,
NewMFARegisterResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_U2F{U2F: u2fRegResp}},
TokenID: resetToken.GetName(),
NewPassword: password,
NewMFARegisterResponse: mfaResp,
})
if err != nil {
return nil, trace.Wrap(err)
@ -1417,12 +1369,7 @@ func createUserWithSecondFactors(srv *TestTLSServer) (*userAuthCreds, error) {
return nil, trace.Wrap(err)
}
u2fDev := &TestDevice{Key: u2fKey}
totpDev, err := RegisterTestDevice(ctx, clt, "otp-1", proto.DeviceType_DEVICE_TYPE_TOTP, u2fDev, WithTestDeviceClock(srv.Clock()))
if err != nil {
return nil, trace.Wrap(err)
}
webDev, err := RegisterTestDevice(ctx, clt, "web-1", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, u2fDev)
totpDev, err := RegisterTestDevice(ctx, clt, "otp-1", proto.DeviceType_DEVICE_TYPE_TOTP, webDev, WithTestDeviceClock(srv.Clock()))
if err != nil {
return nil, trace.Wrap(err)
}
@ -1436,36 +1383,6 @@ func createUserWithSecondFactors(srv *TestTLSServer) (*userAuthCreds, error) {
}, nil
}
// DELETE IN 9.0.0 in favor of getMockedWebauthnAndRegisterRes.
func getLegacyMockedU2FAndRegisterRes(authSrv *Server, tokenID string) (*proto.U2FRegisterResponse, *mocku2f.Key, error) {
res, err := authSrv.CreateRegisterChallenge(context.Background(), &proto.CreateRegisterChallengeRequest{
TokenID: tokenID,
DeviceType: proto.DeviceType_DEVICE_TYPE_U2F,
})
if err != nil {
return nil, nil, trace.Wrap(err)
}
u2fKey, err := mocku2f.Create()
if err != nil {
return nil, nil, trace.Wrap(err)
}
u2fRegResp, err := u2fKey.RegisterResponse(&u2f.RegisterChallenge{
Version: res.GetU2F().GetVersion(),
Challenge: res.GetU2F().GetChallenge(),
AppID: res.GetU2F().GetAppID(),
})
if err != nil {
return nil, nil, trace.Wrap(err)
}
return &proto.U2FRegisterResponse{
RegistrationData: u2fRegResp.RegistrationData,
ClientData: u2fRegResp.ClientData,
}, u2fKey, nil
}
func getMockedWebauthnAndRegisterRes(authSrv *Server, tokenID string) (*TestDevice, *proto.MFARegisterResponse, error) {
res, err := authSrv.CreateRegisterChallenge(context.Background(), &proto.CreateRegisterChallengeRequest{
TokenID: tokenID,

View file

@ -69,22 +69,6 @@ func TestServer_CreateAuthenticateChallenge_authPreference(t *testing.T) {
require.Empty(t, challenge.GetWebauthnChallenge())
},
},
{
name: "OK second_factor:u2f",
spec: &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorU2F,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
},
},
assertChallenge: func(challenge *proto.MFAAuthenticateChallenge) {
require.Empty(t, challenge.GetTOTP())
require.NotEmpty(t, challenge.GetU2F())
require.Empty(t, challenge.GetWebauthnChallenge())
},
},
{
name: "OK second_factor:webauthn (derived from U2F)",
spec: &types.AuthPreferenceSpecV2{
@ -144,34 +128,13 @@ func TestServer_CreateAuthenticateChallenge_authPreference(t *testing.T) {
spec: &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
},
},
assertChallenge: func(challenge *proto.MFAAuthenticateChallenge) {
require.NotNil(t, challenge.GetTOTP())
require.NotEmpty(t, challenge.GetU2F())
require.NotEmpty(t, challenge.GetWebauthnChallenge())
},
},
{
name: "OK second_factor:optional with global Webauthn disable",
spec: &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
},
Webauthn: &types.Webauthn{
Disabled: true,
RPID: "localhost",
},
},
assertChallenge: func(challenge *proto.MFAAuthenticateChallenge) {
require.NotNil(t, challenge.GetTOTP())
require.NotEmpty(t, challenge.GetU2F())
require.Empty(t, challenge.GetWebauthnChallenge())
require.NotEmpty(t, challenge.GetWebauthnChallenge())
},
},
{
@ -179,14 +142,12 @@ func TestServer_CreateAuthenticateChallenge_authPreference(t *testing.T) {
spec: &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOn,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
},
assertChallenge: func(challenge *proto.MFAAuthenticateChallenge) {
require.NotNil(t, challenge.GetTOTP())
require.NotEmpty(t, challenge.GetU2F())
require.NotEmpty(t, challenge.GetWebauthnChallenge())
},
},
@ -198,10 +159,11 @@ func TestServer_CreateAuthenticateChallenge_authPreference(t *testing.T) {
require.NoError(t, authServer.SetAuthPreference(ctx, authPreference))
challenge, err := authServer.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_UserCredentials{UserCredentials: &proto.UserCredentials{
Username: username,
Password: []byte(password),
}},
Request: &proto.CreateAuthenticateChallengeRequest_UserCredentials{
UserCredentials: &proto.UserCredentials{
Username: username,
Password: []byte(password),
}},
})
require.NoError(t, err)
test.assertChallenge(challenge)
@ -233,7 +195,6 @@ func TestServer_AuthenticateUser_mfaDevices(t *testing.T) {
solveChallenge func(*proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error)
}{
{name: "OK TOTP device", solveChallenge: mfa.TOTPDev.SolveAuthn},
{name: "OK U2F device", solveChallenge: mfa.U2FDev.SolveAuthn},
{name: "OK Webauthn device", solveChallenge: mfa.WebDev.SolveAuthn},
}
for _, test := range tests {
@ -371,7 +332,6 @@ func TestCreateAuthenticateChallenge_WithUserCredentials(t *testing.T) {
default:
require.NoError(t, err)
require.NotNil(t, res.GetTOTP())
require.NotEmpty(t, res.GetU2F())
require.NotEmpty(t, res.GetWebauthnChallenge())
}
})
@ -463,7 +423,6 @@ func TestCreateAuthenticateChallenge_WithRecoveryStartToken(t *testing.T) {
default:
require.NoError(t, err)
require.NotNil(t, res.GetTOTP())
require.NotEmpty(t, res.GetU2F())
require.NotEmpty(t, res.GetWebauthnChallenge())
}
})
@ -502,10 +461,6 @@ func TestCreateRegisterChallenge(t *testing.T) {
wantErr bool
deviceType proto.DeviceType
}{
{
name: "u2f challenge",
deviceType: proto.DeviceType_DEVICE_TYPE_U2F,
},
{
name: "totp challenge",
deviceType: proto.DeviceType_DEVICE_TYPE_TOTP,
@ -529,10 +484,6 @@ func TestCreateRegisterChallenge(t *testing.T) {
switch tc.deviceType {
case proto.DeviceType_DEVICE_TYPE_TOTP:
require.NotNil(t, res.GetTOTP().GetQRCode())
case proto.DeviceType_DEVICE_TYPE_U2F:
require.NotNil(t, res.GetU2F())
case proto.DeviceType_DEVICE_TYPE_WEBAUTHN:
require.NotNil(t, res.GetWebauthn())
}
@ -541,17 +492,16 @@ func TestCreateRegisterChallenge(t *testing.T) {
}
type configureMFAResp struct {
User, Password string
TOTPDev, U2FDev, WebDev *TestDevice
User, Password string
TOTPDev, WebDev *TestDevice
}
func configureForMFA(t *testing.T, srv *TestTLSServer) *configureMFAResp {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
// Use default Webauthn config.
})
@ -574,9 +524,6 @@ func configureForMFA(t *testing.T, srv *TestTLSServer) *configureMFAResp {
totpDev, err := RegisterTestDevice(ctx, clt, "totp-1", proto.DeviceType_DEVICE_TYPE_TOTP, nil, WithTestDeviceClock(srv.Clock()))
require.NoError(t, err)
u2fDev, err := RegisterTestDevice(ctx, clt, "u2f-1", proto.DeviceType_DEVICE_TYPE_U2F, totpDev)
require.NoError(t, err)
webDev, err := RegisterTestDevice(ctx, clt, "web-1", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, totpDev)
require.NoError(t, err)
@ -584,7 +531,6 @@ func configureForMFA(t *testing.T, srv *TestTLSServer) *configureMFAResp {
User: username,
Password: password,
TOTPDev: totpDev,
U2FDev: u2fDev,
WebDev: webDev,
}
}

View file

@ -1591,27 +1591,6 @@ func TestAddMFADeviceSync(t *testing.T) {
}
},
},
{
name: "U2F device with privilege exception token",
deviceName: "new-u2f",
getReq: func(deviceName string) *proto.AddMFADeviceSyncRequest {
// Obtain a privilege exception token.
privExToken, err := srv.Auth().createPrivilegeToken(ctx, u.username, UserTokenTypePrivilegeException)
require.NoError(t, err)
// Create register challenge and sign.
u2fRegRes, _, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), privExToken.GetName())
require.NoError(t, err)
return &proto.AddMFADeviceSyncRequest{
TokenID: privExToken.GetName(),
NewDeviceName: deviceName,
NewMFAResponse: &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_U2F{
U2F: u2fRegRes,
}},
}
},
},
{
name: "Webauthn device with privilege exception token",
deviceName: "new-webauthn",

View file

@ -19,7 +19,6 @@ package auth
import (
"context"
"encoding/base32"
"encoding/base64"
"fmt"
"net"
"sort"
@ -38,7 +37,6 @@ import (
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/defaults"
@ -98,39 +96,6 @@ func TestMFADeviceManagement(t *testing.T) {
desc string
opts mfaAddTestOpts
}{
{
desc: "fail U2F auth challenge",
opts: mfaAddTestOpts{
initReq: &proto.AddMFADeviceRequestInit{
DeviceName: "fail-dev",
DeviceType: proto.DeviceType_DEVICE_TYPE_U2F,
},
authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.Len(t, req.U2F, 1)
chal := req.U2F[0]
// Use a different, unregistered device, which should fail
// the authentication challenge.
keyHandle, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(chal.KeyHandle)
require.NoError(t, err)
badDev, err := mocku2f.CreateWithKeyHandle(keyHandle)
require.NoError(t, err)
mresp, err := badDev.SignResponse(&u2f.AuthenticateChallenge{
Challenge: chal.Challenge,
KeyHandle: chal.KeyHandle,
AppID: chal.AppID,
})
require.NoError(t, err)
return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_U2F{U2F: &proto.U2FResponse{
KeyHandle: mresp.KeyHandle,
ClientData: mresp.ClientData,
Signature: mresp.SignatureData,
}}}
},
checkAuthErr: require.Error,
},
},
{
desc: "fail TOTP auth challenge",
opts: mfaAddTestOpts{
@ -155,35 +120,6 @@ func TestMFADeviceManagement(t *testing.T) {
checkAuthErr: require.Error,
},
},
{
desc: "fail a U2F registration challenge",
opts: mfaAddTestOpts{
initReq: &proto.AddMFADeviceRequestInit{
DeviceName: "fail-dev",
DeviceType: proto.DeviceType_DEVICE_TYPE_U2F,
},
authHandler: devs.u2fAuthHandler,
checkAuthErr: require.NoError,
registerHandler: func(t *testing.T, req *proto.MFARegisterChallenge) *proto.MFARegisterResponse {
u2fRegisterChallenge := req.GetU2F()
require.NotEmpty(t, u2fRegisterChallenge)
mdev, err := mocku2f.Create()
require.NoError(t, err)
mresp, err := mdev.RegisterResponse(&u2f.RegisterChallenge{
Challenge: u2fRegisterChallenge.Challenge,
AppID: "wrong app ID", // This should cause registration to fail.
})
require.NoError(t, err)
return &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_U2F{U2F: &proto.U2FRegisterResponse{
RegistrationData: mresp.RegistrationData,
ClientData: mresp.ClientData,
}}}
},
checkRegisterErr: require.Error,
},
},
{
desc: "fail a TOTP registration challenge",
opts: mfaAddTestOpts{
@ -314,7 +250,7 @@ func TestMFADeviceManagement(t *testing.T) {
deviceIDs[dev.GetName()] = dev.Id
}
sort.Strings(deviceNames)
require.Equal(t, deviceNames, []string{devs.TOTPName, devs.U2FName, devs.WebName, webDev2Name})
require.Equal(t, deviceNames, []string{devs.TOTPName, devs.WebName, webDev2Name})
// Delete several of the MFA devices.
deleteTests := []struct {
@ -354,38 +290,6 @@ func TestMFADeviceManagement(t *testing.T) {
checkErr: require.Error,
},
},
{
desc: "fail a U2F auth challenge",
opts: mfaDeleteTestOpts{
initReq: &proto.DeleteMFADeviceRequestInit{
DeviceName: devs.U2FName,
},
authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.Len(t, req.U2F, 1)
chal := req.U2F[0]
// Use a different, unregistered device, which should fail
// the authentication challenge.
keyHandle, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(chal.KeyHandle)
require.NoError(t, err)
badDev, err := mocku2f.CreateWithKeyHandle(keyHandle)
require.NoError(t, err)
mresp, err := badDev.SignResponse(&u2f.AuthenticateChallenge{
Challenge: chal.Challenge,
KeyHandle: chal.KeyHandle,
AppID: chal.AppID,
})
require.NoError(t, err)
return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_U2F{U2F: &proto.U2FResponse{
KeyHandle: mresp.KeyHandle,
ClientData: mresp.ClientData,
Signature: mresp.SignatureData,
}}}
},
checkErr: require.Error,
},
},
{
desc: "fail a webauthn auth challenge",
opts: mfaDeleteTestOpts{
@ -421,16 +325,6 @@ func TestMFADeviceManagement(t *testing.T) {
checkErr: require.NoError,
},
},
{
desc: "delete U2F device by ID",
opts: mfaDeleteTestOpts{
initReq: &proto.DeleteMFADeviceRequestInit{
DeviceName: deviceIDs[devs.U2FName],
},
authHandler: devs.u2fAuthHandler,
checkErr: require.NoError,
},
},
{
desc: "delete webauthn device by name",
opts: mfaDeleteTestOpts{
@ -478,8 +372,6 @@ type mfaDevices struct {
webOrigin string
TOTPName, TOTPSecret string
U2FName string
U2FKey *mocku2f.Key
WebName string
WebKey *mocku2f.Key
}
@ -501,28 +393,6 @@ func (d *mfaDevices) totpAuthHandler(t *testing.T, challenge *proto.MFAAuthentic
}
}
func (d *mfaDevices) u2fAuthHandler(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.Len(t, challenge.U2F, 1)
c := challenge.U2F[0]
resp, err := d.U2FKey.SignResponse(&u2f.AuthenticateChallenge{
Challenge: c.Challenge,
KeyHandle: c.KeyHandle,
AppID: c.AppID,
})
require.NoError(t, err)
return &proto.MFAAuthenticateResponse{
Response: &proto.MFAAuthenticateResponse_U2F{
U2F: &proto.U2FResponse{
KeyHandle: resp.KeyHandle,
ClientData: resp.ClientData,
Signature: resp.SignatureData,
},
},
}
}
func (d *mfaDevices) webAuthHandler(t *testing.T, challenge *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
require.NotNil(t, challenge.WebauthnChallenge)
@ -538,19 +408,15 @@ func (d *mfaDevices) webAuthHandler(t *testing.T, challenge *proto.MFAAuthentica
func addOneOfEachMFADevice(t *testing.T, cl *Client, clock clockwork.Clock, origin string) mfaDevices {
const totpName = "totp-dev"
const u2fName = "u2f-dev"
const webName = "webauthn-dev"
devs := mfaDevices{
clock: clock,
webOrigin: origin,
TOTPName: totpName,
U2FName: u2fName,
WebName: webName,
}
var err error
devs.U2FKey, err = mocku2f.Create()
require.NoError(t, err)
devs.WebKey, err = mocku2f.Create()
require.NoError(t, err)
devs.WebKey.PreferRPID = true
@ -602,49 +468,6 @@ func addOneOfEachMFADevice(t *testing.T, cl *Client, clock clockwork.Clock, orig
},
},
},
{
name: "U2F device",
opts: mfaAddTestOpts{
initReq: &proto.AddMFADeviceRequestInit{
DeviceName: u2fName,
DeviceType: proto.DeviceType_DEVICE_TYPE_U2F,
},
authHandler: devs.totpAuthHandler,
checkAuthErr: require.NoError,
registerHandler: func(t *testing.T, challenge *proto.MFARegisterChallenge) *proto.MFARegisterResponse {
require.NotEmpty(t, challenge.GetU2F())
resp, err := devs.U2FKey.RegisterResponse(&u2f.RegisterChallenge{
Challenge: challenge.GetU2F().Challenge,
AppID: challenge.GetU2F().AppID,
})
require.NoError(t, err)
return &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_U2F{
U2F: &proto.U2FRegisterResponse{
RegistrationData: resp.RegistrationData,
ClientData: resp.ClientData,
},
},
}
},
checkRegisterErr: require.NoError,
assertRegisteredDev: func(t *testing.T, got *types.MFADevice) {
want, err := u2f.NewDevice(
u2fName,
&u2f.Registration{
KeyHandle: devs.U2FKey.KeyHandle,
PubKey: devs.U2FKey.PrivateKey.PublicKey,
},
clock.Now(),
)
want.Id = got.Id
require.NoError(t, err)
require.Empty(t, cmp.Diff(want, got))
},
},
},
{
name: "Webauthn device",
opts: mfaAddTestOpts{
@ -798,17 +621,6 @@ func TestDeleteLastMFADevice(t *testing.T) {
checkErr: require.Error,
},
},
{
name: "NOK sf=U2F trying to delete last U2F device",
secondFactor: constants.SecondFactorU2F,
opts: mfaDeleteTestOpts{
initReq: &proto.DeleteMFADeviceRequestInit{
DeviceName: devs.U2FName,
},
authHandler: devs.u2fAuthHandler,
checkErr: require.Error,
},
},
{
name: "NOK sf=Webauthn trying to delete last Webauthn device",
secondFactor: constants.SecondFactorWebauthn,
@ -831,17 +643,6 @@ func TestDeleteLastMFADevice(t *testing.T) {
checkErr: require.NoError,
},
},
{
name: "OK delete U2F device",
secondFactor: constants.SecondFactorOn,
opts: mfaDeleteTestOpts{
initReq: &proto.DeleteMFADeviceRequestInit{
DeviceName: devs.U2FName,
},
authHandler: devs.u2fAuthHandler,
checkErr: require.NoError,
},
},
{
name: "NOK sf=on trying to delete last MFA device",
secondFactor: constants.SecondFactorOn,
@ -960,13 +761,11 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
// Fetch MFA device IDs.
devs, err := srv.Auth().Identity.GetMFADevices(ctx, user.GetName(), false)
require.NoError(t, err)
var u2fDevID, webDevID string
var webDevID string
for _, dev := range devs {
switch {
case dev.GetU2F() != nil:
u2fDevID = dev.Id
case dev.GetWebauthn() != nil:
if dev.GetWebauthn() != nil {
webDevID = dev.Id
break
}
}
@ -977,32 +776,6 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
desc string
opts generateUserSingleUseCertTestOpts
}{
{
desc: "ssh using U2F",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
},
checkInitErr: require.NoError,
authHandler: registered.u2fAuthHandler,
checkAuthErr: require.NoError,
validateCert: func(t *testing.T, c *proto.SingleUseUserCert) {
crt := c.GetSSH()
require.NotEmpty(t, crt)
cert, err := sshutils.ParseCertificate(crt)
require.NoError(t, err)
require.Equal(t, cert.Extensions[teleport.CertExtensionMFAVerified], u2fDevID)
require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionClientIP]).IsLoopback())
require.Equal(t, cert.ValidBefore, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()))
},
},
},
{
desc: "ssh using webauthn",
opts: generateUserSingleUseCertTestOpts{

View file

@ -249,11 +249,6 @@ func TestServer_ChangePassword(t *testing.T) {
newPass: "llamasarecool11",
device: mfa.TOTPDev,
},
{
name: "OK U2F-based change",
newPass: "llamasarecool12",
device: mfa.U2FDev,
},
{
name: "OK Webauthn-based change",
newPass: "llamasarecool13",
@ -377,42 +372,6 @@ func TestChangeUserAuthentication(t *testing.T) {
}
},
},
{
name: "with second factor u2f",
setAuthPreference: func() {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorU2F,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
},
})
require.NoError(t, err)
err = srv.Auth().SetAuthPreference(ctx, authPreference)
require.NoError(t, err)
},
getReq: func(resetTokenID string) *proto.ChangeUserAuthenticationRequest {
u2fRegResp, _, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), resetTokenID)
require.NoError(t, err)
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password3"),
NewMFARegisterResponse: &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_U2F{
U2F: u2fRegResp,
}},
}
},
// Invalid totp fields when auth settings set to only u2f.
getInvalidReq: func(resetTokenID string) *proto.ChangeUserAuthenticationRequest {
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password3"),
NewMFARegisterResponse: &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_TOTP{}},
}
},
},
{
name: "with second factor webauthn",
setAuthPreference: func() {
@ -485,9 +444,8 @@ func TestChangeUserAuthentication(t *testing.T) {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOn,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
@ -495,15 +453,13 @@ func TestChangeUserAuthentication(t *testing.T) {
require.NoError(t, err)
},
getReq: func(resetTokenID string) *proto.ChangeUserAuthenticationRequest {
u2fRegResp, _, err := getLegacyMockedU2FAndRegisterRes(srv.Auth(), resetTokenID)
_, mfaResp, err := getMockedWebauthnAndRegisterRes(srv.Auth(), resetTokenID)
require.NoError(t, err)
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password4"),
NewMFARegisterResponse: &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_U2F{
U2F: u2fRegResp,
}},
TokenID: resetTokenID,
NewPassword: []byte("password4"),
NewMFARegisterResponse: mfaResp,
}
},
// Empty register response, when auth settings requires second factors.

View file

@ -63,10 +63,7 @@ type loginFlow struct {
}
func (f *loginFlow) begin(ctx context.Context, user string, passwordless bool) (*CredentialAssertion, error) {
switch {
case f.Webauthn.Disabled:
return nil, trace.BadParameter("webauthn disabled")
case user == "" && !passwordless:
if user == "" && !passwordless {
return nil, trace.BadParameter("user required")
}
@ -170,8 +167,6 @@ func beginLogin(
func (f *loginFlow) finish(ctx context.Context, user string, resp *CredentialAssertionResponse, passwordless bool) (*types.MFADevice, string, error) {
switch {
case f.Webauthn.Disabled:
return nil, "", trace.BadParameter("webauthn disabled")
case user == "" && !passwordless:
return nil, "", trace.BadParameter("user required")
case resp == nil:

View file

@ -35,84 +35,6 @@ import (
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
)
func TestWebauthnGlobalDisable(t *testing.T) {
ctx := context.Background()
const user = "llama"
cfg := &types.Webauthn{
RPID: "localhost",
Disabled: true,
}
identity := newFakeIdentity(user)
loginFlow := &wanlib.LoginFlow{
Webauthn: cfg,
Identity: identity,
}
passwordlessFlow := &wanlib.PasswordlessFlow{
Webauthn: cfg,
Identity: identity,
}
registrationFlow := &wanlib.RegistrationFlow{
Webauthn: cfg,
Identity: identity,
}
tests := []struct {
name string
fn func() error
}{
{
name: "LoginFlow.Begin",
fn: func() error {
_, err := loginFlow.Begin(ctx, user)
return err
},
},
{
name: "LoginFlow.Finish",
fn: func() error {
_, err := loginFlow.Finish(ctx, user, &wanlib.CredentialAssertionResponse{})
return err
},
},
{
name: "PasswordlessFlow.Begin",
fn: func() error {
_, err := passwordlessFlow.Begin(ctx)
return err
},
},
{
name: "PasswordlessFlow.Finish",
fn: func() error {
_, _, err := passwordlessFlow.Finish(ctx, &wanlib.CredentialAssertionResponse{})
return err
},
},
{
name: "RegistrationFlow.Begin",
fn: func() error {
_, err := registrationFlow.Begin(ctx, user, false /* passwordless */)
return err
},
},
{
name: "RegistrationFlow.Finish",
fn: func() error {
_, err := registrationFlow.Finish(ctx, user, "devName", &wanlib.CredentialCreationResponse{})
return err
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.fn()
require.Error(t, err)
require.Contains(t, err.Error(), "webauthn disabled")
})
}
}
func TestLoginFlow_BeginFinish(t *testing.T) {
// Simulate a previously registered U2F device.
u2fKey, err := mocku2f.Create()

View file

@ -126,10 +126,7 @@ type RegistrationFlow struct {
// As a side effect Begin may assign (and record in storage) a WebAuthn ID for
// the user.
func (f *RegistrationFlow) Begin(ctx context.Context, user string, passwordless bool) (*CredentialCreation, error) {
switch {
case f.Webauthn.Disabled:
return nil, trace.BadParameter("webauthn disabled")
case user == "":
if user == "" {
return nil, trace.BadParameter("user required")
}
@ -223,8 +220,6 @@ func upsertOrGetWebID(ctx context.Context, user string, identity RegistrationIde
// 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) {
switch {
case f.Webauthn.Disabled:
return nil, trace.BadParameter("webauthn disabled")
case user == "":
return nil, trace.BadParameter("user required")
case deviceName == "":

View file

@ -18,12 +18,10 @@ import (
"bytes"
"context"
"encoding/base32"
"encoding/base64"
"io"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -33,7 +31,6 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
@ -41,12 +38,10 @@ import (
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/prompt"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/require"
u2flib "github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
log "github.com/sirupsen/logrus"
)
@ -82,43 +77,16 @@ func TestTeleportClient_Login_localMFALogin(t *testing.T) {
cfg.KeysDir = t.TempDir()
cfg.InsecureSkipVerify = true
// Replace (and later reset) user-prompting functions.
oldPwd, oldOTP, oldU2F, oldWAN := *client.PasswordFromConsoleFn, *client.PromptOTP, *client.PromptU2F, *client.PromptWebauthn
// Reset functions after tests.
oldPwd := *client.PasswordFromConsoleFn
t.Cleanup(func() {
*client.PasswordFromConsoleFn = oldPwd
*client.PromptOTP = oldOTP
*client.PromptU2F = oldU2F
*client.PromptWebauthn = oldWAN
client.Prompts.Reset()
})
*client.PasswordFromConsoleFn = func() (string, error) {
return password, nil
}
// The loginMocks setup below is used to avoid data races when reading or
// writing client.Prompt* pointers.
// Tests are supposed to replace loginMocks functions instead.
loginMocks := struct {
promptOTP func(ctx context.Context) (string, error)
promptU2F func(ctx context.Context, facet string, challenges ...u2flib.AuthenticateChallenge) (*u2flib.AuthenticateChallengeResponse, error)
promptWebauthn func(ctx context.Context, origin string, assertion *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error)
}{}
var loginMocksMU sync.RWMutex
*client.PromptOTP = func(ctx context.Context, out io.Writer, in *prompt.ContextReader, question string) (string, error) {
loginMocksMU.RLock()
defer loginMocksMU.RUnlock()
return loginMocks.promptOTP(ctx)
}
*client.PromptU2F = func(ctx context.Context, facet string, challenges ...u2flib.AuthenticateChallenge) (*u2flib.AuthenticateChallengeResponse, error) {
loginMocksMU.RLock()
defer loginMocksMU.RUnlock()
return loginMocks.promptU2F(ctx, facet, challenges...)
}
*client.PromptWebauthn = func(ctx context.Context, origin string, assertion *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error) {
loginMocksMU.RLock()
defer loginMocksMU.RUnlock()
return loginMocks.promptWebauthn(ctx, origin, assertion)
}
promptOTPNoop := func(ctx context.Context) (string, error) {
<-ctx.Done() // wait for timeout
return "", ctx.Err()
@ -131,15 +99,6 @@ func TestTeleportClient_Login_localMFALogin(t *testing.T) {
solveOTP := func(ctx context.Context) (string, error) {
return totp.GenerateCode(otpKey, clock.Now())
}
solveU2F := func(ctx context.Context, facet string, challenges ...u2flib.AuthenticateChallenge) (*u2flib.AuthenticateChallengeResponse, error) {
kh := base64.RawURLEncoding.EncodeToString(device.KeyHandle)
for _, c := range challenges {
if kh == c.KeyHandle {
return device.SignResponse(&c)
}
}
return nil, trace.BadParameter("key handle now found")
}
solveWebauthn := func(ctx context.Context, origin string, assertion *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error) {
car, err := device.SignAssertion(origin, assertion)
if err != nil {
@ -157,47 +116,34 @@ func TestTeleportClient_Login_localMFALogin(t *testing.T) {
name string
secondFactor constants.SecondFactorType
solveOTP func(context.Context) (string, error)
solveU2F func(ctx context.Context, facet string, challenges ...u2flib.AuthenticateChallenge) (*u2flib.AuthenticateChallengeResponse, error)
solveWebauthn func(ctx context.Context, origin string, assertion *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error)
}{
{
name: "OK OTP device login",
secondFactor: constants.SecondFactorOptional,
solveOTP: solveOTP,
solveU2F: func(context.Context, string, ...u2flib.AuthenticateChallenge) (*u2flib.AuthenticateChallengeResponse, error) {
panic("unused")
},
name: "OK OTP device login",
secondFactor: constants.SecondFactorOptional,
solveOTP: solveOTP,
solveWebauthn: promptWebauthnNoop,
},
{
name: "OK Webauthn device login",
secondFactor: constants.SecondFactorOptional,
solveOTP: promptOTPNoop,
solveU2F: func(context.Context, string, ...u2flib.AuthenticateChallenge) (*u2flib.AuthenticateChallengeResponse, error) {
panic("unused")
},
name: "OK Webauthn device login",
secondFactor: constants.SecondFactorOptional,
solveOTP: promptOTPNoop,
solveWebauthn: solveWebauthn,
},
{
name: "OK U2F device login",
secondFactor: constants.SecondFactorU2F,
solveOTP: promptOTPNoop,
solveU2F: solveU2F,
solveWebauthn: func(context.Context, string, *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error) {
panic("unused")
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
loginMocksMU.Lock()
loginMocks.promptOTP = test.solveOTP
loginMocks.promptU2F = test.solveU2F
loginMocks.promptWebauthn = test.solveWebauthn
loginMocksMU.Unlock()
client.Prompts.Swap(
func(ctx context.Context, out io.Writer, in *prompt.ContextReader, question string) (string, error) {
return test.solveOTP(ctx)
},
func(ctx context.Context, origin string, assertion *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error) {
return test.solveWebauthn(ctx, origin, assertion)
},
)
authServer := sa.Auth.GetAuthServer()
pref, err := authServer.GetAuthPreference(ctx)
@ -258,9 +204,8 @@ func newStandaloneTeleport(t *testing.T, clock clockwork.Clock) *standaloneBundl
cfg.Auth.Preference, err = types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "localhost",
Facets: []string{"https://localhost", "localhost"},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
@ -300,26 +245,20 @@ func newStandaloneTeleport(t *testing.T, clock clockwork.Clock) *standaloneBundl
tokenID := token.GetName()
res, err := authServer.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
TokenID: tokenID,
DeviceType: proto.DeviceType_DEVICE_TYPE_U2F,
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
})
require.NoError(t, err)
device, err := mocku2f.Create()
require.NoError(t, err)
registerResp, err := device.RegisterResponse(&u2f.RegisterChallenge{
Version: res.GetU2F().GetVersion(),
Challenge: res.GetU2F().GetChallenge(),
AppID: res.GetU2F().GetAppID(),
})
const origin = "https://localhost"
ccr, err := device.SignCredentialCreation(origin, wanlib.CredentialCreationFromProto(res.GetWebauthn()))
require.NoError(t, err)
_, err = authServer.ChangeUserAuthentication(ctx, &proto.ChangeUserAuthenticationRequest{
TokenID: tokenID,
NewPassword: []byte(password),
NewMFARegisterResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_U2F{
U2F: &proto.U2FRegisterResponse{
RegistrationData: registerResp.RegistrationData,
ClientData: registerResp.ClientData,
},
Response: &proto.MFARegisterResponse_Webauthn{
Webauthn: wanlib.CredentialCreationResponseToProto(ccr),
},
},
})

View file

@ -17,11 +17,5 @@ package client
// PasswordFromConsoleFn exports passwordFromConsoleFn for tests.
var PasswordFromConsoleFn = &passwordFromConsoleFn
// PromptOTP exports promptOTP for tests.
var PromptOTP = &promptOTP
// PromptU2F exports promptU2F for tests.
var PromptU2F = &promptU2F
// PromptWebauthn exports promptWebauthn for tests.
var PromptWebauthn = &promptWebauthn
// Prompts exports prompts for tests.
var Prompts = prompts

View file

@ -19,12 +19,13 @@ package client
import (
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/utils/prompt"
"github.com/gravitational/trace"
@ -32,14 +33,32 @@ import (
wancli "github.com/gravitational/teleport/lib/auth/webauthncli"
)
// promptOTP allows tests to override the OTP prompt function.
var promptOTP = prompt.Input
type (
OTPPrompt func(ctx context.Context, out io.Writer, in *prompt.ContextReader, question string) (string, error)
WebPrompt func(ctx context.Context, origin string, assertion *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error)
)
// promptU2F allows tests to override the U2F prompt function.
var promptU2F = u2f.AuthenticateSignChallenge
// PlatformPrompt groups functions that prompt the user for inputs.
// It's purpose is to allow tests to replace actual user prompts with other
// functions.
type PlatformPrompt struct {
// OTP is the OTP prompt function.
OTP OTPPrompt
// Webauthn is the WebAuth prompt function.
Webauthn WebPrompt
}
// promptWebauthn allows tests to override the Webauthn prompt function.
var promptWebauthn = wancli.Login
func (pp *PlatformPrompt) Reset() *PlatformPrompt {
pp.Swap(prompt.Input, wancli.Login)
return pp
}
func (pp *PlatformPrompt) Swap(otp OTPPrompt, web WebPrompt) {
pp.OTP = otp
pp.Webauthn = web
}
var prompts = (&PlatformPrompt{}).Reset()
// PromptMFAChallenge prompts the user to complete MFA authentication
// challenges.
@ -49,14 +68,12 @@ var promptWebauthn = wancli.Login
// devices, like registered vs new.
func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthenticateChallenge, promptDevicePrefix string, quiet bool) (*proto.MFAAuthenticateResponse, error) {
// Is there a challenge present?
if c.TOTP == nil && len(c.U2F) == 0 && c.WebauthnChallenge == nil {
if c.TOTP == nil && c.WebauthnChallenge == nil {
return &proto.MFAAuthenticateResponse{}, nil
}
// We have three maximum challenges, from which we only pick two: TOTP and
// either Webauthn (preferred) or U2F.
hasTOTP := c.TOTP != nil
hasNonTOTP := len(c.U2F) > 0 || c.WebauthnChallenge != nil
hasWebauthn := c.WebauthnChallenge != nil
// Does the current platform support hardware MFA? Adjust accordingly.
switch {
@ -64,11 +81,11 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
return nil, trace.BadParameter("hardware device MFA not supported by your platform, please register an OTP device")
case !wancli.HasPlatformSupport():
// Do not prompt for hardware devices, it won't work.
hasNonTOTP = false
hasWebauthn = false
}
var numGoroutines int
if hasTOTP && hasNonTOTP {
if hasTOTP && hasWebauthn {
numGoroutines = 2
} else {
numGoroutines = 1
@ -81,19 +98,30 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
}
respC := make(chan response, numGoroutines)
// Use ctx and wg to clean up after ourselves.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var wg sync.WaitGroup
cancelAndWait := func() {
cancel()
wg.Wait()
}
// Fire TOTP goroutine.
if hasTOTP {
wg.Add(1)
go func() {
defer wg.Done()
const kind = "TOTP"
var msg string
if !quiet {
if hasNonTOTP {
if hasWebauthn {
msg = fmt.Sprintf("Tap any %[1]ssecurity key or enter a code from a %[1]sOTP device", promptDevicePrefix, promptDevicePrefix)
} else {
msg = fmt.Sprintf("Enter an OTP code from a %sdevice", promptDevicePrefix)
}
}
code, err := promptOTP(ctx, os.Stderr, prompt.Stdin(), msg)
code, err := prompts.OTP(ctx, os.Stderr, prompt.Stdin(), msg)
if err != nil {
respC <- response{kind: kind, err: err}
return
@ -111,24 +139,19 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
fmt.Fprintf(os.Stderr, "Tap any %ssecurity key\n", promptDevicePrefix)
}
// Fire Webauthn or U2F goroutine.
origin := proxyAddr
if !strings.HasPrefix(origin, "https://") {
origin = "https://" + origin
}
switch {
case c.WebauthnChallenge != nil:
// Fire Webauthn goroutine.
if hasWebauthn {
origin := proxyAddr
if !strings.HasPrefix(origin, "https://") {
origin = "https://" + origin
}
wg.Add(1)
go func() {
log.Debugf("WebAuthn: prompting U2F devices with origin %q", origin)
resp, err := promptWebauthn(ctx, origin, wanlib.CredentialAssertionFromProto(c.WebauthnChallenge))
defer wg.Done()
log.Debugf("WebAuthn: prompting devices with origin %q", origin)
resp, err := prompts.Webauthn(ctx, origin, wanlib.CredentialAssertionFromProto(c.WebauthnChallenge))
respC <- response{kind: "WEBAUTHN", resp: resp, err: err}
}()
case len(c.U2F) > 0:
go func() {
log.Debugf("prompting U2F devices with facet %q", origin)
resp, err := promptU2FChallenges(ctx, proxyAddr, c.U2F)
respC <- response{kind: "U2F", resp: resp, err: err}
}()
}
for i := 0; i < numGoroutines; i++ {
@ -143,63 +166,27 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
fmt.Fprintln(os.Stderr) // Print a new line after the prompt
}
// Exiting cancels the context via defer, which makes the remaining
// goroutines stop.
// Cleanup in-flight goroutines.
cancelAndWait()
return resp.resp, nil
case <-ctx.Done():
cancelAndWait()
return nil, trace.Wrap(ctx.Err())
}
}
cancelAndWait()
return nil, trace.BadParameter(
"failed to authenticate using all MFA devices, rerun the command with '-d' to see error details for each device")
}
func promptU2FChallenges(ctx context.Context, origin string, challenges []*proto.U2FChallenge) (*proto.MFAAuthenticateResponse, error) {
u2fChallenges := make([]u2f.AuthenticateChallenge, 0, len(challenges))
for _, chal := range challenges {
u2fChallenges = append(u2fChallenges, u2f.AuthenticateChallenge{
Challenge: chal.Challenge,
KeyHandle: chal.KeyHandle,
AppID: chal.AppID,
})
}
resp, err := promptU2F(ctx, origin, u2fChallenges...)
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_U2F{
U2F: &proto.U2FResponse{
KeyHandle: resp.KeyHandle,
ClientData: resp.ClientData,
Signature: resp.SignatureData,
},
}}, nil
}
// MakeAuthenticateChallenge converts proto to JSON format.
func MakeAuthenticateChallenge(protoChal *proto.MFAAuthenticateChallenge) *auth.MFAAuthenticateChallenge {
chal := &auth.MFAAuthenticateChallenge{
TOTPChallenge: protoChal.GetTOTP() != nil,
}
for _, u2fChal := range protoChal.GetU2F() {
ch := u2f.AuthenticateChallenge{
Version: u2fChal.Version,
Challenge: u2fChal.Challenge,
KeyHandle: u2fChal.KeyHandle,
AppID: u2fChal.AppID,
}
if chal.AuthenticateChallenge == nil {
chal.AuthenticateChallenge = &ch
}
chal.U2FChallenges = append(chal.U2FChallenges, ch)
}
if protoChal.GetWebauthnChallenge() != nil {
chal.WebauthnChallenge = wanlib.CredentialAssertionFromProto(protoChal.WebauthnChallenge)
}
return chal
}
@ -209,8 +196,6 @@ type TOTPRegisterChallenge struct {
// MFARegisterChallenge is an MFA register challenge sent on new MFA register.
type MFARegisterChallenge struct {
// U2F contains U2F register challenge.
U2F *u2f.RegisterChallenge `json:"u2f"`
// Webauthn contains webauthn challenge.
Webauthn *wanlib.CredentialCreation `json:"webauthn"`
// TOTP contains TOTP challenge.
@ -226,21 +211,10 @@ func MakeRegisterChallenge(protoChal *proto.MFARegisterChallenge) *MFARegisterCh
QRCode: protoChal.GetTOTP().GetQRCode(),
},
}
case *proto.MFARegisterChallenge_U2F:
return &MFARegisterChallenge{
U2F: &u2f.RegisterChallenge{
Version: protoChal.GetU2F().GetVersion(),
Challenge: protoChal.GetU2F().GetChallenge(),
AppID: protoChal.GetU2F().GetAppID(),
},
}
case *proto.MFARegisterChallenge_Webauthn:
return &MFARegisterChallenge{
Webauthn: wanlib.CredentialCreationFromProto(protoChal.GetWebauthn()),
}
}
return nil
}

View file

@ -82,7 +82,7 @@ func runPresenceTask(ctx context.Context, term io.Writer, auth auth.ClientI, tc
func solveMFA(ctx context.Context, term io.Writer, tc *TeleportClient, challenge *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) {
fmt.Fprint(term, "\r\nTeleport > Please tap your MFA key\r\n")
// This is here to enforce the usage of an U2F or WebAuthn device.
// This is here to enforce the usage of a MFA device.
// We don't support TOTP for live presence.
challenge.TOTP = nil

View file

@ -33,7 +33,6 @@ import (
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
@ -125,8 +124,6 @@ type AuthenticateSSHUserRequest struct {
// Password for the user, to authenticate in case no MFA check was
// performed.
Password string `json:"password"`
// U2FSignResponse is the signature from the U2F device
U2FSignResponse *u2f.AuthenticateChallengeResponse `json:"u2f_sign_response"`
// WebauthnChallengeResponse is a signed WebAuthn credential assertion.
WebauthnChallengeResponse *wanlib.CredentialAssertionResponse `json:"webauthn_challenge_response"`
// TOTPCode is a code from the TOTP device.
@ -149,8 +146,6 @@ type AuthenticateSSHUserRequest struct {
type AuthenticateWebUserRequest struct {
// User is a teleport username.
User string `json:"user"`
// U2FSignResponse is the signature from the U2F device.
U2FSignResponse *u2f.AuthenticateChallengeResponse `json:"u2f_sign_response,omitempty"`
// WebauthnAssertionResponse is a signed WebAuthn credential assertion.
WebauthnAssertionResponse *wanlib.CredentialAssertionResponse `json:"webauthnAssertionResponse,omitempty"`
}
@ -353,10 +348,10 @@ func SSHAgentLogin(ctx context.Context, login SSHLoginDirect) (*auth.SSHLoginRes
return out, nil
}
// SSHAgentMFALogin requests a MFA challenge (U2F or OTP) via the proxy. If the
// credentials are valid, the proxy wiil return a challenge. We then prompt the
// user to provide 2nd factor and pass the response to the proxy. If the
// authentication succeeds, we will get a temporary certificate back.
// SSHAgentMFALogin requests a MFA challenge via the proxy.
// If the credentials are valid, the proxy will return a challenge. We then
// prompt the user to provide 2nd factor and pass the response to the proxy.
// If the authentication succeeds, we will get a temporary certificate back.
func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*auth.SSHLoginResponse, error) {
clt, _, err := initClient(login.ProxyAddr, login.Insecure, login.Pool)
if err != nil {
@ -367,13 +362,7 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*auth.SSHLoginRes
User: login.User,
Pass: login.Password,
}
// TODO(codingllama): Decide endpoints based on the Ping server version, once
// we have a known version for Webauthn.
challengeJSON, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "mfa", "login", "begin"), beginReq)
// DELETE IN 9.x, fallback not necessary after U2F is removed (codingllama)
if trace.IsNotImplemented(err) {
challengeJSON, err = clt.PostJSON(ctx, clt.Endpoint("webapi", "u2f", "signrequest"), beginReq)
}
if err != nil {
return nil, trace.Wrap(err)
}
@ -382,25 +371,12 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*auth.SSHLoginRes
if err := json.Unmarshal(challengeJSON.Bytes(), challenge); err != nil {
return nil, trace.Wrap(err)
}
// DELETE IN 9.x, fallback not necessary after U2F is removed (codingllama)
if len(challenge.U2FChallenges) == 0 && challenge.AuthenticateChallenge != nil {
// Challenge sent by a pre-6.0 auth server, fall back to the old
// single-device format.
challenge.U2FChallenges = []u2f.AuthenticateChallenge{*challenge.AuthenticateChallenge}
}
// Convert to auth gRPC proto challenge.
challengePB := &proto.MFAAuthenticateChallenge{}
if challenge.TOTPChallenge {
challengePB.TOTP = &proto.TOTPChallenge{}
}
for _, c := range challenge.U2FChallenges {
challengePB.U2F = append(challengePB.U2F, &proto.U2FChallenge{
KeyHandle: c.KeyHandle,
Challenge: c.Challenge,
AppID: c.AppID,
})
}
if challenge.WebauthnChallenge != nil {
challengePB.WebauthnChallenge = wanlib.CredentialAssertionToProto(challenge.WebauthnChallenge)
}
@ -423,12 +399,6 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*auth.SSHLoginRes
switch r := respPB.Response.(type) {
case *proto.MFAAuthenticateResponse_TOTP:
challengeResp.TOTPCode = r.TOTP.Code
case *proto.MFAAuthenticateResponse_U2F:
challengeResp.U2FSignResponse = &u2f.AuthenticateChallengeResponse{
KeyHandle: r.U2F.KeyHandle,
SignatureData: r.U2F.Signature,
ClientData: r.U2F.ClientData,
}
case *proto.MFAAuthenticateResponse_Webauthn:
challengeResp.WebauthnChallengeResponse = wanlib.CredentialAssertionResponseFromProto(r.Webauthn)
default:
@ -436,10 +406,6 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*auth.SSHLoginRes
}
loginRespJSON, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "mfa", "login", "finish"), challengeResp)
// DELETE IN 9.x, fallback not necessary after U2F is removed (codingllama)
if trace.IsNotImplemented(err) {
loginRespJSON, err = clt.PostJSON(ctx, clt.Endpoint("webapi", "u2f", "certs"), challengeResp)
}
if err != nil {
return nil, trace.Wrap(err)
}

View file

@ -48,7 +48,7 @@ import (
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
@ -745,7 +745,10 @@ type Webauthn struct {
RPID string `yaml:"rp_id,omitempty"`
AttestationAllowedCAs []string `yaml:"attestation_allowed_cas,omitempty"`
AttestationDeniedCAs []string `yaml:"attestation_denied_cas,omitempty"`
Disabled bool `yaml:"disabled,omitempty"`
// Disabled has no effect, it is kept solely to not break existing
// configurations.
// DELETE IN 11.0, time to sunset U2F (codingllama).
Disabled bool `yaml:"disabled,omitempty"`
}
func (w *Webauthn) Parse() (*types.Webauthn, error) {
@ -757,13 +760,18 @@ func (w *Webauthn) Parse() (*types.Webauthn, error) {
if err != nil {
return nil, trace.BadParameter("webauthn.attestation_denied_cas: %v", err)
}
if w.Disabled {
log.Warnf(`` +
`The "webauthn.disabled" setting is marked for removal and currently has no effect. ` +
`Please update your configuration to use WebAuthn. ` +
`Refer to https://goteleport.com/docs/access-controls/guides/webauthn/`)
}
return &types.Webauthn{
// Allow any RPID to go through, we rely on
// types.Webauthn.CheckAndSetDefaults to correct it.
RPID: w.RPID,
AttestationAllowedCAs: allowedCAs,
AttestationDeniedCAs: deniedCAs,
Disabled: w.Disabled,
}, nil
}

View file

@ -278,7 +278,7 @@ func TestAuthenticationSection(t *testing.T) {
},
},
"webauthn": cfgMap{
"disabled": true,
"disabled": true, // Kept for backwards compatibility, has no effect.
},
}
},

View file

@ -704,9 +704,6 @@ const (
// WebsocketResize is receiving a resize request.
WebsocketResize = "w"
// WebsocketU2FChallenge is sending a U2F challenge.
WebsocketU2FChallenge = "u"
// WebsocketWebauthnChallenge is sending a webauthn challenge.
WebsocketWebauthnChallenge = "n"
)

View file

@ -34,7 +34,6 @@ import (
authproto "github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/web/mfajson"
@ -457,7 +456,7 @@ func decodeClipboardData(in peekReader, maxLen uint32) (ClipboardData, error) {
const maxMFADataLength = 1024 * 1024
type MFA struct {
// Type should be one of defaults.WebsocketU2FChallenge or defaults.WebsocketWebauthnChallenge
// Type should be defaults.WebsocketWebauthnChallenge
Type byte
// MFAAuthenticateChallenge is the challenge we send to the client.
// Used for messages from Teleport to the user's browser.
@ -481,17 +480,6 @@ func (m MFA) Encode() ([]byte, error) {
}
} else if m.MFAAuthenticateResponse != nil {
switch t := m.MFAAuthenticateResponse.Response.(type) {
case *authproto.MFAAuthenticateResponse_U2F:
msg := m.MFAAuthenticateResponse.GetU2F()
resp := u2f.AuthenticateChallengeResponse{
KeyHandle: msg.KeyHandle,
SignatureData: msg.Signature,
ClientData: msg.ClientData,
}
buff, err = json.Marshal(resp)
if err != nil {
return nil, trace.Wrap(err)
}
case *authproto.MFAAuthenticateResponse_Webauthn:
buff, err = json.Marshal(wanlib.CredentialAssertionResponseFromProto(m.MFAAuthenticateResponse.GetWebauthn()))
if err != nil {
@ -527,10 +515,10 @@ func DecodeMFA(in peekReader) (*MFA, error) {
}
s := string(mt)
switch s {
case defaults.WebsocketWebauthnChallenge, defaults.WebsocketU2FChallenge:
case defaults.WebsocketWebauthnChallenge:
default:
return nil, trace.BadParameter("got mfa type %v, expected %v (WebAuthn) or %v (U2F)",
mt, defaults.WebsocketWebauthnChallenge, defaults.WebsocketU2FChallenge)
return nil, trace.BadParameter(
"got mfa type %v, expected %v (WebAuthn)", mt, defaults.WebsocketWebauthnChallenge)
}
var length uint32
@ -575,10 +563,10 @@ func DecodeMFAChallenge(in peekReader) (*MFA, error) {
}
s := string(mt)
switch s {
case defaults.WebsocketWebauthnChallenge, defaults.WebsocketU2FChallenge:
case defaults.WebsocketWebauthnChallenge:
default:
return nil, trace.BadParameter("got mfa type %v, expected %v (WebAuthn) or %v (U2F)",
mt, defaults.WebsocketWebauthnChallenge, defaults.WebsocketU2FChallenge)
return nil, trace.BadParameter(
"got mfa type %v, expected %v (WebAuthn)", mt, defaults.WebsocketWebauthnChallenge)
}
var length uint32

View file

@ -27,14 +27,16 @@ import (
"path/filepath"
"testing"
"github.com/duo-labs/webauthn/protocol"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
authproto "github.com/gravitational/teleport/api/client/proto"
wantypes "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/defaults"
)
@ -135,15 +137,26 @@ func loadBitmaps(b *testing.B) []PNGFrame {
func TestMFA(t *testing.T) {
var buff bytes.Buffer
c := NewConn(&buff)
mfaWant := &MFA{
Type: defaults.WebsocketU2FChallenge[0],
Type: defaults.WebsocketWebauthnChallenge[0],
MFAAuthenticateChallenge: &auth.MFAAuthenticateChallenge{
U2FChallenges: []u2f.AuthenticateChallenge{
{
Version: "version",
Challenge: "challenge",
KeyHandle: "key_handle",
AppID: "app_id",
WebauthnChallenge: &wanlib.CredentialAssertion{
Response: protocol.PublicKeyCredentialRequestOptions{
Challenge: []byte("challenge"),
Timeout: 10,
RelyingPartyID: "teleport",
AllowedCredentials: []protocol.CredentialDescriptor{
{
Type: "public-key",
CredentialID: []byte("credential id"),
Transport: []protocol.AuthenticatorTransport{protocol.USB},
},
},
UserVerification: "discouraged",
Extensions: protocol.AuthenticationExtensions{
"ext1": "value1",
},
},
},
},
@ -155,13 +168,21 @@ func TestMFA(t *testing.T) {
require.Equal(t, mfaWant, mfaGot)
respWant := &MFA{
Type: defaults.WebsocketU2FChallenge[0],
Type: defaults.WebsocketWebauthnChallenge[0],
MFAAuthenticateResponse: &authproto.MFAAuthenticateResponse{
Response: &authproto.MFAAuthenticateResponse_U2F{
U2F: &authproto.U2FResponse{
KeyHandle: "key_handler",
ClientData: "client_data",
Signature: "signature",
Response: &authproto.MFAAuthenticateResponse_Webauthn{
Webauthn: &wantypes.CredentialAssertionResponse{
Type: "public-key",
RawId: []byte("credential id"),
Response: &wantypes.AuthenticatorAssertionResponse{
ClientDataJson: []byte("client data json"),
AuthenticatorData: []byte("authenticator data"),
Signature: []byte("signature"),
UserHandle: []byte("user handle"),
},
Extensions: &wantypes.AuthenticationExtensionsClientOutputs{
AppId: true,
},
},
},
},

View file

@ -44,7 +44,6 @@ import (
apievents "github.com/gravitational/teleport/api/types/events"
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
@ -361,14 +360,6 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
h.GET("/webapi/github/callback", h.WithMetaRedirect(h.githubCallback))
h.POST("/webapi/github/login/console", httplib.MakeHandler(h.githubLoginConsole))
// U2F related APIs
// DELETE IN 9.x, superseded by /mfa/ endpoints (codingllama)
h.GET("/webapi/u2f/signuptokens/:token", httplib.MakeHandler(h.u2fRegisterRequest)) // replaced with /webapi/mfa/token/:token/registerchallenge
h.POST("/webapi/u2f/password/changerequest", h.WithAuth(h.createAuthenticateChallengeWithPassword)) // replaced with /webapi/mfa/authenticatechallenge/password
h.POST("/webapi/u2f/signrequest", httplib.MakeHandler(h.mfaLoginBegin)) // replaced with /webapi/mfa/login/begin
h.POST("/webapi/u2f/sessions", httplib.MakeHandler(h.mfaLoginFinishSession)) // replaced with /webapi/mfa/login/finishsession
h.POST("/webapi/u2f/certs", httplib.MakeHandler(h.mfaLoginFinish)) // replaced with /webapi/mfa/login/finish
// MFA public endpoints.
h.POST("/webapi/mfa/login/begin", httplib.MakeHandler(h.mfaLoginBegin))
h.POST("/webapi/mfa/login/finish", httplib.MakeHandler(h.mfaLoginFinish))
@ -1518,8 +1509,6 @@ type changeUserAuthenticationRequest struct {
TokenID string `json:"token"`
// Password is user password string converted to bytes.
Password []byte `json:"password"`
// U2FRegisterResponse is U2F registration challenge response.
U2FRegisterResponse *u2f.RegisterChallengeResponse `json:"u2f_register_response,omitempty"`
// WebauthnCreationResponse is the signed credential creation response.
WebauthnCreationResponse *wanlib.CredentialCreationResponse `json:"webauthnCreationResponse"`
}
@ -1541,13 +1530,6 @@ func (h *Handler) changeUserAuthentication(w http.ResponseWriter, r *http.Reques
Webauthn: wanlib.CredentialCreationResponseToProto(req.WebauthnCreationResponse),
},
}
case req.U2FRegisterResponse != nil:
protoReq.NewMFARegisterResponse = &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_U2F{
U2F: &proto.U2FRegisterResponse{
RegistrationData: req.U2FRegisterResponse.RegistrationData,
ClientData: req.U2FRegisterResponse.ClientData,
},
}}
case req.SecondFactorToken != "":
protoReq.NewMFARegisterResponse = &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_TOTP{
TOTP: &proto.TOTPRegisterResponse{Code: req.SecondFactorToken},
@ -1650,38 +1632,16 @@ func (h *Handler) getResetPasswordToken(ctx context.Context, tokenID string) (in
}, nil
}
// u2fRegisterRequest is called to get a U2F challenge for registering a U2F key
//
// GET /webapi/u2f/signuptokens/:token
//
// Response:
//
// {"version":"U2F_V2","challenge":"randombase64string","appId":"https://mycorp.com:3080"}
//
func (h *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
res, err := h.auth.proxyClient.CreateRegisterChallenge(r.Context(), &proto.CreateRegisterChallengeRequest{
TokenID: p.ByName("token"),
DeviceType: proto.DeviceType_DEVICE_TYPE_U2F,
})
if err != nil {
return nil, trace.Wrap(err)
}
chal := client.MakeRegisterChallenge(res)
return chal.U2F, nil
}
// mfaLoginBegin is the first step in the MFA authentication ceremony, which
// may be completed either via mfaLoginFinish (SSH) or mfaLoginFinishSession (Web).
//
// POST /webapi/u2f/signrequest (deprecated)
// POST /webapi/mfa/login/begin
//
// {"user": "alex", "pass": "abc123"}
//
// Successful response:
//
// {"webauthn_challenge": {...}, "u2f_challenges": [...], "totp_challenge": true}
// {"webauthn_challenge": {...}, "totp_challenge": true}
func (h *Handler) mfaLoginBegin(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
var req *client.MFAChallengeRequest
if err := httplib.ReadJSON(r, &req); err != nil {
@ -1703,7 +1663,6 @@ func (h *Handler) mfaLoginBegin(w http.ResponseWriter, r *http.Request, p httpro
// mfaLoginFinish completes the MFA login ceremony, returning a new SSH
// certificate if successful.
//
// POST /v1/webapi/u2f/certs (deprecated)
// POST /v1/mfa/login/finish
//
// { "user": "bob", "password": "pass", "pub_key": "key to sign", "ttl": 1000000000 } # password-only
@ -1728,7 +1687,6 @@ func (h *Handler) mfaLoginFinish(w http.ResponseWriter, r *http.Request, p httpr
// mfaLoginFinishSession completes the MFA login ceremony, returning a new web
// session if successful.
//
// POST /webapi/u2f/session (deprecated)
// POST /webapi/mfa/login/finishsession
//
// {"user": "alex", "webauthn_challenge_response": {...}}

View file

@ -20,7 +20,6 @@ import (
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"testing"
"time"
@ -29,133 +28,24 @@ import (
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/client"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
func TestU2FLogin(t *testing.T) {
for _, sf := range []constants.SecondFactorType{
constants.SecondFactorU2F,
constants.SecondFactorOptional,
constants.SecondFactorOn,
} {
sf := sf
t.Run(fmt.Sprintf("second_factor_%s", sf), func(t *testing.T) {
t.Parallel()
testU2FLogin(t, sf)
})
}
}
func testU2FLogin(t *testing.T, sf constants.SecondFactorType) {
env := newWebPack(t, 1)
clusterMFA := configureClusterForMFA(t, env, &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: sf,
U2F: &types.U2F{
AppID: "https://" + env.server.TLS.ClusterName(),
Facets: []string{"https://" + env.server.TLS.ClusterName()},
},
})
user := clusterMFA.User
password := clusterMFA.Password
device := clusterMFA.U2FDev.Key
// normal login
clt, err := client.NewWebClient(env.proxies[0].webURL.String(), roundtrip.HTTPClient(client.NewInsecureWebClient()))
require.NoError(t, err)
re, err := clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.MFAChallengeRequest{
User: user,
Pass: password,
})
require.NoError(t, err)
var u2fSignReq u2f.AuthenticateChallenge
require.NoError(t, json.Unmarshal(re.Bytes(), &u2fSignReq))
u2fSignResp, err := device.SignResponse(&u2fSignReq)
require.NoError(t, err)
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), &client.AuthenticateWebUserRequest{
User: user,
U2FSignResponse: u2fSignResp,
})
require.NoError(t, err)
// bad login: corrupted sign responses, should fail
re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.MFAChallengeRequest{
User: user,
Pass: password,
})
require.NoError(t, err)
require.NoError(t, json.Unmarshal(re.Bytes(), &u2fSignReq))
u2fSignResp, err = device.SignResponse(&u2fSignReq)
require.NoError(t, err)
// corrupted KeyHandle
u2fSignRespCopy := u2fSignResp
u2fSignRespCopy.KeyHandle = u2fSignRespCopy.KeyHandle + u2fSignRespCopy.KeyHandle
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), &client.AuthenticateWebUserRequest{
User: user,
U2FSignResponse: u2fSignRespCopy,
})
require.Error(t, err)
// corrupted SignatureData
u2fSignRespCopy = u2fSignResp
u2fSignRespCopy.SignatureData = u2fSignRespCopy.SignatureData[:10] + u2fSignRespCopy.SignatureData[20:]
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), &client.AuthenticateWebUserRequest{
User: user,
U2FSignResponse: u2fSignRespCopy,
})
require.Error(t, err)
// corrupted ClientData
u2fSignRespCopy = u2fSignResp
u2fSignRespCopy.ClientData = u2fSignRespCopy.ClientData[:10] + u2fSignRespCopy.ClientData[20:]
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), &client.AuthenticateWebUserRequest{
User: user,
U2FSignResponse: u2fSignRespCopy,
})
require.Error(t, err)
// bad login: counter not increasing, should fail
device.SetCounter(0)
re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.MFAChallengeRequest{
User: user,
Pass: password,
})
require.NoError(t, err)
require.NoError(t, json.Unmarshal(re.Bytes(), &u2fSignReq))
u2fSignResp, err = device.SignResponse(&u2fSignReq)
require.NoError(t, err)
_, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), &client.AuthenticateWebUserRequest{
User: user,
U2FSignResponse: u2fSignResp,
})
require.Error(t, err)
}
func TestWebauthnLogin_ssh_u2fDevice(t *testing.T) {
func TestWebauthnLogin_ssh(t *testing.T) {
env := newWebPack(t, 1)
clusterMFA := configureClusterForMFA(t, env, &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOn,
U2F: &types.U2F{
AppID: "https://" + env.server.TLS.ClusterName(),
Facets: []string{"https://" + env.server.TLS.ClusterName()},
Webauthn: &types.Webauthn{
RPID: env.server.TLS.ClusterName(),
},
// Use default Webauthn configuration.
})
user := clusterMFA.User
password := clusterMFA.Password
device := clusterMFA.U2FDev.Key
device := clusterMFA.WebDev.Key
clt, err := client.NewWebClient(env.proxies[0].webURL.String(), roundtrip.HTTPClient(client.NewInsecureWebClient()))
require.NoError(t, err)
@ -199,20 +89,19 @@ func TestWebauthnLogin_ssh_u2fDevice(t *testing.T) {
require.NotEmpty(t, loginResp.HostSigners)
}
func TestWebauthnLogin_web_u2fDevice(t *testing.T) {
func TestWebauthnLogin_web(t *testing.T) {
env := newWebPack(t, 1)
clusterMFA := configureClusterForMFA(t, env, &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOn,
U2F: &types.U2F{
AppID: "https://" + env.server.TLS.ClusterName(),
Facets: []string{"https://" + env.server.TLS.ClusterName()},
Webauthn: &types.Webauthn{
RPID: env.server.TLS.ClusterName(),
},
// Use default Webauthn configuration.
})
user := clusterMFA.User
password := clusterMFA.Password
device := clusterMFA.U2FDev.Key
device := clusterMFA.WebDev.Key
clt, err := client.NewWebClient(env.proxies[0].webURL.String(), roundtrip.HTTPClient(client.NewInsecureWebClient()))
require.NoError(t, err)
@ -249,7 +138,7 @@ func TestWebauthnLogin_web_u2fDevice(t *testing.T) {
type configureMFAResp struct {
User, Password string
U2FDev *auth.TestDevice
WebDev *auth.TestDevice
}
func configureClusterForMFA(t *testing.T, env *webPack, spec *types.AuthPreferenceSpecV2) *configureMFAResp {
@ -271,12 +160,12 @@ func configureClusterForMFA(t *testing.T, env *webPack, spec *types.AuthPreferen
// Register device.
clt, err := env.server.NewClient(auth.TestUser(user))
require.NoError(t, err)
u2fDev, err := auth.RegisterTestDevice(ctx, clt, "u2f", proto.DeviceType_DEVICE_TYPE_U2F, nil /* authenticator */)
webDev, err := auth.RegisterTestDevice(ctx, clt, "webauthn", proto.DeviceType_DEVICE_TYPE_WEBAUTHN, nil /* authenticator */)
require.NoError(t, err)
return &configureMFAResp{
User: user,
Password: password,
U2FDev: u2fDev,
WebDev: webDev,
}
}

View file

@ -57,7 +57,6 @@ import (
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/mocku2f"
"github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/bpf"
@ -1092,49 +1091,6 @@ func TestTerminalRequireSessionMfa(t *testing.T) {
return webauthnResBytes
},
},
{
name: "with u2f",
getAuthPreference: func() types.AuthPreference {
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorU2F,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
},
RequireSessionMFA: true,
})
require.NoError(t, err)
return ap
},
registerDevice: func() *auth.TestDevice {
u2fDev, err := auth.RegisterTestDevice(ctx, clt, "u2f", apiProto.DeviceType_DEVICE_TYPE_U2F, nil /* authenticator */)
require.NoError(t, err)
return u2fDev
},
getChallengeResponseBytes: func(chals *auth.MFAAuthenticateChallenge, dev *auth.TestDevice) []byte {
res, err := dev.SolveAuthn(&apiProto.MFAAuthenticateChallenge{
U2F: []*apiProto.U2FChallenge{{
KeyHandle: chals.U2FChallenges[0].KeyHandle,
Challenge: chals.U2FChallenges[0].Challenge,
AppID: chals.U2FChallenges[0].AppID,
Version: chals.U2FChallenges[0].Version,
}},
})
require.NoError(t, err)
u2fResBytes, err := json.Marshal(&u2f.AuthenticateChallengeResponse{
KeyHandle: res.GetU2F().KeyHandle,
SignatureData: res.GetU2F().Signature,
ClientData: res.GetU2F().ClientData,
})
require.NoError(t, err)
return u2fResBytes
},
},
}
for _, tc := range cases {
@ -1255,24 +1211,6 @@ func TestDesktopAccessMFARequiresMfa(t *testing.T) {
mfaHandler func(t *testing.T, ws *websocket.Conn, dev *auth.TestDevice)
registerDevice func(t *testing.T, ctx context.Context, clt *auth.Client) *auth.TestDevice
}{
{
name: "u2f",
authPref: types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorU2F,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
},
RequireSessionMFA: true,
},
mfaHandler: handleMFAU2FCChallenge,
registerDevice: func(t *testing.T, ctx context.Context, clt *auth.Client) *auth.TestDevice {
dev, err := auth.RegisterTestDevice(ctx, clt, "u2f", apiProto.DeviceType_DEVICE_TYPE_U2F, nil /* authenticator */)
require.NoError(t, err)
return dev
},
},
{
name: "webauthn",
authPref: types.AuthPreferenceSpecV2{
@ -1340,6 +1278,7 @@ func TestDesktopAccessMFARequiresMfa(t *testing.T) {
})
}
}
func handleMFAWebauthnChallenge(t *testing.T, ws *websocket.Conn, dev *auth.TestDevice) {
mfaChallange, err := tdp.DecodeMFAChallenge(bufio.NewReader(&WebsocketIO{Conn: ws}))
require.NoError(t, err)
@ -1358,33 +1297,6 @@ func handleMFAWebauthnChallenge(t *testing.T, ws *websocket.Conn, dev *auth.Test
require.NoError(t, err)
}
func handleMFAU2FCChallenge(t *testing.T, ws *websocket.Conn, dev *auth.TestDevice) {
mfaChallange, err := tdp.DecodeMFAChallenge(bufio.NewReader(&WebsocketIO{Conn: ws}))
require.NoError(t, err)
res, err := dev.SolveAuthn(&apiProto.MFAAuthenticateChallenge{
U2F: []*apiProto.U2FChallenge{{
KeyHandle: mfaChallange.U2FChallenges[0].KeyHandle,
Challenge: mfaChallange.U2FChallenges[0].Challenge,
AppID: mfaChallange.U2FChallenges[0].AppID,
Version: mfaChallange.U2FChallenges[0].Version,
}},
})
require.NoError(t, err)
err = tdp.NewConn(&WebsocketIO{Conn: ws}).OutputMessage(tdp.MFA{
Type: defaults.WebsocketU2FChallenge[0],
MFAAuthenticateResponse: &authproto.MFAAuthenticateResponse{
Response: &authproto.MFAAuthenticateResponse_U2F{
U2F: &authproto.U2FResponse{
KeyHandle: res.GetU2F().KeyHandle,
ClientData: res.GetU2F().ClientData,
Signature: res.GetU2F().Signature,
},
},
},
})
require.NoError(t, err)
}
func (s *WebSuite) TestWebAgentForward(c *C) {
ws, err := s.makeTerminal(s.authPack(c, "foo"))
c.Assert(err, IsNil)
@ -1734,64 +1646,6 @@ func (s *WebSuite) TestChangePasswordAndAddTOTPDeviceWithToken(c *C) {
c.Assert(response.Created, IsNil)
}
func (s *WebSuite) TestChangePasswordAndAddU2FDeviceWithToken(c *C) {
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorU2F,
U2F: &types.U2F{
AppID: "https://" + s.server.ClusterName(),
Facets: []string{"https://" + s.server.ClusterName()},
},
})
c.Assert(err, IsNil)
err = s.server.Auth().SetAuthPreference(s.ctx, ap)
c.Assert(err, IsNil)
s.createUser(c, "user2", "root", "password", "")
// create reset password token
token, err := s.server.Auth().CreateResetPasswordToken(context.TODO(), auth.CreateUserTokenRequest{
Name: "user2",
})
c.Assert(err, IsNil)
clt := s.client()
re, err := clt.Get(context.Background(), clt.Endpoint("webapi", "u2f", "signuptokens", token.GetName()), url.Values{})
c.Assert(err, IsNil)
var u2fRegReq u2f.RegisterChallenge
c.Assert(json.Unmarshal(re.Bytes(), &u2fRegReq), IsNil)
u2fRegResp, err := s.mockU2F.RegisterResponse(&u2fRegReq)
c.Assert(err, IsNil)
data, err := json.Marshal(auth.ChangePasswordWithTokenRequest{
TokenID: token.GetName(),
Password: []byte("qweQWE"),
U2FRegisterResponse: u2fRegResp,
})
c.Assert(err, IsNil)
req, err := http.NewRequest("PUT", clt.Endpoint("webapi", "users", "password", "token"), bytes.NewBuffer(data))
c.Assert(err, IsNil)
csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
addCSRFCookieToReq(req, csrfToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set(csrf.HeaderName, csrfToken)
re, err = clt.Client.RoundTrip(func() (*http.Response, error) {
return clt.Client.HTTPClient().Do(req)
})
c.Assert(err, IsNil)
// Test that no recovery codes are returned b/c cloud is not turned on.
var response ui.RecoveryCodes
c.Assert(json.Unmarshal(re.Bytes(), &response), IsNil)
c.Assert(response.Codes, IsNil)
c.Assert(response.Created, IsNil)
}
// TestEmptyMotD ensures that responses returned by both /webapi/ping and
// /webapi/motd work when no MotD is set
func (s *WebSuite) TestEmptyMotD(c *C) {
@ -2326,9 +2180,8 @@ func TestAddMFADevice(t *testing.T) {
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "https://localhost",
Facets: []string{"https://localhost"},
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
@ -2352,7 +2205,6 @@ func TestAddMFADevice(t *testing.T) {
name string
deviceName string
getTOTPCode func() string
getU2FResp func() *u2f.RegisterChallengeResponse
getWebauthnResp func() *wanlib.CredentialCreationResponse
}{
{
@ -2372,26 +2224,6 @@ func TestAddMFADevice(t *testing.T) {
return regRes.GetTOTP().Code
},
},
{
name: "new U2F device",
deviceName: "new-u2f",
getU2FResp: func() *u2f.RegisterChallengeResponse {
// Get u2f register challenge.
res, err := env.server.Auth().CreateRegisterChallenge(ctx, &apiProto.CreateRegisterChallengeRequest{
TokenID: privilegeToken,
DeviceType: apiProto.DeviceType_DEVICE_TYPE_U2F,
})
require.NoError(t, err)
_, regRes, err := auth.NewTestDeviceFromChallenge(res)
require.NoError(t, err)
return &u2f.RegisterChallengeResponse{
RegistrationData: regRes.GetU2F().RegistrationData,
ClientData: regRes.GetU2F().ClientData,
}
},
},
{
name: "new Webauthn device",
deviceName: "new-webauthn",
@ -2416,15 +2248,11 @@ func TestAddMFADevice(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var totpCode string
var u2fRegResp *u2f.RegisterChallengeResponse
var webauthnRegResp *wanlib.CredentialCreationResponse
switch {
case tc.getU2FResp != nil:
u2fRegResp = tc.getU2FResp()
case tc.getWebauthnResp != nil:
if tc.getWebauthnResp != nil {
webauthnRegResp = tc.getWebauthnResp()
default:
} else {
totpCode = tc.getTOTPCode()
}
@ -2434,7 +2262,6 @@ func TestAddMFADevice(t *testing.T) {
PrivilegeTokenID: privilegeToken,
DeviceName: tc.deviceName,
SecondFactorToken: totpCode,
U2FRegisterResponse: u2fRegResp,
WebauthnRegisterResponse: webauthnRegResp,
})
require.NoError(t, err)
@ -2472,9 +2299,8 @@ func TestGetAndDeleteMFADevices_WithRecoveryApprovedToken(t *testing.T) {
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "https://" + env.server.ClusterName(),
Facets: []string{"https://" + env.server.ClusterName()},
Webauthn: &types.Webauthn{
RPID: env.server.ClusterName(),
},
})
require.NoError(t, err)
@ -2585,7 +2411,6 @@ func TestCreateAuthenticateChallenge(t *testing.T) {
err = json.Unmarshal(res.Bytes(), &chal)
require.NoError(t, err)
require.True(t, chal.TOTPChallenge)
require.Empty(t, chal.U2FChallenges)
require.Empty(t, chal.WebauthnChallenge)
})
}
@ -2602,9 +2427,8 @@ func TestCreateRegisterChallenge(t *testing.T) {
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOn,
U2F: &types.U2F{
AppID: "https://" + env.server.ClusterName(),
Facets: []string{"https://" + env.server.ClusterName()},
Webauthn: &types.Webauthn{
RPID: env.server.ClusterName(),
},
})
require.NoError(t, err)
@ -2623,10 +2447,6 @@ func TestCreateRegisterChallenge(t *testing.T) {
name string
deviceType string
}{
{
name: "u2f challenge",
deviceType: "u2f",
},
{
name: "totp challenge",
deviceType: "totp",
@ -2651,8 +2471,6 @@ func TestCreateRegisterChallenge(t *testing.T) {
require.NoError(t, json.Unmarshal(res.Bytes(), &chal))
switch tc.deviceType {
case "u2f":
require.NotNil(t, chal.U2F)
case "totp":
require.NotNil(t, chal.TOTP.QRCode)
case "webauthn":
@ -3820,8 +3638,7 @@ func validateTerminalStream(t *testing.T, conn *websocket.Conn) {
require.NoError(t, err)
}
type mockProxySettings struct {
}
type mockProxySettings struct{}
func (mock *mockProxySettings) GetProxySettings(ctx context.Context) (*webclient.ProxySettings, error) {
return &webclient.ProxySettings{}, nil

View file

@ -20,7 +20,6 @@ import (
"net/http"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/httplib"
@ -75,9 +74,7 @@ type addMFADeviceRequest struct {
DeviceName string `json:"deviceName"`
// SecondFactorToken is the totp code.
SecondFactorToken string `json:"secondFactorToken"`
// U2FRegisterResponse is U2F registration challenge response.
U2FRegisterResponse *u2f.RegisterChallengeResponse `json:"u2fRegisterResponse"`
// WebauthnRegisterResponse is U2F registration challenge response.
// WebauthnRegisterResponse is a WebAuthn registration challenge response.
WebauthnRegisterResponse *webauthn.CredentialCreationResponse `json:"webauthnRegisterResponse"`
}
@ -98,13 +95,6 @@ func (h *Handler) addMFADeviceHandle(w http.ResponseWriter, r *http.Request, par
protoReq.NewMFAResponse = &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_TOTP{
TOTP: &proto.TOTPRegisterResponse{Code: req.SecondFactorToken},
}}
case req.U2FRegisterResponse != nil:
protoReq.NewMFAResponse = &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_U2F{
U2F: &proto.U2FRegisterResponse{
RegistrationData: req.U2FRegisterResponse.RegistrationData,
ClientData: req.U2FRegisterResponse.ClientData,
},
}}
case req.WebauthnRegisterResponse != nil:
protoReq.NewMFAResponse = &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_Webauthn{
Webauthn: webauthn.CredentialCreationResponseToProto(req.WebauthnRegisterResponse),

View file

@ -75,10 +75,10 @@ type tdpMFACodec struct{}
func (tdpMFACodec) encode(chal *auth.MFAAuthenticateChallenge, envelopeType string) ([]byte, error) {
switch envelopeType {
case defaults.WebsocketWebauthnChallenge, defaults.WebsocketU2FChallenge:
case defaults.WebsocketWebauthnChallenge:
default:
return nil, trace.BadParameter("received envelope type %v, expected either %v (WebAuthn) or %v (U2F)",
envelopeType, defaults.WebsocketWebauthnChallenge, defaults.WebsocketU2FChallenge)
return nil, trace.BadParameter(
"received envelope type %v, expected %v (WebAuthn)", envelopeType, defaults.WebsocketWebauthnChallenge)
}
tdpMsg := tdp.MFA{

View file

@ -20,7 +20,6 @@ import (
"net/http"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/httplib"
@ -37,8 +36,6 @@ type changePasswordReq struct {
NewPassword []byte `json:"new_password"`
// SecondFactorToken is user 2nd factor token
SecondFactorToken string `json:"second_factor_token"`
// U2FSignResponse is U2F response
U2FSignResponse *u2f.AuthenticateChallengeResponse `json:"u2f_sign_response"`
// WebauthnAssertionResponse is a Webauthn response
WebauthnAssertionResponse *wanlib.CredentialAssertionResponse `json:"webauthnAssertionResponse"`
}
@ -60,7 +57,6 @@ func (h *Handler) changePassword(w http.ResponseWriter, r *http.Request, p httpr
OldPassword: req.OldPassword,
NewPassword: req.NewPassword,
SecondFactorToken: req.SecondFactorToken,
U2FSignResponse: req.U2FSignResponse,
WebauthnResponse: req.WebauthnAssertionResponse,
}

View file

@ -583,11 +583,6 @@ func (s *sessionCache) AuthenticateWebUser(req *client.AuthenticateWebUserReques
authReq := auth.AuthenticateUserRequest{
Username: req.User,
}
if req.U2FSignResponse != nil {
authReq.U2F = &auth.U2FSignResponseCreds{
SignResponse: *req.U2FSignResponse,
}
}
if req.WebauthnAssertionResponse != nil {
authReq.Webauthn = req.WebauthnAssertionResponse
}
@ -637,11 +632,6 @@ func (s *sessionCache) AuthenticateSSHUser(c client.AuthenticateSSHUserRequest)
if c.Password != "" {
authReq.Pass = &auth.PassCreds{Password: []byte(c.Password)}
}
if c.U2FSignResponse != nil {
authReq.U2F = &auth.U2FSignResponseCreds{
SignResponse: *c.U2FSignResponse,
}
}
if c.WebauthnChallengeResponse != nil {
authReq.Webauthn = c.WebauthnChallengeResponse
}

View file

@ -36,7 +36,6 @@ import (
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/u2f"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
@ -350,33 +349,11 @@ func promptMFAChallenge(
// Convert from proto to JSON types.
switch {
// Webauthn takes precedence.
case c.GetWebauthnChallenge() != nil:
envelopeType = defaults.WebsocketWebauthnChallenge
chal = &auth.MFAAuthenticateChallenge{
WebauthnChallenge: wanlib.CredentialAssertionFromProto(c.WebauthnChallenge),
}
case len(c.U2F) > 0:
u2fChals := make([]u2f.AuthenticateChallenge, 0, len(c.U2F))
envelopeType = defaults.WebsocketU2FChallenge
for _, uc := range c.U2F {
u2fChals = append(u2fChals, u2f.AuthenticateChallenge{
Version: uc.Version,
Challenge: uc.Challenge,
KeyHandle: uc.KeyHandle,
AppID: uc.AppID,
})
}
chal = &auth.MFAAuthenticateChallenge{
AuthenticateChallenge: &u2f.AuthenticateChallenge{
// Get the common challenge fields from the first item.
// All of these fields should be identical for all u2fChals.
Challenge: u2fChals[0].Challenge,
AppID: u2fChals[0].AppID,
Version: u2fChals[0].Version,
},
U2FChallenges: u2fChals,
}
default:
return nil, trace.AccessDenied("only hardware keys are supported on the web terminal, please register a hardware device to connect to this server")
}
@ -545,7 +522,7 @@ func (t *TerminalHandler) writeError(err error, ws *websocket.Conn) error {
// and port.
func resolveServerHostPort(servername string, existingServers []types.Server) (string, int, error) {
// If port is 0, client wants us to figure out which port to use.
var defaultPort = 0
defaultPort := 0
if servername == "" {
return "", defaultPort, trace.BadParameter("empty server name")

View file

@ -24,7 +24,6 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/u2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/web/ui"
@ -167,8 +166,6 @@ func deleteUser(r *http.Request, params httprouter.Params, m userAPIGetter, user
type privilegeTokenRequest struct {
// SecondFactorToken is the totp code.
SecondFactorToken string `json:"secondFactorToken"`
// U2FSignResponse is u2f sign response for a u2f challenge.
U2FSignResponse *u2f.AuthenticateChallengeResponse `json:"u2fSignResponse"`
// WebauthnResponse is the response from authenticators.
WebauthnResponse *wanlib.CredentialAssertionResponse `json:"webauthnAssertionResponse"`
}
@ -187,14 +184,6 @@ func (h *Handler) createPrivilegeTokenHandle(w http.ResponseWriter, r *http.Requ
protoReq.ExistingMFAResponse = &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{
TOTP: &proto.TOTPResponse{Code: req.SecondFactorToken},
}}
case req.U2FSignResponse != nil:
protoReq.ExistingMFAResponse = &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_U2F{
U2F: &proto.U2FResponse{
KeyHandle: req.U2FSignResponse.KeyHandle,
ClientData: req.U2FSignResponse.ClientData,
Signature: req.U2FSignResponse.SignatureData,
},
}}
case req.WebauthnResponse != nil:
protoReq.ExistingMFAResponse = &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_Webauthn{
Webauthn: wanlib.CredentialAssertionResponseToProto(req.WebauthnResponse),