Add API Token Cache (#16547)

One of the issues holding back performance of the API is the problem of hashing.
Whilst banning BASIC authentication with passwords will help, the API Token scheme
still requires a PBKDF2 hash - which means that heavy API use (using Tokens) can
still cause enormous numbers of hash computations.

A slight solution to this whilst we consider moving to using JWT based tokens and/or
a session orientated solution is to simply cache the successful tokens. This has some
security issues but this should be balanced by the security issues of load from
hashing.

Related #14668

Signed-off-by: Andrew Thornton <art27@cantab.net>

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
tokarchuk/v1.17
zeripath 3 years ago committed by GitHub
parent 274aeb3a9e
commit e0853d4a21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      custom/conf/app.example.ini
  2. 1
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  3. 10
      models/models.go
  4. 41
      models/token.go
  5. 2
      modules/setting/setting.go

@ -378,6 +378,10 @@ INTERNAL_TOKEN=
;; ;;
;; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed ;; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed
;PASSWORD_CHECK_PWN = false ;PASSWORD_CHECK_PWN = false
;;
;; Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations.
;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
;SUCCESSFUL_TOKENS_CACHE_SIZE = 20
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

@ -441,6 +441,7 @@ relation to port exhaustion.
- spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~`` - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~``
- off - do not check password complexity - off - do not check password complexity
- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed. - `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed.
- `SUCCESSFUL_TOKENS_CACHE_SIZE`: **20**: Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
## OpenID (`openid`) ## OpenID (`openid`)

@ -17,6 +17,7 @@ import (
// Needed for the MySQL driver // Needed for the MySQL driver
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
lru "github.com/hashicorp/golang-lru"
"xorm.io/xorm" "xorm.io/xorm"
"xorm.io/xorm/names" "xorm.io/xorm/names"
"xorm.io/xorm/schemas" "xorm.io/xorm/schemas"
@ -234,6 +235,15 @@ func NewEngine(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err e
return fmt.Errorf("sync database struct error: %v", err) return fmt.Errorf("sync database struct error: %v", err)
} }
if setting.SuccessfulTokensCacheSize > 0 {
successfulAccessTokenCache, err = lru.New(setting.SuccessfulTokensCacheSize)
if err != nil {
return fmt.Errorf("unable to allocate AccessToken cache: %v", err)
}
} else {
successfulAccessTokenCache = nil
}
return nil return nil
} }

@ -14,8 +14,11 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
gouuid "github.com/google/uuid" gouuid "github.com/google/uuid"
lru "github.com/hashicorp/golang-lru"
) )
var successfulAccessTokenCache *lru.Cache
// AccessToken represents a personal access token. // AccessToken represents a personal access token.
type AccessToken struct { type AccessToken struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@ -52,6 +55,21 @@ func NewAccessToken(t *AccessToken) error {
return err return err
} }
func getAccessTokenIDFromCache(token string) int64 {
if successfulAccessTokenCache == nil {
return 0
}
tInterface, ok := successfulAccessTokenCache.Get(token)
if !ok {
return 0
}
t, ok := tInterface.(int64)
if !ok {
return 0
}
return t
}
// GetAccessTokenBySHA returns access token by given token value // GetAccessTokenBySHA returns access token by given token value
func GetAccessTokenBySHA(token string) (*AccessToken, error) { func GetAccessTokenBySHA(token string) (*AccessToken, error) {
if token == "" { if token == "" {
@ -66,17 +84,38 @@ func GetAccessTokenBySHA(token string) (*AccessToken, error) {
return nil, ErrAccessTokenNotExist{token} return nil, ErrAccessTokenNotExist{token}
} }
} }
var tokens []AccessToken
lastEight := token[len(token)-8:] lastEight := token[len(token)-8:]
if id := getAccessTokenIDFromCache(token); id > 0 {
token := &AccessToken{
TokenLastEight: lastEight,
}
// Re-get the token from the db in case it has been deleted in the intervening period
has, err := x.ID(id).Get(token)
if err != nil {
return nil, err
}
if has {
return token, nil
}
successfulAccessTokenCache.Remove(token)
}
var tokens []AccessToken
err := x.Table(&AccessToken{}).Where("token_last_eight = ?", lastEight).Find(&tokens) err := x.Table(&AccessToken{}).Where("token_last_eight = ?", lastEight).Find(&tokens)
if err != nil { if err != nil {
return nil, err return nil, err
} else if len(tokens) == 0 { } else if len(tokens) == 0 {
return nil, ErrAccessTokenNotExist{token} return nil, ErrAccessTokenNotExist{token}
} }
for _, t := range tokens { for _, t := range tokens {
tempHash := hashToken(token, t.TokenSalt) tempHash := hashToken(token, t.TokenSalt)
if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 { if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 {
if successfulAccessTokenCache != nil {
successfulAccessTokenCache.Add(token, t.ID)
}
return &t, nil return &t, nil
} }
} }

@ -189,6 +189,7 @@ var (
PasswordComplexity []string PasswordComplexity []string
PasswordHashAlgo string PasswordHashAlgo string
PasswordCheckPwn bool PasswordCheckPwn bool
SuccessfulTokensCacheSize int
// UI settings // UI settings
UI = struct { UI = struct {
@ -840,6 +841,7 @@ func NewContext() {
PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2") PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2")
CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true) CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true)
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
InternalToken = loadInternalToken(sec) InternalToken = loadInternalToken(sec)

Loading…
Cancel
Save