Files
teleport/lib/auth/password_test.go
Maxim fbdd4f214e fix: apply Access List grants in ChangeUserAuthentication (#65724)
- Password reset flow (ChangeUserAuthentication) was skipping
  CallLoginHooks and GetUserOrLoginState so the resulting web session
  was issues with only the user's static roles/traits.
- Update ChangeUserAuthentication to call the login hooks and resolve
  the UserLoginState before creating the session to match the normal
  login flow.
2026-04-21 23:27:45 +00:00

996 lines
32 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth_test
import (
"context"
"encoding/base32"
"fmt"
"math/rand/v2"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/header"
wanpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/authcatest"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/authtest"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/events/eventstest"
"github.com/gravitational/teleport/lib/services"
)
func TestUserNotFound(t *testing.T) {
t.Parallel()
ctx := context.Background()
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{Dir: t.TempDir()})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
username := "unknown-user"
password := "feefiefoefum"
err = as.AuthServer.CheckPasswordWOToken(ctx, username, []byte(password))
require.Error(t, err)
// Make sure the error is not a NotFound. That would be a username oracle.
require.True(t, trace.IsBadParameter(err))
}
func TestPasswordLengthChange(t *testing.T) {
t.Parallel()
ctx := context.Background()
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
AuthPreferenceSpec: &types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOff,
},
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
const (
username = "llama@goteleport.com"
password = "a"
// Pre-calculated bcrypt hash of "a" at minimum cost.
passwordHash = `$2a$10$xyxHFtG04s0kegyq1jwGB.faThKRIzDTCArbvKPxH6UKHriWz79H6`
)
u, _, err := authtest.CreateUserAndRole(as.AuthServer, username, []string{username}, nil)
require.NoError(t, err)
// Set an initial password that is shorter than minimum length
u.SetLocalAuth(&types.LocalAuthSecrets{PasswordHash: []byte(passwordHash)})
_, err = as.AuthServer.UpsertUser(ctx, u)
require.NoError(t, err)
// Ensure that a shorter password still works for auth
err = as.AuthServer.CheckPasswordWOToken(ctx, username, []byte(password))
require.NoError(t, err)
}
func TestChangePassword(t *testing.T) {
t.Parallel()
ctx := t.Context()
clock := clockwork.NewFakeClock()
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
Clock: clock,
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
mockEmitter := &eventstest.MockRecorderEmitter{}
as.AuthServer.SetEmitter(mockEmitter)
req, err := prepareForPasswordChange(as.AuthServer, "user1", []byte("abcdef123456"), constants.SecondFactorOff)
require.NoError(t, err)
req.NewPassword = []byte("defceba654321")
err = as.AuthServer.ChangePassword(ctx, req)
require.NoError(t, err)
require.Equal(t, events.UserPasswordChangeEvent, mockEmitter.LastEvent().GetType())
require.Equal(t, "user1", mockEmitter.LastEvent().(*apievents.UserPasswordChange).User)
shouldLockAfterFailedAttempts(t, as.AuthServer, req)
// advance time and make sure we can login again
clock.Advance(defaults.AccountLockInterval + time.Second)
req.OldPassword = req.NewPassword
req.NewPassword = []byte("123456abcdef")
err = as.AuthServer.ChangePassword(ctx, req)
require.NoError(t, err)
}
func TestChangePasswordWithOTP(t *testing.T) {
t.Parallel()
clock := clockwork.NewFakeClock()
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
Clock: clock,
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
req, err := prepareForPasswordChange(as.AuthServer, "user2", []byte("abcdef123456"), constants.SecondFactorOTP)
require.NoError(t, err)
otpSecret := base32.StdEncoding.EncodeToString([]byte("def456"))
dev, err := services.NewTOTPDevice("otp", otpSecret, clock.Now())
require.NoError(t, err)
ctx := context.Background()
err = as.AuthServer.UpsertMFADevice(ctx, req.User, dev)
require.NoError(t, err)
validToken, err := totp.GenerateCode(otpSecret, clock.Now())
require.NoError(t, err)
// change password
req.NewPassword = []byte("defceba654321")
req.SecondFactorToken = validToken
err = as.AuthServer.ChangePassword(ctx, req)
require.NoError(t, err)
shouldLockAfterFailedAttempts(t, as.AuthServer, req)
// advance time and make sure we can login again
clock.Advance(defaults.AccountLockInterval + time.Second)
validToken, _ = totp.GenerateCode(otpSecret, clock.Now())
req.OldPassword = req.NewPassword
req.NewPassword = []byte("123456abcdef")
req.SecondFactorToken = validToken
err = as.AuthServer.ChangePassword(ctx, req)
require.NoError(t, err)
}
func TestServer_ChangePassword(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
mfa := configureForMFA(t, srv)
username := mfa.User
password := mfa.Password
userClient, err := srv.NewClient(authtest.TestUser(username))
require.NoError(t, err)
passwordlessDev, err := authtest.RegisterTestDevice(
context.Background(),
userClient,
"passwordless-1",
proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
mfa.TOTPDev,
authtest.WithPasswordless())
require.NoError(t, err)
tests := []struct {
name string
oldPass string
newPass string
device *authtest.Device
challengeRequest *proto.CreateAuthenticateChallengeRequest
}{
{
name: "OK TOTP-based change",
oldPass: password,
newPass: "llamasarecool11",
device: mfa.TOTPDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
},
},
},
{
name: "OK TOTP-based change (legacy flow)",
oldPass: "llamasarecool11",
newPass: "llamasarecool12",
device: mfa.TOTPDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_UserCredentials{
UserCredentials: &proto.UserCredentials{
Username: username,
Password: []byte("llamasarecool11"),
},
},
},
},
{
name: "OK Webauthn-based change",
oldPass: "llamasarecool12",
newPass: "llamasarecool13",
device: mfa.WebDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
},
},
},
{
name: "OK with verification explicitly set to discouraged",
oldPass: "llamasarecool13",
newPass: "llamasarecool14",
device: mfa.WebDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
UserVerificationRequirement: "discouraged",
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
},
},
},
{
name: "OK passwordless change",
oldPass: "",
newPass: "llamasarecool15",
device: passwordlessDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
UserVerificationRequirement: "required",
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
},
},
},
}
authServer := srv.Auth()
ctx := context.Background()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
oldPass := []byte(test.oldPass)
newPass := []byte(test.newPass)
// Acquire and solve an MFA challenge.
mfaChallenge, err := userClient.CreateAuthenticateChallenge(ctx, test.challengeRequest)
require.NoError(t, err, "creating challenge")
mfaResp, err := test.device.SolveAuthn(mfaChallenge)
require.NoError(t, err, "solving challenge with device")
// Change password.
req := &proto.ChangePasswordRequest{
User: username,
OldPassword: oldPass,
NewPassword: newPass,
}
switch {
case mfaResp.GetTOTP() != nil:
req.SecondFactorToken = mfaResp.GetTOTP().Code
case mfaResp.GetWebauthn() != nil:
req.Webauthn = mfaResp.GetWebauthn()
}
require.NoError(t, userClient.ChangePassword(ctx, req), "changing password")
// Did the password change take effect?
require.NoError(t, authServer.CheckPasswordWOToken(ctx, username, newPass), "password change didn't take effect")
})
}
}
// This test asserts that an attacker is unable to change password without
// providing the old one if they take over a user's web session and use a
// different type of WebAuthn challenge that would be normally requested by the
// Web UI. This is a regression test for
// https://github.com/gravitational/teleport-private/issues/1369.
func TestServer_ChangePassword_Fails(t *testing.T) {
t.Parallel()
server := newTestTLSServer(t)
mfa := configureForMFA(t, server)
authServer := server.Auth()
ctx := context.Background()
username := mfa.User
password := mfa.Password
tests := []struct {
name string
oldPass string
device *authtest.Device
challengeRequest *proto.CreateAuthenticateChallengeRequest
createChallengeAssertion require.ErrorAssertionFunc
}{
{
name: "No old password, TOTP challenge",
oldPass: "",
device: mfa.TOTPDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
},
},
createChallengeAssertion: require.NoError,
},
{
name: "No old password, WebAuthn challenge",
oldPass: "",
device: mfa.WebDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
},
},
createChallengeAssertion: require.NoError,
},
{
name: "Empty challenge request",
oldPass: password,
device: mfa.WebDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{},
createChallengeAssertion: require.Error,
},
{
name: "Unspecified challenge scope",
oldPass: password,
device: mfa.WebDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{},
},
createChallengeAssertion: require.Error,
},
{
name: "Illegal challenge scope",
oldPass: password,
device: mfa.WebDev,
challengeRequest: &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
},
},
createChallengeAssertion: require.NoError,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
newPass := []byte("capybarasarecool123")
oldPass := []byte(test.oldPass)
userClient, err := server.NewClient(authtest.TestUser(username))
require.NoError(t, err)
defer userClient.Close()
// Acquire and solve an MFA challenge.
mfaChallenge, err := userClient.CreateAuthenticateChallenge(ctx, test.challengeRequest)
test.createChallengeAssertion(t, err, "creating challenge")
if err != nil {
return
}
mfaResp, err := test.device.SolveAuthn(mfaChallenge)
require.NoError(t, err, "solving challenge with device")
// Change password.
req := &proto.ChangePasswordRequest{
User: username,
OldPassword: oldPass,
NewPassword: newPass,
}
switch {
case mfaResp.GetTOTP() != nil:
req.SecondFactorToken = mfaResp.GetTOTP().Code
case mfaResp.GetWebauthn() != nil:
req.Webauthn = mfaResp.GetWebauthn()
}
err = userClient.ChangePassword(ctx, req)
assert.True(t,
trace.IsAccessDenied(err),
"ChangePassword error mismatch, want=AccessDenied, got=%v (%T)",
err, trace.Unwrap(err))
// Did the password change take effect?
assert.Error(t, authServer.CheckPasswordWOToken(ctx, username, newPass), "password was changed")
})
}
}
func TestChangeUserAuthentication(t *testing.T) {
t.Parallel()
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{Dir: t.TempDir()})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
authServer := as.AuthServer
clock := as.Clock()
ctx := context.Background()
tests := []struct {
name string
setAuthPreference func(t *testing.T)
getReq func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest
getInvalidReq func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest
}{
{
name: "with second factor off and password only",
setAuthPreference: func(t *testing.T) {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOff,
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
},
getReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password1357"),
}
},
},
{
name: "with second factor otp",
setAuthPreference: func(t *testing.T) {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOTP,
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
},
getReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
registerChal, err := authServer.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
TokenID: resetTokenID,
DeviceType: proto.DeviceType_DEVICE_TYPE_TOTP,
})
require.NoError(t, err, "CreateRegisterChallenge")
_, registerSolved, err := authtest.NewTestDeviceFromChallenge(registerChal, authtest.WithTestDeviceClock(clock))
require.NoError(t, err, "NewTestDeviceFromChallenge")
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password2468"),
NewMFARegisterResponse: registerSolved,
}
},
// Invalid MFA fields when auth settings set to only otp.
getInvalidReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password2468"),
NewMFARegisterResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_Webauthn{
Webauthn: &wanpb.CredentialCreationResponse{},
},
},
}
},
},
{
name: "with second factor webauthn",
setAuthPreference: func(t *testing.T) {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorWebauthn,
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
},
getReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
registerChal, err := authServer.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
TokenID: resetTokenID,
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
DeviceUsage: proto.DeviceUsage_DEVICE_USAGE_MFA,
})
require.NoError(t, err, "CreateRegisterChallenge")
_, registerSolved, err := authtest.NewTestDeviceFromChallenge(registerChal)
require.NoError(t, err, "NewTestDeviceFromChallenge")
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password3579"),
NewMFARegisterResponse: registerSolved,
}
},
// Invalid totp fields when auth settings set to only webauthn.
getInvalidReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password3579"),
NewMFARegisterResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_TOTP{},
},
}
},
},
{
name: "with passwordless",
setAuthPreference: func(t *testing.T) {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorWebauthn,
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
},
getReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
registerChal, err := authServer.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
TokenID: resetTokenID,
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
DeviceUsage: proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS,
})
require.NoError(t, err, "CreateRegisterChallenge")
_, registerSolved, err := authtest.NewTestDeviceFromChallenge(registerChal, authtest.WithPasswordless())
require.NoError(t, err, "NewTestDeviceFromChallenge")
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewMFARegisterResponse: registerSolved,
}
},
// Missing webauthn for passwordless.
getInvalidReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewMFARegisterResponse: &proto.MFARegisterResponse{
Response: &proto.MFARegisterResponse_TOTP{},
},
}
},
},
{
name: "with second factor on",
setAuthPreference: func(t *testing.T) {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOn,
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
},
getReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
registerChal, err := authServer.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{
TokenID: resetTokenID,
DeviceType: proto.DeviceType_DEVICE_TYPE_WEBAUTHN,
DeviceUsage: proto.DeviceUsage_DEVICE_USAGE_MFA,
})
require.NoError(t, err, "CreateRegisterChallenge")
_, registerSolved, err := authtest.NewTestDeviceFromChallenge(registerChal)
require.NoError(t, err, "NewTestDeviceFromChallenge")
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password4680"),
NewMFARegisterResponse: registerSolved,
NewDeviceName: "new-device",
}
},
// Empty register response, when auth settings requires second factors.
getInvalidReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password4680"),
}
},
},
{
name: "with second factor optional and no second factor",
setAuthPreference: func(t *testing.T) {
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
},
getReq: func(t *testing.T, resetTokenID string) *proto.ChangeUserAuthenticationRequest {
return &proto.ChangeUserAuthenticationRequest{
TokenID: resetTokenID,
NewPassword: []byte("password5791"),
}
},
},
}
for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
username := fmt.Sprintf("llama%v@goteleport.com", rand.Int())
_, _, err := authtest.CreateUserAndRole(authServer, username, []string{username}, nil)
require.NoError(t, err)
c.setAuthPreference(t)
resetToken, err := authServer.CreateResetPasswordToken(ctx, authclient.CreateUserTokenRequest{
Name: username,
})
require.NoError(t, err)
token := resetToken.GetName()
if c.getInvalidReq != nil {
invalidReq := c.getInvalidReq(t, token)
_, err := auth.ChangeUserAuthentication(ctx, authServer, invalidReq)
require.True(t, trace.IsBadParameter(err))
}
validReq := c.getReq(t, token)
_, err = auth.ChangeUserAuthentication(ctx, authServer, validReq)
require.NoError(t, err)
// Test password is updated.
if len(validReq.NewPassword) != 0 {
err := authServer.CheckPasswordWOToken(ctx, username, validReq.NewPassword)
require.NoError(t, err)
}
// Test device was registered.
if validReq.NewMFARegisterResponse != nil {
devs, err := authServer.Services.GetMFADevices(ctx, username, false /* without secrets*/)
require.NoError(t, err)
require.Len(t, devs, 1)
// Test device name setting.
dev := devs[0]
var wantName string
switch {
case validReq.NewDeviceName != "":
wantName = validReq.NewDeviceName
case dev.GetTotp() != nil:
wantName = "otp"
case dev.GetWebauthn() != nil:
wantName = "webauthn"
}
require.Equal(t, wantName, dev.GetName(), "device name mismatch")
}
})
}
}
// TestChangeUserAuthentication_AccessListRolesApplied verifies that roles
// granted via Access Lists are included in the web session created by the
// password-reset flow (ChangeUserAuthentication).
//
// Regression test for: https://github.com/gravitational/teleport/issues/65722
func TestChangeUserAuthentication_AccessListRolesApplied(t *testing.T) {
t.Parallel()
ctx := t.Context()
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{Dir: t.TempDir()})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
authServer := as.AuthServer
// Disable second factor so the reset request only needs a password.
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOff,
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
// Create the role that the access list will grant.
grantedRole, err := types.NewRole("al-granted-role", types.RoleSpecV6{})
require.NoError(t, err)
_, err = authServer.UpsertRole(ctx, grantedRole)
require.NoError(t, err)
// Create a user with a single static role only.
username := "reset-al-test@goteleport.com"
_, _, err = authtest.CreateUserAndRole(authServer, username, []string{"access"}, nil)
require.NoError(t, err)
user, err := authServer.GetUser(ctx, username, false)
require.NoError(t, err)
// Create an access list that grants al-granted-role to its members.
al, err := accesslist.NewAccessList(header.Metadata{
Name: "test-access-list",
}, accesslist.Spec{
Title: "Test Access List",
Owners: []accesslist.Owner{
{Name: username, Description: "owner"},
},
Audit: accesslist.Audit{
NextAuditDate: authServer.GetClock().Now().Add(24 * time.Hour),
},
Grants: accesslist.Grants{
Roles: []string{"al-granted-role"},
},
})
require.NoError(t, err)
_, err = authServer.UpsertAccessList(ctx, al)
require.NoError(t, err)
// Add the user as a member of the access list.
member, err := accesslist.NewAccessListMember(header.Metadata{
Name: username,
}, accesslist.AccessListMemberSpec{
AccessList: al.GetName(),
Name: username,
Joined: authServer.GetClock().Now(),
Expires: authServer.GetClock().Now().Add(24 * time.Hour),
Reason: "test",
AddedBy: user.GetName(),
})
require.NoError(t, err)
_, err = authServer.UpsertAccessListMember(ctx, member)
require.NoError(t, err)
// Create a reset password token and call ChangeUserAuthentication.
resetToken, err := authServer.CreateResetPasswordToken(ctx, authclient.CreateUserTokenRequest{
Name: username,
})
require.NoError(t, err)
resp, err := authServer.ChangeUserAuthentication(ctx, &proto.ChangeUserAuthenticationRequest{
TokenID: resetToken.GetName(),
NewPassword: []byte("newpassword123!"),
})
require.NoError(t, err)
require.NotNil(t, resp.WebSession)
// Parse the TLS certificate from the web session and verify it contains
// the Access List-granted role.
_, identity := parseX509PEMAndIdentity(t, resp.WebSession.GetTLSCert())
require.Contains(t, identity.Groups, "al-granted-role",
"expected Access List-granted role in session certificate, got roles: %v", identity.Groups)
}
func TestChangeUserAuthenticationWithErrors(t *testing.T) {
t.Parallel()
authPreference, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactors: []types.SecondFactorType{types.SecondFactorType_SECOND_FACTOR_TYPE_OTP},
})
require.NoError(t, err)
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
AuthPreferenceSpec: &authPreference.(*types.AuthPreferenceV2).Spec,
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
ctx := context.Background()
username := "joe@example.com"
_, _, err = authtest.CreateUserAndRole(as.AuthServer, username, []string{username}, nil)
require.NoError(t, err)
token, err := as.AuthServer.CreateResetPasswordToken(ctx, authclient.CreateUserTokenRequest{
Name: username,
})
require.NoError(t, err)
validPassword := []byte("qwertyQWERTY1")
validTokenID := token.GetName()
type testCase struct {
desc string
secondFactor constants.SecondFactorType
req *proto.ChangeUserAuthenticationRequest
}
testCases := []testCase{
{
secondFactor: constants.SecondFactorOff,
desc: "invalid tokenID value",
req: &proto.ChangeUserAuthenticationRequest{
TokenID: "what_token",
NewPassword: validPassword,
},
},
{
secondFactor: constants.SecondFactorOff,
desc: "invalid password",
req: &proto.ChangeUserAuthenticationRequest{
TokenID: validTokenID,
NewPassword: []byte("short"),
},
},
{
secondFactor: constants.SecondFactorOTP,
desc: "missing second factor",
req: &proto.ChangeUserAuthenticationRequest{
TokenID: validTokenID,
NewPassword: validPassword,
},
},
{
secondFactor: constants.SecondFactorOTP,
desc: "invalid OTP value",
req: &proto.ChangeUserAuthenticationRequest{
TokenID: validTokenID,
NewPassword: validPassword,
NewMFARegisterResponse: &proto.MFARegisterResponse{Response: &proto.MFARegisterResponse_TOTP{
TOTP: &proto.TOTPRegisterResponse{Code: "invalid"},
}},
},
},
}
for _, tc := range testCases {
// set new auth preference settings
authPreference.SetSecondFactor(tc.secondFactor)
authPreference, err = as.AuthServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
_, err = auth.ChangeUserAuthentication(ctx, as.AuthServer, tc.req)
require.Error(t, err, "test case %q", tc.desc)
}
authPreference.SetSecondFactor(constants.SecondFactorOff)
_, err = as.AuthServer.UpsertAuthPreference(ctx, authPreference)
require.NoError(t, err)
_, err = auth.ChangeUserAuthentication(ctx, as.AuthServer, &proto.ChangeUserAuthenticationRequest{
TokenID: validTokenID,
NewPassword: validPassword,
})
require.NoError(t, err)
// invite token cannot be reused
_, err = auth.ChangeUserAuthentication(ctx, as.AuthServer, &proto.ChangeUserAuthenticationRequest{
TokenID: validTokenID,
NewPassword: validPassword,
})
require.Error(t, err)
}
func TestResetPassword(t *testing.T) {
t.Parallel()
as, err := authtest.NewAuthServer(authtest.AuthServerConfig{
Dir: t.TempDir(),
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, as.Close()) })
_, _, err = authtest.CreateUserAndRole(as.AuthServer, "dave", []string{"dave"}, nil)
require.NoError(t, err)
// Using the Identity service makes it easier to set up the test case.
err = as.AuthServer.UpsertPassword("dave", []byte("it's full of stars!"))
require.NoError(t, err)
// Reset password.
ctx := context.Background()
err = as.AuthServer.ResetPassword(ctx, "dave")
require.NoError(t, err)
// Make sure that the password has been reset.
u, err := as.AuthServer.IdentityInternal.GetUser(ctx, "dave", true /* withSecrets */)
require.NoError(t, err)
assert.Nil(t, u.GetLocalAuth(), "user LocalAuth not nil")
assert.Equal(t, types.PasswordState_PASSWORD_STATE_UNSET, u.GetPasswordState())
// Make sure that we can reset once again (i.e. we don't complain if there's
// no password).
err = as.AuthServer.ResetPassword(ctx, "dave")
require.NoError(t, err)
}
func shouldLockAfterFailedAttempts(t *testing.T, as *auth.Server, req *proto.ChangePasswordRequest) {
loginAttempts, _ := as.GetUserLoginAttempts(req.User)
require.Empty(t, loginAttempts)
for i := range defaults.MaxLoginAttempts {
err := as.ChangePassword(t.Context(), req)
require.Error(t, err)
loginAttempts, _ = as.GetUserLoginAttempts(req.User)
require.Len(t, loginAttempts, i+1)
}
err := as.ChangePassword(t.Context(), req)
require.True(t, trace.IsAccessDenied(err))
}
func prepareForPasswordChange(as *auth.Server, user string, pass []byte, secondFactorType constants.SecondFactorType) (*proto.ChangePasswordRequest, error) {
ctx := context.Background()
req := &proto.ChangePasswordRequest{
User: user,
OldPassword: pass,
}
userCA, err := authcatest.NewCA(types.UserCA, "me.localhost")
if err != nil {
return nil, trace.Wrap(err)
}
if err := as.UpsertCertAuthority(ctx, userCA); err != nil {
return nil, err
}
hostCA, err := authcatest.NewCA(types.HostCA, "me.localhost")
if err != nil {
return nil, err
}
if err := as.UpsertCertAuthority(ctx, hostCA); err != nil {
return nil, err
}
ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: secondFactorType,
})
if err != nil {
return nil, err
}
if _, err := as.UpsertAuthPreference(ctx, ap); err != nil {
return nil, err
}
if _, _, err := authtest.CreateUserAndRole(as, user, []string{user}, nil); err != nil {
return nil, err
}
if err := as.UpsertPassword(user, pass); err != nil {
return nil, err
}
return req, nil
}