github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/mw_basic_auth.go (about)

     1  package gateway
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"errors"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	cache "github.com/pmylund/go-cache"
    13  	"github.com/sirupsen/logrus"
    14  	"golang.org/x/crypto/bcrypt"
    15  	"golang.org/x/sync/singleflight"
    16  
    17  	"github.com/TykTechnologies/murmur3"
    18  	"github.com/TykTechnologies/tyk/apidef"
    19  	"github.com/TykTechnologies/tyk/config"
    20  	"github.com/TykTechnologies/tyk/headers"
    21  	"github.com/TykTechnologies/tyk/regexp"
    22  	"github.com/TykTechnologies/tyk/storage"
    23  	"github.com/TykTechnologies/tyk/user"
    24  )
    25  
    26  const defaultBasicAuthTTL = time.Duration(60) * time.Second
    27  
    28  var basicAuthCache = cache.New(60*time.Second, 60*time.Minute)
    29  
    30  var cacheGroup singleflight.Group
    31  
    32  // BasicAuthKeyIsValid uses a username instead of
    33  type BasicAuthKeyIsValid struct {
    34  	BaseMiddleware
    35  
    36  	bodyUserRegexp     *regexp.Regexp
    37  	bodyPasswordRegexp *regexp.Regexp
    38  }
    39  
    40  func (k *BasicAuthKeyIsValid) Name() string {
    41  	return "BasicAuthKeyIsValid"
    42  }
    43  
    44  // EnabledForSpec checks if UseBasicAuth is set in the API definition.
    45  func (k *BasicAuthKeyIsValid) EnabledForSpec() bool {
    46  	if !k.Spec.UseBasicAuth {
    47  		return false
    48  	}
    49  
    50  	var err error
    51  
    52  	if k.Spec.BasicAuth.ExtractFromBody {
    53  		if k.Spec.BasicAuth.BodyUserRegexp == "" || k.Spec.BasicAuth.BodyPasswordRegexp == "" {
    54  			k.Logger().Error("Basic Auth configured to extract credentials from body, but regexps are empty")
    55  			return false
    56  		}
    57  
    58  		k.bodyUserRegexp, err = regexp.Compile(k.Spec.BasicAuth.BodyUserRegexp)
    59  		if err != nil {
    60  			k.Logger().WithError(err).Error("Invalid user body regexp")
    61  			return false
    62  		}
    63  
    64  		k.bodyPasswordRegexp, err = regexp.Compile(k.Spec.BasicAuth.BodyPasswordRegexp)
    65  		if err != nil {
    66  			k.Logger().WithError(err).Error("Invalid user password regexp")
    67  			return false
    68  		}
    69  	}
    70  
    71  	return true
    72  }
    73  
    74  // requestForBasicAuth sends error code and message along with WWW-Authenticate header to client.
    75  func (k *BasicAuthKeyIsValid) requestForBasicAuth(w http.ResponseWriter, msg string) (error, int) {
    76  	authReply := "Basic realm=\"" + k.Spec.Name + "\""
    77  
    78  	w.Header().Add(headers.WWWAuthenticate, authReply)
    79  	return errors.New(msg), http.StatusUnauthorized
    80  }
    81  
    82  // getAuthType overrides BaseMiddleware.getAuthType.
    83  func (k *BasicAuthKeyIsValid) getAuthType() string {
    84  	return basicType
    85  }
    86  
    87  func (k *BasicAuthKeyIsValid) basicAuthHeaderCredentials(w http.ResponseWriter, r *http.Request) (username, password string, err error, code int) {
    88  	token, _ := k.getAuthToken(k.getAuthType(), r)
    89  	logger := k.Logger().WithField("key", obfuscateKey(token))
    90  	if token == "" {
    91  		// No header value, fail
    92  		err, code = k.requestForBasicAuth(w, "Authorization field missing")
    93  		return
    94  	}
    95  
    96  	bits := strings.Split(token, " ")
    97  	if len(bits) != 2 {
    98  		// Header malformed
    99  		logger.Info("Attempted access with malformed header, header not in basic auth format.")
   100  
   101  		err, code = errors.New("Attempted access with malformed header, header not in basic auth format"), http.StatusBadRequest
   102  		return
   103  	}
   104  
   105  	// Decode the username:password string
   106  	authvaluesStr, err := base64.StdEncoding.DecodeString(bits[1])
   107  	if err != nil {
   108  		logger.Info("Base64 Decoding failed of basic auth data: ", err)
   109  
   110  		err, code = errors.New("Attempted access with malformed header, auth data not encoded correctly"), http.StatusBadRequest
   111  		return
   112  	}
   113  
   114  	authValues := strings.Split(string(authvaluesStr), ":")
   115  	if len(authValues) != 2 {
   116  		// Header malformed
   117  		logger.Info("Attempted access with malformed header, values not in basic auth format.")
   118  
   119  		err, code = errors.New("Attempted access with malformed header, values not in basic auth format"), http.StatusBadRequest
   120  		return
   121  	}
   122  
   123  	username, password = authValues[0], authValues[1]
   124  	return
   125  }
   126  
   127  func (k *BasicAuthKeyIsValid) basicAuthBodyCredentials(w http.ResponseWriter, r *http.Request) (username, password string, err error, code int) {
   128  	body, _ := ioutil.ReadAll(r.Body)
   129  	r.Body = ioutil.NopCloser(bytes.NewReader(body))
   130  
   131  	userMatch := k.bodyUserRegexp.FindAllSubmatch(body, 1)
   132  	if len(userMatch) == 0 {
   133  		err, code = errors.New("Body do not contain username"), http.StatusBadRequest
   134  		return
   135  	}
   136  
   137  	if len(userMatch[0]) < 2 {
   138  		err, code = errors.New("username should be inside regexp match group"), http.StatusBadRequest
   139  		return
   140  	}
   141  
   142  	passMatch := k.bodyPasswordRegexp.FindAllSubmatch(body, 1)
   143  
   144  	if len(passMatch) == 0 {
   145  		err, code = errors.New("Body do not contain password"), http.StatusBadRequest
   146  		return
   147  	}
   148  
   149  	if len(passMatch[0]) < 2 {
   150  		err, code = errors.New("password should be inside regexp match group"), http.StatusBadRequest
   151  		return
   152  	}
   153  
   154  	username, password = string(userMatch[0][1]), string(passMatch[0][1])
   155  
   156  	return username, password, nil, 0
   157  }
   158  
   159  // ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail
   160  func (k *BasicAuthKeyIsValid) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
   161  	if ctxGetRequestStatus(r) == StatusOkAndIgnore {
   162  		return nil, http.StatusOK
   163  	}
   164  
   165  	username, password, err, code := k.basicAuthHeaderCredentials(w, r)
   166  	token := r.Header.Get(headers.Authorization)
   167  	if err != nil {
   168  		if k.Spec.BasicAuth.ExtractFromBody {
   169  			w.Header().Del(headers.WWWAuthenticate)
   170  			username, password, err, code = k.basicAuthBodyCredentials(w, r)
   171  		} else {
   172  			k.Logger().Warn("Attempted access with malformed header, no auth header found.")
   173  		}
   174  
   175  		if err != nil {
   176  			return err, code
   177  		}
   178  	}
   179  
   180  	// Check if API key valid
   181  	keyName := username
   182  	logger := k.Logger().WithField("key", obfuscateKey(keyName))
   183  	session, keyExists := k.CheckSessionAndIdentityForValidKey(&keyName, r)
   184  	if !keyExists {
   185  		if config.Global().HashKeyFunction == "" {
   186  			logger.Warning("Attempted access with non-existent user.")
   187  			return k.handleAuthFail(w, r, token)
   188  		} else { // check for key with legacy format "org_id" + "user_name"
   189  			logger.Info("Could not find user, falling back to legacy format key.")
   190  			legacyKeyName := strings.TrimPrefix(username, k.Spec.OrgID)
   191  			keyName, _ = storage.GenerateToken(k.Spec.OrgID, legacyKeyName, "")
   192  			session, keyExists = k.CheckSessionAndIdentityForValidKey(&keyName, r)
   193  			if !keyExists {
   194  				logger.Warning("Attempted access with non-existent user.")
   195  				return k.handleAuthFail(w, r, token)
   196  			}
   197  		}
   198  	}
   199  
   200  	switch session.BasicAuthData.Hash {
   201  	case user.HashBCrypt:
   202  		if err := k.compareHashAndPassword(session.BasicAuthData.Password, password, logger); err != nil {
   203  			logger.Warn("Attempted access with existing user, failed password check.")
   204  			return k.handleAuthFail(w, r, token)
   205  		}
   206  	case user.HashPlainText:
   207  		if session.BasicAuthData.Password != password {
   208  			logger.Warn("Attempted access with existing user, failed password check.")
   209  			return k.handleAuthFail(w, r, token)
   210  		}
   211  	}
   212  
   213  	// Set session state on context, we will need it later
   214  	switch k.Spec.BaseIdentityProvidedBy {
   215  	case apidef.BasicAuthUser, apidef.UnsetAuth:
   216  		ctxSetSession(r, &session, keyName, false)
   217  	}
   218  
   219  	return nil, http.StatusOK
   220  }
   221  
   222  func (k *BasicAuthKeyIsValid) handleAuthFail(w http.ResponseWriter, r *http.Request, token string) (error, int) {
   223  
   224  	// Fire Authfailed Event
   225  	AuthFailed(k, r, token)
   226  
   227  	// Report in health check
   228  	reportHealthValue(k.Spec, KeyFailure, "-1")
   229  
   230  	return k.requestForBasicAuth(w, "User not authorised")
   231  }
   232  
   233  func (k *BasicAuthKeyIsValid) doBcryptWithCache(cacheDuration time.Duration, hashedPassword []byte, password []byte) error {
   234  	if err := bcrypt.CompareHashAndPassword(hashedPassword, password); err != nil {
   235  		return err
   236  	}
   237  
   238  	hasher := murmur3.New64()
   239  	hasher.Write(password)
   240  	basicAuthCache.Set(string(hashedPassword), string(hasher.Sum(nil)), cacheDuration)
   241  
   242  	return nil
   243  }
   244  
   245  func (k *BasicAuthKeyIsValid) compareHashAndPassword(hash string, password string, logEntry *logrus.Entry) error {
   246  	passwordBytes := []byte(password)
   247  	hashBytes := []byte(hash)
   248  
   249  	if k.Spec.BasicAuth.DisableCaching {
   250  		logEntry.Debug("cache disabled")
   251  		return bcrypt.CompareHashAndPassword(hashBytes, passwordBytes)
   252  	}
   253  
   254  	cacheTTL := defaultBasicAuthTTL // set a default TTL, then override based on BasicAuth.CacheTTL
   255  	if k.Spec.BasicAuth.CacheTTL > 0 {
   256  		cacheTTL = time.Duration(k.Spec.BasicAuth.CacheTTL) * time.Second
   257  	}
   258  
   259  	cachedPass, inCache := basicAuthCache.Get(hash)
   260  	if !inCache {
   261  		logEntry.Debug("cache enabled: miss: bcrypt")
   262  		_, err, _ := cacheGroup.Do(hash+"."+password, func() (interface{}, error) {
   263  			return nil, k.doBcryptWithCache(cacheTTL, hashBytes, passwordBytes)
   264  		})
   265  
   266  		return err
   267  	}
   268  
   269  	hasher := murmur3.New64()
   270  	hasher.Write(passwordBytes)
   271  	if cachedPass.(string) != string(hasher.Sum(nil)) {
   272  
   273  		logEntry.Warn("cache enabled: hit: failed auth: bcrypt")
   274  		return bcrypt.CompareHashAndPassword(hashBytes, passwordBytes)
   275  	}
   276  
   277  	logEntry.Debug("cache enabled: hit: success")
   278  	return nil
   279  }