From c24f712cb6ac4f38006abde55b61d4649235a963 Mon Sep 17 00:00:00 2001 From: Matthew Knight Date: Tue, 17 Feb 2026 16:05:33 -0800 Subject: [PATCH] Remove dummy user_id from OAuth state sessions Use a simple signed cookie for OAuth state instead of PGStore, which required a dummy user_id placeholder to satisfy the session store's save logic. Fixes #24 Co-Authored-By: Claude Opus 4.6 --- internal/handlers/public/oauth.go | 54 ++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/internal/handlers/public/oauth.go b/internal/handlers/public/oauth.go index 839f4b0..bb903fe 100644 --- a/internal/handlers/public/oauth.go +++ b/internal/handlers/public/oauth.go @@ -53,12 +53,16 @@ func (h *OAuthHandler) Login(c *gin.Context) { } state := generateState() - session, _ := h.deps.SessionStore.Get(c.Request, "oauth_state") - session.Values["state"] = state - session.Values["user_id"] = "00000000-0000-0000-0000-000000000000" // placeholder for save - if err := session.Save(c.Request, c.Writer); err != nil { - log.Error().Err(err).Msg("save oauth state error") - } + isSecure := strings.HasPrefix(h.deps.Config.BaseURL, "https") + http.SetCookie(c.Writer, &http.Cookie{ + Name: "oauth_state", + Value: state, + Path: "/", + MaxAge: 600, // 10 minutes + HttpOnly: true, + Secure: isSecure, + SameSite: http.SameSiteLaxMode, + }) url := provider.Config.AuthCodeURL(state, oauth2.AccessTypeOffline) c.Redirect(http.StatusTemporaryRedirect, url) @@ -73,12 +77,17 @@ func (h *OAuthHandler) Callback(c *gin.Context) { } // Verify state - session, _ := h.deps.SessionStore.Get(c.Request, "oauth_state") - expectedState, _ := session.Values["state"].(string) - if c.Query("state") != expectedState { + stateCookie, err := c.Request.Cookie("oauth_state") + if err != nil || c.Query("state") != stateCookie.Value { c.String(http.StatusBadRequest, "Invalid state parameter") return } + // Clear the state cookie + http.SetCookie(c.Writer, &http.Cookie{ + Name: "oauth_state", + Path: "/", + MaxAge: -1, + }) code := c.Query("code") token, err := provider.Config.Exchange(c.Request.Context(), code) @@ -137,12 +146,16 @@ func (h *OAuthHandler) appleLogin(c *gin.Context) { } state := generateState() - session, _ := h.deps.SessionStore.Get(c.Request, "oauth_state") - session.Values["state"] = state - session.Values["user_id"] = "00000000-0000-0000-0000-000000000000" - if err := session.Save(c.Request, c.Writer); err != nil { - log.Error().Err(err).Msg("save oauth state error") - } + isSecure := strings.HasPrefix(h.deps.Config.BaseURL, "https") + http.SetCookie(c.Writer, &http.Cookie{ + Name: "oauth_state", + Value: state, + Path: "/", + MaxAge: 600, // 10 minutes + HttpOnly: true, + Secure: isSecure, + SameSite: http.SameSiteNoneMode, // Apple uses form_post cross-origin + }) url := appleProvider.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, auth.AppleAuthCodeOption()) c.Redirect(http.StatusTemporaryRedirect, url) @@ -164,12 +177,17 @@ func (h *OAuthHandler) AppleCallback(c *gin.Context) { code := c.PostForm("code") state := c.PostForm("state") - session, _ := h.deps.SessionStore.Get(c.Request, "oauth_state") - expectedState, _ := session.Values["state"].(string) - if state != expectedState { + stateCookie, err := c.Request.Cookie("oauth_state") + if err != nil || state != stateCookie.Value { c.String(http.StatusBadRequest, "Invalid state parameter") return } + // Clear the state cookie + http.SetCookie(c.Writer, &http.Cookie{ + Name: "oauth_state", + Path: "/", + MaxAge: -1, + }) token, err := appleProvider.ExchangeCode(c.Request.Context(), code) if err != nil {