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 }