From 0615a361b4431c54919b1ab9cc2f529c10184112 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 25 Jul 2021 12:53:10 +0100 Subject: [PATCH] Add API Token Cache 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 --- 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 6ea31586a74d5..165cf61a7fa7a 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 41dd0b702eb0b..559acdeead383 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -440,6 +440,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 610933d3270bd..24a30fd2f3cd3 100644 --- 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 357afe44a7c0b..353c2dcd155b4 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 593677344fd94..d584ed3d4d10a 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)