mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 05:55:42 +08:00
* 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>
375 lines
9.8 KiB
Go
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())
|
|
}
|