From e0853d4a21bd1c718f23a3cac6148f5063e4e810 Mon Sep 17 00:00:00 2001 From: zeripath Date: Tue, 17 Aug 2021 19:30:42 +0100 Subject: [PATCH] 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 Co-authored-by: Lunny Xiao --- custom/conf/app.example.ini | 4 ++ .../doc/advanced/config-cheat-sheet.en-us.md | 1 + models/models.go | 10 +++++ models/token.go | 41 ++++++++++++++++++- modules/setting/setting.go | 2 + 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 44516b5e64..95dd8073a4 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -378,6 +378,10 @@ INTERNAL_TOKEN= ;; ;; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed ;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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index e94c3ece2a..572e33af7f 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -441,6 +441,7 @@ relation to port exhaustion. - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~`` - off - do not check password complexity - `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`) diff --git a/models/models.go b/models/models.go index c843302296..4e1448241a 100755 --- a/models/models.go +++ b/models/models.go @@ -17,6 +17,7 @@ import ( // Needed for the MySQL driver _ "github.com/go-sql-driver/mysql" + lru "github.com/hashicorp/golang-lru" "xorm.io/xorm" "xorm.io/xorm/names" "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) } + 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 } diff --git a/models/token.go b/models/token.go index 8e1f91d43f..9baa763f1c 100644 --- a/models/token.go +++ b/models/token.go @@ -14,8 +14,11 @@ import ( "code.gitea.io/gitea/modules/util" gouuid "github.com/google/uuid" + lru "github.com/hashicorp/golang-lru" ) +var successfulAccessTokenCache *lru.Cache + // AccessToken represents a personal access token. type AccessToken struct { ID int64 `xorm:"pk autoincr"` @@ -52,6 +55,21 @@ func NewAccessToken(t *AccessToken) error { 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 func GetAccessTokenBySHA(token string) (*AccessToken, error) { if token == "" { @@ -66,17 +84,38 @@ func GetAccessTokenBySHA(token string) (*AccessToken, error) { return nil, ErrAccessTokenNotExist{token} } } - var tokens []AccessToken + 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) if err != nil { return nil, err } else if len(tokens) == 0 { return nil, ErrAccessTokenNotExist{token} } + for _, t := range tokens { tempHash := hashToken(token, t.TokenSalt) if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 { + if successfulAccessTokenCache != nil { + successfulAccessTokenCache.Add(token, t.ID) + } return &t, nil } } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 593677344f..d584ed3d4d 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -189,6 +189,7 @@ var ( PasswordComplexity []string PasswordHashAlgo string PasswordCheckPwn bool + SuccessfulTokensCacheSize int // UI settings UI = struct { @@ -840,6 +841,7 @@ func NewContext() { PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2") CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) + SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) InternalToken = loadInternalToken(sec)