Files
teleport/lib/auth/password.go
T
Michael Wilson 49c64aabcb Move the authorizer into its own package. (#22776)
* Move the authorizer into its own package.

The Authorizer and associated functions and constants have been moved into
their own package `authz`. This will allow us to more easily create separate
services for gRPC functionality so that we're not pushing as much into the
`lib/auth` package.

Though this PR is large, the only actual code change here is making
`ContextForLocalUser` exported and changing the `permissions_test.go` so that
it doesn't rely on the `lib/auth` package.

* Use context functions.

* Test and linting fixes.

* Add in a temporary alias file to ensure e still compiles.

* Use functions instead of var aliases for performance reasons.

* Adding in godocs, cleaning up tests.

* Fix tests.

* Add a few more aliases.
2023-03-09 02:21:10 +00:00

390 lines
12 KiB
Go

// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"crypto/subtle"
"net/mail"
"github.com/gravitational/trace"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/bcrypt"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/utils/keys"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
// This is bcrypt hash for password "barbaz".
var fakePasswordHash = []byte(`$2a$10$Yy.e6BmS2SrGbBDsyDLVkOANZmvjjMR890nUGSXFJHBXWzxe7T44m`)
// ChangeUserAuthentication implements AuthService.ChangeUserAuthentication.
func (s *Server) ChangeUserAuthentication(ctx context.Context, req *proto.ChangeUserAuthenticationRequest) (*proto.ChangeUserAuthenticationResponse, error) {
user, err := s.changeUserAuthentication(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
// Check if a user can receive new recovery codes.
_, emailErr := mail.ParseAddress(user.GetName())
hasEmail := emailErr == nil
hasMFA := req.GetNewMFARegisterResponse() != nil
recoveryAllowed := s.isAccountRecoveryAllowed(ctx) == nil
createRecoveryCodes := hasEmail && hasMFA && recoveryAllowed
var newRecovery *proto.RecoveryCodes
if createRecoveryCodes {
newRecovery, err = s.generateAndUpsertRecoveryCodes(ctx, user.GetName())
if err != nil {
return nil, trace.Wrap(err)
}
}
webSession, err := s.createUserWebSession(ctx, user)
if err != nil {
if keys.IsPrivateKeyPolicyError(err) {
// Do not return an error, otherwise
// the user won't be able to receive
// recovery codes. Even with no recovery codes
// this positive response indicates the user
// has successfully reset/registered their account.
return &proto.ChangeUserAuthenticationResponse{
Recovery: newRecovery,
PrivateKeyPolicyEnabled: true,
}, nil
}
return nil, trace.Wrap(err)
}
sess, ok := webSession.(*types.WebSessionV2)
if !ok {
return nil, trace.BadParameter("unexpected WebSessionV2 type %T", sess)
}
return &proto.ChangeUserAuthenticationResponse{
WebSession: sess,
Recovery: newRecovery,
}, nil
}
// ResetPassword securely generates a new random password and assigns it to user.
// This method is used to invalidate existing user password during password
// reset process.
func (s *Server) ResetPassword(username string) (string, error) {
user, err := s.GetUser(username, false)
if err != nil {
return "", trace.Wrap(err)
}
password, err := utils.CryptoRandomHex(defaults.ResetPasswordLength)
if err != nil {
return "", trace.Wrap(err)
}
err = s.UpsertPassword(user.GetName(), []byte(password))
if err != nil {
return "", trace.Wrap(err)
}
return password, nil
}
// ChangePassword updates users password based on the old password.
func (s *Server) ChangePassword(ctx context.Context, req *proto.ChangePasswordRequest) error {
// validate new password
if err := services.VerifyPassword(req.NewPassword); err != nil {
return trace.Wrap(err)
}
// Authenticate.
user := req.User
authReq := AuthenticateUserRequest{
Username: user,
Webauthn: wanlib.CredentialAssertionResponseFromProto(req.Webauthn),
}
if len(req.OldPassword) > 0 {
authReq.Pass = &PassCreds{
Password: req.OldPassword,
}
}
if req.SecondFactorToken != "" {
authReq.OTP = &OTPCreds{
Password: req.OldPassword,
Token: req.SecondFactorToken,
}
}
if _, _, err := s.authenticateUser(ctx, authReq); err != nil {
return trace.Wrap(err)
}
if err := s.UpsertPassword(user, req.NewPassword); err != nil {
return trace.Wrap(err)
}
if err := s.emitter.EmitAuditEvent(s.closeCtx, &apievents.UserPasswordChange{
Metadata: apievents.Metadata{
Type: events.UserPasswordChangeEvent,
Code: events.UserPasswordChangeCode,
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, user),
}); err != nil {
log.WithError(err).Warn("Failed to emit password change event.")
}
return nil
}
// checkPasswordWOToken checks just password without checking OTP tokens
// used in case of SSH authentication, when token has been validated.
func (s *Server) checkPasswordWOToken(user string, password []byte) error {
const errMsg = "invalid username or password"
err := services.VerifyPassword(password)
if err != nil {
return trace.BadParameter(errMsg)
}
hash, err := s.GetPasswordHash(user)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
userFound := true
if trace.IsNotFound(err) {
userFound = false
log.Debugf("Username %q not found, using fake hash to mitigate timing attacks.", user)
hash = fakePasswordHash
}
if err = bcrypt.CompareHashAndPassword(hash, password); err != nil {
log.Debugf("Password for %q does not match", user)
return trace.BadParameter(errMsg)
}
// Careful! The bcrypt check above may succeed for an unknown user when the
// provided password is "barbaz", which is what fakePasswordHash hashes to.
if !userFound {
return trace.BadParameter(errMsg)
}
return nil
}
type checkPasswordResult struct {
mfaDev *types.MFADevice
}
// checkPassword checks the password and OTP token. Called by tsh or lib/web/*.
func (s *Server) checkPassword(user string, password []byte, otpToken string) (*checkPasswordResult, error) {
err := s.checkPasswordWOToken(user, password)
if err != nil {
return nil, trace.Wrap(err)
}
mfaDev, err := s.checkOTP(user, otpToken)
if err != nil {
return nil, trace.Wrap(err)
}
return &checkPasswordResult{mfaDev: mfaDev}, nil
}
// checkOTP checks if the OTP token is valid.
func (s *Server) checkOTP(user string, otpToken string) (*types.MFADevice, error) {
// get the previously used token to mitigate token replay attacks
usedToken, err := s.GetUsedTOTPToken(user)
if err != nil {
return nil, trace.Wrap(err)
}
// we use a constant time compare function to mitigate timing attacks
if subtle.ConstantTimeCompare([]byte(otpToken), []byte(usedToken)) == 1 {
return nil, trace.BadParameter("previously used totp token")
}
ctx := context.TODO()
devs, err := s.Services.GetMFADevices(ctx, user, true)
if err != nil {
return nil, trace.Wrap(err)
}
for _, dev := range devs {
totpDev := dev.GetTotp()
if totpDev == nil {
continue
}
if err := s.checkTOTP(ctx, user, otpToken, dev); err != nil {
log.WithError(err).Errorf("Using TOTP device %q", dev.GetName())
continue
}
return dev, nil
}
return nil, trace.AccessDenied("invalid totp token")
}
// checkTOTP checks if the TOTP token is valid.
func (s *Server) checkTOTP(ctx context.Context, user, otpToken string, dev *types.MFADevice) error {
if dev.GetTotp() == nil {
return trace.BadParameter("checkTOTP called with non-TOTP MFADevice %T", dev.Device)
}
// we use totp.ValidateCustom over totp.Validate so we can use
// a fake clock in tests to get reliable results
valid, err := totp.ValidateCustom(otpToken, dev.GetTotp().Key, s.clock.Now(), totp.ValidateOpts{
Period: teleport.TOTPValidityPeriod,
Skew: teleport.TOTPSkew,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
if err != nil {
return trace.AccessDenied("failed to validate TOTP code: %v", err)
}
if !valid {
return trace.AccessDenied("invalid one time token, please check if the token has expired and try again")
}
// if we have a valid token, update the previously used token
if err := s.UpsertUsedTOTPToken(user, otpToken); err != nil {
return trace.Wrap(err)
}
// Update LastUsed timestamp on the device.
dev.LastUsed = s.clock.Now()
if err := s.UpsertMFADevice(ctx, user, dev); err != nil {
return trace.Wrap(err)
}
return nil
}
func (s *Server) changeUserAuthentication(ctx context.Context, req *proto.ChangeUserAuthenticationRequest) (types.User, error) {
// Get cluster configuration and check if local auth is allowed.
authPref, err := s.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if !authPref.GetAllowLocalAuth() {
return nil, trace.AccessDenied(noLocalAuth)
}
reqPasswordless := len(req.GetNewPassword()) == 0 && authPref.GetAllowPasswordless()
switch {
case reqPasswordless:
if req.GetNewMFARegisterResponse() == nil || req.NewMFARegisterResponse.GetWebauthn() == nil {
return nil, trace.BadParameter("passwordless: missing webauthn credentials")
}
default:
if err := services.VerifyPassword(req.GetNewPassword()); err != nil {
return nil, trace.Wrap(err)
}
}
// Check if token exists.
token, err := s.getResetPasswordToken(ctx, req.TokenID)
if err != nil {
return nil, trace.Wrap(err)
}
if token.Expiry().Before(s.clock.Now().UTC()) {
return nil, trace.BadParameter("expired token")
}
err = s.changeUserSecondFactor(ctx, req, token)
if err != nil {
return nil, trace.Wrap(err)
}
username := token.GetUser()
// Delete this token first to minimize the chances
// of partially updated user with still valid token.
err = s.deleteUserTokens(ctx, username)
if err != nil {
return nil, trace.Wrap(err)
}
if !reqPasswordless {
if err := s.UpsertPassword(username, req.GetNewPassword()); err != nil {
return nil, trace.Wrap(err)
}
}
user, err := s.GetUser(username, false)
if err != nil {
return nil, trace.Wrap(err)
}
return user, nil
}
func (s *Server) changeUserSecondFactor(ctx context.Context, req *proto.ChangeUserAuthenticationRequest, token types.UserToken) error {
username := token.GetUser()
cap, err := s.GetAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}
switch sf := cap.GetSecondFactor(); {
case sf == constants.SecondFactorOff:
return nil
case req.GetNewMFARegisterResponse() == nil && sf == constants.SecondFactorOptional:
// Optional second factor does not enforce users to add a MFA device.
// No need to check if a user already has registered devices since we expect
// users to have no devices at this point.
//
// The ChangeUserAuthenticationRequest is made with a reset or invite token
// where a reset token would've reset the users' MFA devices, and an invite
// token is a new user with no devices.
return nil
case req.GetNewMFARegisterResponse() == nil:
return trace.BadParameter("no second factor sent during user %q password reset", username)
}
deviceName := req.GetNewDeviceName()
// Using default values here is safe since we don't expect users to have
// any devices at this point.
if deviceName == "" {
switch {
case req.GetNewMFARegisterResponse().GetTOTP() != nil:
deviceName = "otp"
case req.GetNewMFARegisterResponse().GetWebauthn() != nil:
deviceName = "webauthn"
default:
// Fallback to something reasonable while letting verifyMFARespAndAddDevice
// worry about the "unknown" response type.
deviceName = "mfa"
log.Warnf("Unexpected MFA register response type, setting device name to %q: %T", deviceName, req.GetNewMFARegisterResponse().Response)
}
}
deviceUsage := proto.DeviceUsage_DEVICE_USAGE_MFA
if len(req.GetNewPassword()) == 0 {
deviceUsage = proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS
}
_, err = s.verifyMFARespAndAddDevice(ctx, &newMFADeviceFields{
username: token.GetUser(),
newDeviceName: deviceName,
tokenID: token.GetName(),
deviceResp: req.GetNewMFARegisterResponse(),
deviceUsage: deviceUsage,
})
return trace.Wrap(err)
}