diff --git a/lib/auth/webauthn/register.go b/lib/auth/webauthn/register.go index 253bd8dbd553d..a13ffb82a2809 100644 --- a/lib/auth/webauthn/register.go +++ b/lib/auth/webauthn/register.go @@ -147,11 +147,16 @@ func (f *RegistrationFlow) Begin(ctx context.Context, user string, passwordless if dev.GetU2F() != nil { continue } - // Skip resident/non-resident keys depending on whether it's a passwordless - // registration. - // Letting users have both allows them to "swap" between key types in the - // same device. - if webDev := dev.GetWebauthn(); webDev != nil && webDev.ResidentKey != passwordless { + + // Let authenticator "upgrades" from non-resident (MFA) to resident + // (passwordless) happen, but prevent "downgrades" from resident to + // non-resident. + // + // Modern passkey implementations will "disobey" our MFA registrations and + // actually create passkeys, silently replacing the old passkey with the new + // "MFA" key, which can make Teleport confused (for example, by letting the + // "MFA" key be deleted because Teleport thinks the passkey still exists). + if webDev := dev.GetWebauthn(); webDev != nil && !webDev.ResidentKey && passwordless { continue } diff --git a/lib/auth/webauthn/register_test.go b/lib/auth/webauthn/register_test.go index 9dcfb4ad74060..3643973402a0c 100644 --- a/lib/auth/webauthn/register_test.go +++ b/lib/auth/webauthn/register_test.go @@ -143,32 +143,32 @@ func TestRegistrationFlow_Begin_excludeList(t *testing.T) { const user = "llama" const rpID = "localhost" - dev1ID := []byte{1, 1, 1} // U2F - web1ID := []byte{1, 1, 2} // WebAuthn / MFA - rk1ID := []byte{1, 1, 3} // WebAuthn / passwordless - dev1 := &types.MFADevice{ + u2fID := []byte{1, 1, 1} // U2F + mfaID := []byte{1, 1, 2} // WebAuthn / MFA + passkeyID := []byte{1, 1, 3} // WebAuthn / passwordless + u2fDev := &types.MFADevice{ Device: &types.MFADevice_U2F{ U2F: &types.U2FDevice{ - KeyHandle: dev1ID, + KeyHandle: u2fID, }, }, } - web1 := &types.MFADevice{ + mfaDev := &types.MFADevice{ Device: &types.MFADevice_Webauthn{ Webauthn: &types.WebauthnDevice{ - CredentialId: web1ID, + CredentialId: mfaID, }, }, } - rk1 := &types.MFADevice{ + passkeyDev := &types.MFADevice{ Device: &types.MFADevice_Webauthn{ Webauthn: &types.WebauthnDevice{ - CredentialId: rk1ID, + CredentialId: passkeyID, ResidentKey: true, }, }, } - identity := newFakeIdentity(user, dev1, web1, rk1) + identity := newFakeIdentity(user, u2fDev, mfaDev, passkeyDev) rf := wanlib.RegistrationFlow{ Webauthn: &types.Webauthn{ @@ -184,13 +184,18 @@ func TestRegistrationFlow_Begin_excludeList(t *testing.T) { wantExcludeList [][]byte }{ { - name: "MFA", - wantExcludeList: [][]byte{web1ID}, // U2F and resident excluded + name: "MFA", + wantExcludeList: [][]byte{ + mfaID, + passkeyID, // Prevents "downgrades" + }, }, { - name: "passwordless", - passwordless: true, - wantExcludeList: [][]byte{rk1ID}, // U2F and MFA excluded + name: "passwordless", + passwordless: true, + wantExcludeList: [][]byte{ + passkeyID, + }, }, } for _, test := range tests {