Skip to content

Commit 0615a36

Browse files
committed
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 <art27@cantab.net>
1 parent 6a33b29 commit 0615a36

File tree

5 files changed

+57
-1
lines changed

5 files changed

+57
-1
lines changed

custom/conf/app.example.ini

+4
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,10 @@ INTERNAL_TOKEN=
378378
;;
379379
;; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed
380380
;PASSWORD_CHECK_PWN = false
381+
;;
382+
;; 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.
383+
;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
384+
;SUCCESSFUL_TOKENS_CACHE_SIZE = 20
381385

382386
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
383387
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ relation to port exhaustion.
440440
- spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~``
441441
- off - do not check password complexity
442442
- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed.
443+
- `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.
443444

444445
## OpenID (`openid`)
445446

models/models.go

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
// Needed for the MySQL driver
1919
_ "github.com/go-sql-driver/mysql"
20+
lru "github.com/hashicorp/golang-lru"
2021
"xorm.io/xorm"
2122
"xorm.io/xorm/names"
2223
"xorm.io/xorm/schemas"
@@ -234,6 +235,15 @@ func NewEngine(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err e
234235
return fmt.Errorf("sync database struct error: %v", err)
235236
}
236237

238+
if setting.SuccessfulTokensCacheSize > 0 {
239+
successfulAccessTokenCache, err = lru.New(setting.SuccessfulTokensCacheSize)
240+
if err != nil {
241+
return fmt.Errorf("unable to allocate AccessToken cache: %v", err)
242+
}
243+
} else {
244+
successfulAccessTokenCache = nil
245+
}
246+
237247
return nil
238248
}
239249

models/token.go

+40-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import (
1414
"code.gitea.io/gitea/modules/util"
1515

1616
gouuid "github.com/google/uuid"
17+
lru "github.com/hashicorp/golang-lru"
1718
)
1819

20+
var successfulAccessTokenCache *lru.Cache
21+
1922
// AccessToken represents a personal access token.
2023
type AccessToken struct {
2124
ID int64 `xorm:"pk autoincr"`
@@ -52,6 +55,21 @@ func NewAccessToken(t *AccessToken) error {
5255
return err
5356
}
5457

58+
func getAccessTokenIDFromCache(token string) int64 {
59+
if successfulAccessTokenCache == nil {
60+
return 0
61+
}
62+
tInterface, ok := successfulAccessTokenCache.Get(token)
63+
if !ok {
64+
return 0
65+
}
66+
t, ok := tInterface.(int64)
67+
if !ok {
68+
return 0
69+
}
70+
return t
71+
}
72+
5573
// GetAccessTokenBySHA returns access token by given token value
5674
func GetAccessTokenBySHA(token string) (*AccessToken, error) {
5775
if token == "" {
@@ -66,17 +84,38 @@ func GetAccessTokenBySHA(token string) (*AccessToken, error) {
6684
return nil, ErrAccessTokenNotExist{token}
6785
}
6886
}
69-
var tokens []AccessToken
87+
7088
lastEight := token[len(token)-8:]
89+
90+
if id := getAccessTokenIDFromCache(token); id > 0 {
91+
token := &AccessToken{
92+
TokenLastEight: lastEight,
93+
}
94+
// Re-get the token from the db in case it has been deleted in the intervening period
95+
has, err := x.ID(id).Get(token)
96+
if err != nil {
97+
return nil, err
98+
}
99+
if has {
100+
return token, nil
101+
}
102+
successfulAccessTokenCache.Remove(token)
103+
}
104+
105+
var tokens []AccessToken
71106
err := x.Table(&AccessToken{}).Where("token_last_eight = ?", lastEight).Find(&tokens)
72107
if err != nil {
73108
return nil, err
74109
} else if len(tokens) == 0 {
75110
return nil, ErrAccessTokenNotExist{token}
76111
}
112+
77113
for _, t := range tokens {
78114
tempHash := hashToken(token, t.TokenSalt)
79115
if subtle.ConstantTimeCompare([]byte(t.TokenHash), []byte(tempHash)) == 1 {
116+
if successfulAccessTokenCache != nil {
117+
successfulAccessTokenCache.Add(token, t.ID)
118+
}
80119
return &t, nil
81120
}
82121
}

modules/setting/setting.go

+2
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ var (
189189
PasswordComplexity []string
190190
PasswordHashAlgo string
191191
PasswordCheckPwn bool
192+
SuccessfulTokensCacheSize int
192193

193194
// UI settings
194195
UI = struct {
@@ -840,6 +841,7 @@ func NewContext() {
840841
PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2")
841842
CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true)
842843
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
844+
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
843845

844846
InternalToken = loadInternalToken(sec)
845847

0 commit comments

Comments
 (0)