Skip to content

Commit e0853d4

Browse files
zeripathlunny
andauthored
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 <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 274aeb3 commit e0853d4

File tree

5 files changed

+57
-1
lines changed

5 files changed

+57
-1
lines changed

custom/conf/app.example.ini

Lines changed: 4 additions & 0 deletions
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

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

445446
## OpenID (`openid`)
446447

models/models.go

Lines changed: 10 additions & 0 deletions
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

Lines changed: 40 additions & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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)