diff --git a/handlers/google_auth.go b/handlers/google_auth.go index de7877f..af84223 100644 --- a/handlers/google_auth.go +++ b/handlers/google_auth.go @@ -603,48 +603,47 @@ func checkEmailInDB(email string) (bool, error) { func LogoutHandler(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") - if !isValidAuthHeader(authHeader) { - helper.RespondWithError(w, http.StatusUnauthorized, "Authorization header missing or invalid") - return - } + clearRefreshTokenCookie(w) + clearCSRFCookie(w) - tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, bearerPrefix)) - if tokenString == "" { - helper.RespondWithError(w, http.StatusUnauthorized, "Token is missing or empty") - return - } + if isValidAuthHeader(authHeader) { + tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, bearerPrefix)) + if tokenString != "" { + token, err := jwt.ParseWithClaims(tokenString, &models.AccessToken{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + if rsaPrivateKey == nil { + return nil, errors.New("RSA private key is not initialized") + } + return &rsaPrivateKey.PublicKey, nil + }) - token, err := jwt.ParseWithClaims(tokenString, &models.AccessToken{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - if rsaPrivateKey == nil { - return nil, errors.New("RSA private key is not initialized") - } - return &rsaPrivateKey.PublicKey, nil - }) - - if err == nil { - if claims, ok := token.Claims.(*models.AccessToken); ok { - userID, err := services.GetUserIDFromEmail(claims.Email) if err == nil { - if err := RevokeAllUserSessions(userID); err != nil { - helper.LogError(err, "Failed to revoke user sessions during logout") + if claims, ok := token.Claims.(*models.AccessToken); ok { + userID, err := services.GetUserIDFromEmail(claims.Email) + if err == nil { + if err := RevokeAllUserSessions(userID); err != nil { + helper.LogError(err, "Failed to revoke user sessions during logout") + } + } else { + helper.LogError(err, "Failed to get user ID during logout") + } } } else { - helper.LogError(err, "Failed to get user ID during logout") + helper.LogError(err, "Failed to parse JWT token during logout") } + } else { + helper.LogWarn("Authorization header contains empty bearer token during logout") } } else { - helper.LogError(err, "Failed to parse JWT token during logout") + helper.LogWarn("Authorization header missing or invalid during logout; proceeding with cookie clear only") } if err := accessLog(r, nil, 18, nil); err != nil { helper.LogError(err, "Failed to write access log during logout") } - clearRefreshTokenCookie(w) - response := map[string]interface{}{ "message": "Successfully logged out", "action": "clear_session_storage", @@ -709,3 +708,58 @@ func clearRefreshTokenCookie(w http.ResponseWriter) { helper.LogInfo("Refresh token cookie clearing commands sent to browser") } + +func clearCSRFCookie(w http.ResponseWriter) { + helper.LogInfo("Clearing csrf_token cookie...") + + isSecure := strings.HasPrefix(os.Getenv("BACKEND_URL"), HTTPS) + + // Match middleware cookie characteristics first (host-only, SameSiteStrict) + primaryCookie := &http.Cookie{ + Name: "csrf_token", + Value: "", + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + Expires: time.Unix(0, 0), + MaxAge: -1, + } + http.SetCookie(w, primaryCookie) + helper.LogInfo(fmt.Sprintf("CSRF cookie clear #1 sent: Name=%s, Domain=%s, Secure=%v, SameSite=%v", + primaryCookie.Name, primaryCookie.Domain, primaryCookie.Secure, primaryCookie.SameSite)) + + // Fallback for local/dev browser behavior where secure or samesite attributes differ + fallbackCookie := &http.Cookie{ + Name: "csrf_token", + Value: "", + Path: "/", + HttpOnly: true, + Secure: isSecure, + SameSite: http.SameSiteLaxMode, + Expires: time.Unix(0, 0), + MaxAge: -1, + } + http.SetCookie(w, fallbackCookie) + helper.LogInfo(fmt.Sprintf("CSRF cookie clear #2 sent: Name=%s, Domain=%s, Secure=%v, SameSite=%v", + fallbackCookie.Name, fallbackCookie.Domain, fallbackCookie.Secure, fallbackCookie.SameSite)) + + if !isSecure { + localhostCookie := &http.Cookie{ + Name: "csrf_token", + Value: "", + Path: "/", + Domain: "localhost", + HttpOnly: true, + Secure: false, + SameSite: http.SameSiteLaxMode, + Expires: time.Unix(0, 0), + MaxAge: -1, + } + http.SetCookie(w, localhostCookie) + helper.LogInfo(fmt.Sprintf("CSRF cookie clear #3 sent: Name=%s, Domain=%s, Secure=%v, SameSite=%v", + localhostCookie.Name, localhostCookie.Domain, localhostCookie.Secure, localhostCookie.SameSite)) + } + + helper.LogInfo("CSRF token cookie clearing commands sent to browser") +}