mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +01:00 
			
		
		
		
	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>
This commit is contained in:
		@@ -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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user