Skip to content

Commit

Permalink
Merge branch 'branch/v15' into bot/backport-39033-branch/v15
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoShaka authored Mar 8, 2024
2 parents 8bf9d4a + 894bc4a commit 6f1425c
Show file tree
Hide file tree
Showing 35 changed files with 978 additions and 178 deletions.
65 changes: 35 additions & 30 deletions api/gen/proto/go/teleport/mfa/v1/mfa.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/proto/teleport/mfa/v1/mfa.proto
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ enum ChallengeScope {
// more precise scope. Instead, new scopes should be added. This scope may
// also be split into multiple smaller scopes in the future.
CHALLENGE_SCOPE_ADMIN_ACTION = 7;
// Used for changing user's password.
CHALLENGE_SCOPE_CHANGE_PASSWORD = 8;
}

// ChallengeAllowReuse determines whether an MFA challenge response can be used
Expand Down
13 changes: 9 additions & 4 deletions lib/auth/accountrecovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

"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"
apievents "github.com/gravitational/teleport/api/types/events"
wanpb "github.com/gravitational/teleport/api/types/webauthn"
Expand Down Expand Up @@ -1232,10 +1233,14 @@ func TestGetAccountRecoveryCodes(t *testing.T) {

func triggerLoginLock(t *testing.T, srv *Server, username string) {
for i := 1; i <= defaults.MaxLoginAttempts; i++ {
_, _, _, err := srv.authenticateUser(context.Background(), AuthenticateUserRequest{
Username: username,
OTP: &OTPCreds{},
})
_, _, _, err := srv.authenticateUser(
context.Background(),
AuthenticateUserRequest{
Username: username,
OTP: &OTPCreds{},
},
mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN},
)
require.True(t, trace.IsAccessDenied(err))

// Test last attempt returns locked error.
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ func TestServer_Authenticate_nonPasswordlessRequiresUsername(t *testing.T) {
{
name: "WebAuthn",
dev: mfa.WebDev,
wantErr: "invalid Webauthn response", // generic error as it _could_ be a passwordless attempt
wantErr: "invalid credentials", // generic error as it _could_ be a passwordless attempt
},
}
for _, test := range tests {
Expand Down
8 changes: 7 additions & 1 deletion lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/header"
Expand Down Expand Up @@ -624,7 +625,12 @@ func TestAuthenticateUser_mfaDeviceLocked(t *testing.T) {

t.Run("locked device password change", func(t *testing.T) {
chal, err := userClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{},
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
ChallengeExtensions: &mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
},
})
require.NoError(t, err, "CreateAuthenticateChallenge")

Expand Down
38 changes: 31 additions & 7 deletions lib/auth/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ type SessionCreds struct {
func (a *Server) AuthenticateUser(ctx context.Context, req AuthenticateUserRequest) (services.UserState, services.AccessChecker, error) {
username := req.Username

verifyMFALocks, mfaDev, actualUsername, err := a.authenticateUser(ctx, req)
requiredExt := mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
}
verifyMFALocks, mfaDev, actualUsername, err := a.authenticateUser(
ctx, req, requiredExt)
if err != nil {
// Log event after authentication failure
if err := a.emitAuthAuditEvent(ctx, authAuditProps{
Expand Down Expand Up @@ -259,7 +263,7 @@ var (
authenticateHeadlessError = trace.AccessDenied("headless authentication failed")
// authenticateWebauthnError is the generic error returned for failed WebAuthn
// authentication attempts.
authenticateWebauthnError = trace.AccessDenied("invalid Webauthn response")
authenticateWebauthnError = trace.AccessDenied("invalid credentials")
// invalidUserPassError is the error for when either the provided username or
// password is incorrect.
invalidUserPassError = trace.AccessDenied("invalid username or password")
Expand All @@ -285,7 +289,19 @@ type verifyMFADeviceLocksParams struct {
}

// authenticateUser authenticates a user through various methods (password, MFA,
// passwordless)
// passwordless). Note that "authenticate" may mean different things, depending
// on the request and required extensions. The caller is responsible for making
// sure that the request and extensions correctly describe the conditions that
// are being asserted during the authentication process. One important caveat is
// that if the request contains a WebAuthn challenge response, the user name is
// known, and `authenticateUser` is called without a password, the function only
// performs a second factor check; in this case, the caller must provide
// appropriate required extensions to validate the WebAuthn challenge response.
//
// The `requiredExt` parameter is used to assert that the MFA challenge has a
// correct extensions. It is ignored for passwordless authentication, where we
// override it with an extension that has scope set to
// `mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN`.
//
// Returns a callback to verify MFA device locks, the MFA device used to
// authenticate (if applicable), and the authenticated user name.
Expand All @@ -294,8 +310,9 @@ type verifyMFADeviceLocksParams struct {
func (a *Server) authenticateUser(
ctx context.Context,
req AuthenticateUserRequest,
requiredExt mfav1.ChallengeExtensions,
) (verifyLocks func(verifyMFADeviceLocksParams) error, mfaDev *types.MFADevice, user string, err error) {
mfaDev, user, err = a.authenticateUserInternal(ctx, req)
mfaDev, user, err = a.authenticateUserInternal(ctx, req, requiredExt)
if err != nil || mfaDev == nil {
return func(verifyMFADeviceLocksParams) error { return nil }, mfaDev, user, trace.Wrap(err)
}
Expand Down Expand Up @@ -340,7 +357,11 @@ func (a *Server) authenticateUser(
}

// Do not use this method directly, use authenticateUser instead.
func (a *Server) authenticateUserInternal(ctx context.Context, req AuthenticateUserRequest) (mfaDev *types.MFADevice, user string, err error) {
func (a *Server) authenticateUserInternal(
ctx context.Context,
req AuthenticateUserRequest,
requiredExt mfav1.ChallengeExtensions,
) (mfaDev *types.MFADevice, user string, err error) {
if err := req.CheckAndSetDefaults(); err != nil {
return nil, "", trace.Wrap(err)
}
Expand All @@ -349,6 +370,10 @@ func (a *Server) authenticateUserInternal(ctx context.Context, req AuthenticateU

// Only one path if passwordless, other variants shouldn't see an empty user.
if passwordless {
if requiredExt.Scope != mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN {
return nil, "", trace.BadParameter(
"passwordless authentication can only be used with the PASSWORDLESS scope")
}
return a.authenticatePasswordless(ctx, req)
}

Expand Down Expand Up @@ -381,8 +406,7 @@ func (a *Server) authenticateUserInternal(ctx context.Context, req AuthenticateU
Webauthn: wantypes.CredentialAssertionResponseToProto(req.Webauthn),
},
}
requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN}
mfaData, err := a.ValidateMFAAuthResponse(ctx, mfaResponse, user, requiredExt)
mfaData, err := a.ValidateMFAAuthResponse(ctx, mfaResponse, user, &requiredExt)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down
18 changes: 15 additions & 3 deletions lib/auth/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/subtle"
"net/mail"

"github.com/go-webauthn/webauthn/protocol"
"github.com/gravitational/trace"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
Expand All @@ -31,6 +32,7 @@ import (
"github.com/gravitational/teleport"
"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"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/keys"
Expand Down Expand Up @@ -129,16 +131,26 @@ func (a *Server) ChangePassword(ctx context.Context, req *proto.ChangePasswordRe
Username: user,
Webauthn: wantypes.CredentialAssertionResponseFromProto(req.Webauthn),
}
authReq.Pass = &PassCreds{
Password: req.OldPassword,
requiredExt := mfav1.ChallengeExtensions{
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_CHANGE_PASSWORD,
}
if len(req.OldPassword) > 0 {
authReq.Pass = &PassCreds{
Password: req.OldPassword,
}
} else {
// If the user didn't provide their old password, we need to require
// identity verification (i.e. make sure that a resident token used for
// MFA).
requiredExt.UserVerificationRequirement = string(protocol.VerificationRequired)
}
if req.SecondFactorToken != "" {
authReq.OTP = &OTPCreds{
Password: req.OldPassword,
Token: req.SecondFactorToken,
}
}
verifyMFALocks, _, _, err := a.authenticateUser(ctx, authReq)
verifyMFALocks, _, _, err := a.authenticateUser(ctx, authReq, requiredExt)
if err != nil {
return trace.Wrap(err)
}
Expand Down
Loading

0 comments on commit 6f1425c

Please sign in to comment.