mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
* 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.
390 lines
12 KiB
Go
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)
|
|
}
|