From 5caee887dec196cdff8b7b0586dce8b9851e9ea0 Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 1 Jun 2026 12:22:49 +0300 Subject: [PATCH] fix: ensure no oidc code reuse --- internal/controller/oidc_controller.go | 15 ++++++++++++ internal/service/oidc_service.go | 33 ++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index bf6d1f2f..d84bf9bf 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -327,6 +327,18 @@ func (controller *OIDCController) Token(c *gin.Context) { entry, ok := controller.oidc.GetCodeEntry(controller.oidc.Hash(req.Code), client.ClientID) if !ok { + // ensure no code reuse + usedCodeSub, ok := controller.oidc.IsCodeUsed(controller.oidc.Hash(req.Code)) + + if ok { + controller.log.App.Warn().Msg("Code reuse detected") + controller.oidc.DeleteSessionBySub(c, usedCodeSub) + c.JSON(400, gin.H{ + "error": "invalid_grant", + }) + return + } + controller.log.App.Warn().Msg("Code not found") c.JSON(400, gin.H{ "error": "invalid_grant", @@ -334,6 +346,9 @@ func (controller *OIDCController) Token(c *gin.Context) { return } + // mark code as used to prevent reuse + controller.oidc.MarkCodeAsUsed(controller.oidc.Hash(req.Code), entry.Userinfo.Sub) + if entry.RedirectURI != req.RedirectURI { controller.log.App.Warn().Msg("Redirect URI does not match") c.JSON(400, gin.H{ diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index 33826665..235877f9 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -126,6 +126,10 @@ type AuthorizeCodeEntry struct { Userinfo UserinfoResponse } +type UsedCodeEntry struct { + Sub string +} + type OIDCService struct { log *logger.Logger config model.Config @@ -138,7 +142,8 @@ type OIDCService struct { issuer string caches struct { - code *CacheStore[AuthorizeCodeEntry] + code *CacheStore[AuthorizeCodeEntry] + usedCode *CacheStore[UsedCodeEntry] } } @@ -305,7 +310,9 @@ func NewOIDCService( // Create caches codeCash := NewCacheStore[AuthorizeCodeEntry](256) + usedCode := NewCacheStore[UsedCodeEntry](256) service.caches.code = codeCash + service.caches.usedCode = usedCode // Start cache cleanup routine dg.Go(func(ctx context.Context) { @@ -316,6 +323,7 @@ func NewOIDCService( select { case <-ticker.C: service.caches.code.Sweep() + service.caches.usedCode.Sweep() case <-ctx.Done(): return } @@ -406,7 +414,7 @@ func (service *OIDCService) CreateCode(req AuthorizeRequest, userContext model.U } // Store the code in the cache - service.caches.code.Set(entry.CodeHash, entry, 10*time.Minute) + service.caches.code.Set(entry.CodeHash, entry, 1*time.Minute) return code } @@ -817,3 +825,24 @@ func (service *OIDCService) hashAndEncodePKCE(codeVerifier string) string { func (service *OIDCService) CreateSub(userContext model.UserContext, clientId string) string { return utils.GenerateUUID(fmt.Sprintf("%s:%s", userContext.GetUsername(), clientId)) } + +func (service *OIDCService) IsCodeUsed(codeHash string) (string, bool) { + entry, ok := service.caches.usedCode.Get(codeHash) + + if !ok { + return "", false + } + + return entry.Sub, true +} + +func (service *OIDCService) MarkCodeAsUsed(codeHash string, sub string) { + entry := UsedCodeEntry{ + Sub: sub, + } + service.caches.usedCode.Set(codeHash, entry, 2*time.Minute) +} + +func (service *OIDCService) DeleteSessionBySub(ctx context.Context, sub string) error { + return service.queries.DeleteOIDCSessionBySub(ctx, sub) +}