瀏覽代碼

fix: suppress 400 on /auth/refresh for guests via session cookie

Backend: set non-httpOnly session=1 cookie alongside refresh_token,
clear on logout/revoke.
Frontend: hasSessionCookie() checks document.cookie before calling
refreshAccessToken(). Replaces unreliable localStorage flag.
neyrogovnarik 1 月之前
父節點
當前提交
7052a25834
共有 4 個文件被更改,包括 36 次插入8 次删除
  1. 11 3
      backend/internal/handlers/auth.go
  2. 13 2
      backend/internal/handlers/helpers.go
  3. 6 3
      frontend/src/hooks/useAuth.tsx
  4. 6 0
      frontend/src/lib/api.ts

+ 11 - 3
backend/internal/handlers/auth.go

@@ -159,7 +159,7 @@ func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter, r *http.Req
 	if secure {
 		sameSite = http.SameSiteStrictMode
 	}
-	cookie := &http.Cookie{
+	http.SetCookie(w, &http.Cookie{
 		Name:     refreshTokenCookieName,
 		Value:    "",
 		Path:     "/",
@@ -167,8 +167,16 @@ func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter, r *http.Req
 		Secure:   secure,
 		SameSite: sameSite,
 		MaxAge:   -1,
-	}
-	http.SetCookie(w, cookie)
+	})
+	http.SetCookie(w, &http.Cookie{
+		Name:     "session",
+		Value:    "",
+		Path:     "/",
+		HttpOnly: false,
+		Secure:   secure,
+		SameSite: sameSite,
+		MaxAge:   -1,
+	})
 }
 
 func validationMessage(tag, param string) string {

+ 13 - 2
backend/internal/handlers/helpers.go

@@ -22,14 +22,25 @@ func isSecure(r *http.Request) bool {
 }
 
 func setRefreshTokenCookie(w http.ResponseWriter, token string, r *http.Request) {
+	secure := isSecure(r)
+	maxAge := int((30 * 24 * time.Hour).Seconds())
 	http.SetCookie(w, &http.Cookie{
 		Name:     refreshTokenCookieName,
 		Value:    token,
 		Path:     "/",
 		HttpOnly: true,
-		Secure:   isSecure(r),
+		Secure:   secure,
 		SameSite: http.SameSiteLaxMode,
-		MaxAge:   int((30 * 24 * time.Hour).Seconds()),
+		MaxAge:   maxAge,
+	})
+	http.SetCookie(w, &http.Cookie{
+		Name:     "session",
+		Value:    "1",
+		Path:     "/",
+		HttpOnly: false,
+		Secure:   secure,
+		SameSite: http.SameSiteLaxMode,
+		MaxAge:   maxAge,
 	})
 }
 

+ 6 - 3
frontend/src/hooks/useAuth.tsx

@@ -2,7 +2,7 @@
 
 import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
 import type { User } from '@/types'
-import { api, setAccessToken, refreshAccessToken } from '@/lib/api'
+import { api, setAccessToken, refreshAccessToken, hasSessionCookie } from '@/lib/api'
 
 /**
  * Состояние аутентификации, доступное через контекст
@@ -34,6 +34,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
   const [isLoading, setIsLoading] = useState(true)
 
   const refreshSession = useCallback(async () => {
+    if (!hasSessionCookie()) {
+      setIsLoading(false)
+      return
+    }
     try {
       const newToken = await refreshAccessToken()
       setAccessToken(newToken)
@@ -64,8 +68,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
   const logout = useCallback(async () => {
     try {
       await api.post('/auth/logout', {})
-    } catch (e) {
-      console.warn('logout failed', e)
+    } catch {
     }
     setAccessToken(null)
     setUser(null)

+ 6 - 0
frontend/src/lib/api.ts

@@ -13,6 +13,12 @@ export function getAccessToken(): string | null {
   return accessToken
 }
 
+/** Проверяет наличие session-куки (не-httpOnly индикатор, что refresh-токен есть на сервере) */
+export function hasSessionCookie(): boolean {
+  if (typeof document === 'undefined') return false
+  return document.cookie.split('; ').some(c => c.startsWith('session='))
+}
+
 /**
  * Обновляет access-токен через cookie refresh-токена.
  * Гарантирует единственный одновременный запрос на обновление.