Browse Source

fix: session cookie set by frontend JS (same origin), not backend (cross-domain)

Backend reverts to only setting refresh_token (httpOnly).
Frontend sets/clears session=1 cookie via document.cookie on the app domain.
hasSessionCookie() now correctly reads cookie on the same origin.
neyrogovnarik 1 month ago
parent
commit
cb8a501f5e

+ 0 - 9
backend/internal/handlers/auth.go

@@ -168,15 +168,6 @@ func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter, r *http.Req
 		SameSite: sameSite,
 		MaxAge:   -1,
 	})
-	http.SetCookie(w, &http.Cookie{
-		Name:     "session",
-		Value:    "",
-		Path:     "/",
-		HttpOnly: false,
-		Secure:   secure,
-		SameSite: sameSite,
-		MaxAge:   -1,
-	})
 }
 
 func validationMessage(tag, param string) string {

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

@@ -22,25 +22,14 @@ 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:   secure,
+		Secure:   isSecure(r),
 		SameSite: http.SameSiteLaxMode,
-		MaxAge:   maxAge,
-	})
-	http.SetCookie(w, &http.Cookie{
-		Name:     "session",
-		Value:    "1",
-		Path:     "/",
-		HttpOnly: false,
-		Secure:   secure,
-		SameSite: http.SameSiteLaxMode,
-		MaxAge:   maxAge,
+		MaxAge:   int((30 * 24 * time.Hour).Seconds()),
 	})
 }
 

+ 5 - 1
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, hasSessionCookie } from '@/lib/api'
+import { api, setAccessToken, refreshAccessToken, hasSessionCookie, setSessionCookie, clearSessionCookie } from '@/lib/api'
 
 /**
  * Состояние аутентификации, доступное через контекст
@@ -44,6 +44,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
       const res = await api.get<User>('/auth/me')
       setUser(res)
     } catch {
+      clearSessionCookie()
       setAccessToken(null)
       setUser(null)
     }
@@ -57,12 +58,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
     const res = await api.post<{ user: User; access_token: string }>('/auth/login', { email, password })
     setAccessToken(res.access_token)
     setUser(res.user)
+    setSessionCookie()
   }, [])
 
   const register = useCallback(async (email: string, password: string, role: string, name?: string) => {
     const res = await api.post<{ user: User; access_token: string }>('/auth/register', { email, password, role, name })
     setAccessToken(res.access_token)
     setUser(res.user)
+    setSessionCookie()
   }, [])
 
   const logout = useCallback(async () => {
@@ -72,6 +75,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
     }
     setAccessToken(null)
     setUser(null)
+    clearSessionCookie()
   }, [])
 
   return (

+ 11 - 1
frontend/src/lib/api.ts

@@ -13,12 +13,22 @@ export function getAccessToken(): string | null {
   return accessToken
 }
 
-/** Проверяет наличие session-куки (не-httpOnly индикатор, что refresh-токен есть на сервере) */
+const SESSION_COOKIE_AGE = 30 * 24 * 60 * 60 // 30 дней
+
+/** Проверяет наличие session-куки (индикатор, что refresh-токен есть на сервере) */
 export function hasSessionCookie(): boolean {
   if (typeof document === 'undefined') return false
   return document.cookie.split('; ').some(c => c.startsWith('session='))
 }
 
+export function setSessionCookie(): void {
+  document.cookie = `session=1; path=/; max-age=${SESSION_COOKIE_AGE}; SameSite=Lax`
+}
+
+export function clearSessionCookie(): void {
+  document.cookie = 'session=; path=/; max-age=0'
+}
+
 /**
  * Обновляет access-токен через cookie refresh-токена.
  * Гарантирует единственный одновременный запрос на обновление.