11package middleware
22
33import (
4+ "context"
45 "encoding/base64"
6+ "encoding/json"
57 "errors"
68 "net/http"
79 "strings"
10+ "time"
811
912 "github.com/gin-gonic/gin"
13+ "github.com/redis/go-redis/v9"
1014 "go.opentelemetry.io/otel"
1115 "go.opentelemetry.io/otel/attribute"
1216 "go.opentelemetry.io/otel/trace"
@@ -61,10 +65,16 @@ func GetUserKEKBase64IfEncrypted(c *gin.Context) string {
6165 return base64 .StdEncoding .EncodeToString (kek )
6266}
6367
68+ const (
69+ projectAuthCachePrefix = "project:auth:"
70+ projectAuthCacheTTL = 5 * time .Minute
71+ )
72+
6473// ProjectAuth returns a middleware that authenticates requests using project bearer tokens.
6574// Token format: sk-ac-{auth_secret}.{encrypted_master_key}
6675// Derives a KEK and stores it in context for downstream encryption operations.
67- func ProjectAuth (cfg * config.Config , db * gorm.DB ) gin.HandlerFunc {
76+ // It caches project lookups in Redis to avoid hitting the database on every request.
77+ func ProjectAuth (cfg * config.Config , db * gorm.DB , rdb * redis.Client ) gin.HandlerFunc {
6878 return func (c * gin.Context ) {
6979 // Create auth span without propagating context to avoid nested span hierarchy
7080 authCtx , authSpan := otel .Tracer ("middleware" ).Start (
@@ -93,8 +103,8 @@ func ProjectAuth(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
93103 // HMAC lookup uses auth_secret (both formats)
94104 lookup := tokens .HMAC256Hex (cfg .Root .SecretPepper , parsed .AuthSecret )
95105
96- var project model. Project
97- if err := db . WithContext ( authCtx ). Where ( & model. Project { SecretKeyHMAC : lookup }). First ( & project ). Error ; err != nil {
106+ project , err := lookupProject ( authCtx , db , rdb , lookup )
107+ if err != nil {
98108 if errors .Is (err , gorm .ErrRecordNotFound ) {
99109 authSpan .SetAttributes (attribute .Bool ("authenticated" , false ))
100110 authSpan .End ()
@@ -135,7 +145,7 @@ func ProjectAuth(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
135145 )
136146 authSpan .End ()
137147
138- c .Set ("project" , & project )
148+ c .Set ("project" , project )
139149 SetWideEventField (c , "project_id" , project .ID .String ())
140150
141151 // Derive KEK: unwrap master_key from token if present.
@@ -157,3 +167,35 @@ func ProjectAuth(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
157167 c .Next ()
158168 }
159169}
170+
171+ // lookupProject tries Redis cache first, falls back to DB on miss or Redis error.
172+ func lookupProject (ctx context.Context , db * gorm.DB , rdb * redis.Client , hmac string ) (* model.Project , error ) {
173+ cacheKey := projectAuthCachePrefix + hmac
174+
175+ // Try Redis first
176+ if rdb != nil {
177+ data , err := rdb .Get (ctx , cacheKey ).Bytes ()
178+ if err == nil {
179+ var project model.Project
180+ if json .Unmarshal (data , & project ) == nil {
181+ return & project , nil
182+ }
183+ }
184+ // On redis.Nil or any other error, fall through to DB
185+ }
186+
187+ // DB lookup
188+ var project model.Project
189+ if err := db .WithContext (ctx ).Where (& model.Project {SecretKeyHMAC : hmac }).First (& project ).Error ; err != nil {
190+ return nil , err
191+ }
192+
193+ // Write-back to Redis (best-effort, don't block on failure)
194+ if rdb != nil {
195+ if data , err := json .Marshal (& project ); err == nil {
196+ _ = rdb .Set (ctx , cacheKey , data , projectAuthCacheTTL ).Err ()
197+ }
198+ }
199+
200+ return & project , nil
201+ }
0 commit comments