perf(auth): look up api tokens by their hash index (#6915)

Every bearer-authenticated request, including each node-sync call, loaded all
enabled api_tokens rows and compared each stored SHA-256 hash in constant time.
Tokens are stored hashed, so the database can compare hashes directly: a
timing leak there reveals only a stored hash, which cannot authenticate. The
lookup is now one indexed seek on idx_api_tokens_token, which AutoMigrate
creates from the model tag on new and existing databases; scope and expiry
are still checked afterwards.
This commit is contained in:
25f2002654
2026-10-24 08:36:49 +05:30
committed by GitHub
parent 9f0f2b3802
commit f6a233220a
4 changed files with 66 additions and 22 deletions
+2 -1
View File
@@ -21,7 +21,7 @@ func TestAutoMigrateCreatesHotPathIndexes(t *testing.T) {
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&model.ClientRecord{}, &xray.ClientTraffic{}, &model.ClientGlobalTraffic{}); err != nil {
if err := db.AutoMigrate(&model.ClientRecord{}, &xray.ClientTraffic{}, &model.ClientGlobalTraffic{}, &model.ApiToken{}); err != nil {
t.Fatalf("automigrate: %v", err)
}
@@ -33,6 +33,7 @@ func TestAutoMigrateCreatesHotPathIndexes(t *testing.T) {
{&xray.ClientTraffic{}, "idx_client_traffics_inbound"},
{&xray.ClientTraffic{}, "idx_client_traffics_renew"},
{&model.ClientGlobalTraffic{}, "idx_client_global_email"},
{&model.ApiToken{}, "idx_api_tokens_token"},
}
for _, c := range cases {
if !db.Migrator().HasIndex(c.model, c.index) {
+1 -1
View File
@@ -178,7 +178,7 @@ func IsNodeSyncApiScope(s string) bool {
type ApiToken struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"uniqueIndex;not null"`
Token string `json:"token" gorm:"not null"` // SHA-256 hash; the plaintext is shown only once at creation
Token string `json:"token" gorm:"not null;index:idx_api_tokens_token"` // SHA-256 hash; the plaintext is shown only once at creation
Enabled bool `json:"enabled" gorm:"default:true"`
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"`
Scope string `json:"scope" gorm:"not null;default:admin"`