Files
teleport/lib/auth/password.go
T
3c670d5d58 Merge Teleport V4.3 UI branch to master (#3583)
* Add monorepo

* Add reset/passwd capability for local users (#3287)

* Add UserTokens to allow password resets

* Pass context down through ChangePasswordWithToken

* Rename UserToken to ResetPasswordToken

* Add auto formatting for proto files

* Add common Marshaller interfaces to reset password token

* Allow enterprise "tctl" reuse OSS user methods (#3344)

* Pass localAuthEnabled flag to UI (#3412)

* Added LocalAuthEnabled prop to WebConfigAuthSetting struct in webconfig.go
* Added LocalAuthEnabled state as part of webCfg in  apiserver.go

* update e-refs

* Fix a regression bug after merge

* Update tctl CLI output msgs (#3442)

* Use local user client when resolving user roles

* Update webapps ref

* Add and retrieve fields from Cluster struct (#3476)

* Set Teleport versions for node, auth, proxy init heartbeat
* Add and retrieve fields NodeCount, PublicURL, AuthVersion from Clusters
* Remove debug logging to avoid log pollution when getting public_addr of proxy
* Create helper func GuessProxyHost to get the public_addr of a proxy host
* Refactor newResetPasswordToken to use GuessProxyHost and remove publicUrl func

* Remove webapps submodule

* Add webassets submodule

* Replace webapps sub-module reference with webassets

* Update webassets path in Makefile

* Update webassets

1b11b26 Simplify and clean up Makefile (#62) https://github.com/gravitational/webapps/commit/1b11b26

* Retrieve cluster details for user context (#3515)

* Let GuessProxyHost also return proxy's version
* Unit test GuessProxyHostAndVersion & GetClusterDetails

* Update webassets

4dfef4e Fix build pipeline (#66) https://github.com/gravitational/webapps/commit/4dfef4e

* Update e-ref

* Update webassets

0647568 Fix OSS redirects https://github.com/gravitational/webapps/commit/0647568

* update e-ref

* Update webassets

e0f4189 Address security audit warnings Updates  "minimist" package which is used by 7y old "optimist". https://github.com/gravitational/webapps/commit/e0f4189

* Add new attr to Session struct (#3574)

* Add fields ServerHostname and ServerAddr
* Set these fields on newSession

* Ensure webassets submodule during build

* Update e-ref

* Ensure webassets before running unit-tests

* Update E-ref

Co-authored-by: Lisa Kim <lisa@gravitational.com>
Co-authored-by: Pierre Beaucamp <pierre@gravitational.com>
Co-authored-by: Jenkins <jenkins@gravitational.io>
2020-04-15 15:35:26 -04:00

375 lines
9.8 KiB
Go

package auth
import (
"context"
"crypto/subtle"
"golang.org/x/crypto/bcrypt"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/tstranex/u2f"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
var fakePasswordHash = []byte(`$2a$10$Yy.e6BmS2SrGbBDsyDLVkOANZmvjjMR890nUGSXFJHBXWzxe7T44m`)
// ChangePasswordWithTokenRequest defines a request to change user password
type ChangePasswordWithTokenRequest struct {
// SecondFactorToken is 2nd factor token value
SecondFactorToken string `json:"second_factor_token"`
// TokenID is this token ID
TokenID string `json:"token"`
// Password is user password
Password []byte `json:"password"`
// U2FRegisterResponse is U2F register response
U2FRegisterResponse u2f.RegisterResponse `json:"u2f_register_response"`
}
// ChangePasswordWithToken changes password with token
func (s *AuthServer) ChangePasswordWithToken(ctx context.Context, req ChangePasswordWithTokenRequest) (services.WebSession, error) {
user, err := s.changePasswordWithToken(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
sess, err := s.createUserWebSession(user)
if err != nil {
return nil, trace.Wrap(err)
}
return sess, 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 *AuthServer) 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 changes user passsword
func (s *AuthServer) ChangePassword(req services.ChangePasswordReq) error {
// validate new password
err := services.VerifyPassword(req.NewPassword)
if err != nil {
return trace.Wrap(err)
}
authPreference, err := s.GetAuthPreference()
if err != nil {
return trace.Wrap(err)
}
userID := req.User
fn := func() error {
secondFactor := authPreference.GetSecondFactor()
switch secondFactor {
case teleport.OFF:
return s.CheckPasswordWOToken(userID, req.OldPassword)
case teleport.OTP:
return s.CheckPassword(userID, req.OldPassword, req.SecondFactorToken)
case teleport.U2F:
if req.U2FSignResponse == nil {
return trace.BadParameter("missing U2F sign response")
}
return s.CheckU2FSignResponse(userID, req.U2FSignResponse)
}
return trace.BadParameter("unsupported second factor method: %q", secondFactor)
}
err = s.WithUserLock(userID, fn)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(s.UpsertPassword(userID, req.NewPassword))
}
// CheckPasswordWOToken checks just password without checking OTP tokens
// used in case of SSH authentication, when token has been validated.
func (s *AuthServer) 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)
}
if trace.IsNotFound(err) {
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)
}
return nil
}
// CheckPassword checks the password and OTP token. Called by tsh or lib/web/*.
func (s *AuthServer) CheckPassword(user string, password []byte, otpToken string) error {
err := s.CheckPasswordWOToken(user, password)
if err != nil {
return trace.Wrap(err)
}
err = s.CheckOTP(user, otpToken)
return trace.Wrap(err)
}
// CheckOTP determines the type of OTP token used (for legacy HOTP support), fetches the
// appropriate type from the backend, and checks if the token is valid.
func (s *AuthServer) CheckOTP(user string, otpToken string) error {
var err error
otpType, err := s.getOTPType(user)
if err != nil {
return trace.Wrap(err)
}
switch otpType {
case teleport.HOTP:
otp, err := s.GetHOTP(user)
if err != nil {
return trace.Wrap(err)
}
// look ahead n tokens to see if we can find a matching token
if !otp.Scan(otpToken, defaults.HOTPFirstTokensRange) {
return trace.BadParameter("bad one time token")
}
// we need to upsert the hotp state again because the
// counter was incremented
if err := s.UpsertHOTP(user, otp); err != nil {
return trace.Wrap(err)
}
case teleport.TOTP:
otpSecret, err := s.GetTOTP(user)
if err != nil {
return trace.Wrap(err)
}
// get the previously used token to mitigate token replay attacks
usedToken, err := s.GetUsedTOTPToken(user)
if err != nil {
return trace.Wrap(err)
}
// we use a constant time compare function to mitigate timing attacks
if subtle.ConstantTimeCompare([]byte(otpToken), []byte(usedToken)) == 1 {
return trace.BadParameter("previously used totp token")
}
// 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, otpSecret, s.clock.Now(), totp.ValidateOpts{
Period: teleport.TOTPValidityPeriod,
Skew: teleport.TOTPSkew,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
})
if err != nil {
log.Errorf("unable to validate token: %v", err)
return trace.BadParameter("unable to validate token")
}
if !valid {
return trace.BadParameter("invalid totp token")
}
// if we have a valid token, update the previously used token
err = s.UpsertUsedTOTPToken(user, otpToken)
if err != nil {
return trace.Wrap(err)
}
}
return nil
}
// CreateSignupU2FRegisterRequest creates U2F requests
func (s *AuthServer) CreateSignupU2FRegisterRequest(tokenID string) (u2fRegisterRequest *u2f.RegisterRequest, e error) {
cap, err := s.GetAuthPreference()
if err != nil {
return nil, trace.Wrap(err)
}
universalSecondFactor, err := cap.GetU2F()
if err != nil {
return nil, trace.Wrap(err)
}
_, err = s.GetResetPasswordToken(context.TODO(), tokenID)
if err != nil {
return nil, trace.Wrap(err)
}
c, err := u2f.NewChallenge(universalSecondFactor.AppID, universalSecondFactor.Facets)
if err != nil {
return nil, trace.Wrap(err)
}
err = s.UpsertU2FRegisterChallenge(tokenID, c)
if err != nil {
return nil, trace.Wrap(err)
}
request := c.RegisterRequest()
return request, nil
}
// getOTPType returns the type of OTP token used, HOTP or TOTP.
// Deprecated: Remove this method once HOTP support has been removed from Gravity.
func (s *AuthServer) getOTPType(user string) (string, error) {
_, err := s.GetHOTP(user)
if err != nil {
if trace.IsNotFound(err) {
return teleport.TOTP, nil
}
return "", trace.Wrap(err)
}
return teleport.HOTP, nil
}
func (s *AuthServer) changePasswordWithToken(ctx context.Context, req ChangePasswordWithTokenRequest) (services.User, error) {
// Get cluster configuration and check if local auth is allowed.
clusterConfig, err := s.GetClusterConfig()
if err != nil {
return nil, trace.Wrap(err)
}
if clusterConfig.GetLocalAuth() == false {
return nil, trace.AccessDenied(noLocalAuth)
}
err = services.VerifyPassword(req.Password)
if 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(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.deleteResetPasswordTokens(ctx, username)
if err != nil {
return nil, trace.Wrap(err)
}
// Set a new password.
err = s.UpsertPassword(username, []byte(req.Password))
if 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 *AuthServer) changeUserSecondFactor(req ChangePasswordWithTokenRequest, ResetPasswordToken services.ResetPasswordToken) error {
username := ResetPasswordToken.GetUser()
cap, err := s.GetAuthPreference()
if err != nil {
return trace.Wrap(err)
}
switch cap.GetSecondFactor() {
case teleport.OFF:
return nil
case teleport.OTP, teleport.TOTP, teleport.HOTP:
secrets, err := s.Identity.GetResetPasswordTokenSecrets(context.TODO(), req.TokenID)
if err != nil {
return trace.Wrap(err)
}
// TODO: create a separate method to validate TOTP without inserting it first
err = s.UpsertTOTP(username, secrets.GetOTPKey())
if err != nil {
return trace.Wrap(err)
}
err = s.CheckOTP(username, req.SecondFactorToken)
if err != nil {
return trace.Wrap(err)
}
return nil
case teleport.U2F:
_, err = cap.GetU2F()
if err != nil {
return trace.Wrap(err)
}
challenge, err := s.GetU2FRegisterChallenge(req.TokenID)
if err != nil {
return trace.Wrap(err)
}
u2fRes := req.U2FRegisterResponse
reg, err := u2f.Register(u2fRes, *challenge, &u2f.Config{SkipAttestationVerify: true})
if err != nil {
return trace.Wrap(err)
}
err = s.UpsertU2FRegistration(username, reg)
if err != nil {
return trace.Wrap(err)
}
err = s.UpsertU2FRegistrationCounter(username, 0)
if err != nil {
return trace.Wrap(err)
}
return nil
}
return trace.BadParameter("unknown second factor type %q", cap.GetSecondFactor())
}