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:
parent
274aeb3a9e
commit
e0853d4a21
5 changed files with 57 additions and 1 deletions
|
@ -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 a new issue