From 80ca119017bedd46586c1f752bc0a310ddab2e13 Mon Sep 17 00:00:00 2001
From: Cam Saul <cammsaul@gmail.com>
Date: Thu, 21 Mar 2019 12:34:45 -0700
Subject: [PATCH] Change HttpOnly cookie name -> metabase.SESSION

---
 frontend/src/metabase/lib/cookies.js          | 31 +--------
 .../test/legacy-selenium/auth/login.spec.js   |  6 +-
 src/metabase/middleware/session.clj           | 67 +++++++++++++------
 3 files changed, 54 insertions(+), 50 deletions(-)

diff --git a/frontend/src/metabase/lib/cookies.js b/frontend/src/metabase/lib/cookies.js
index 860ccb863ce..1b934a75485 100644
--- a/frontend/src/metabase/lib/cookies.js
+++ b/frontend/src/metabase/lib/cookies.js
@@ -1,38 +1,11 @@
-import { clearGoogleAuthCredentials } from "metabase/lib/auth";
-
 import Cookies from "js-cookie";
 
-export const METABASE_SESSION_COOKIE = "metabase.SESSION_ID";
+// METABASE_SESSION_COOKIE is only used for e2e tests. In normal usage cookie is set automatically by login endpoints
+export const METABASE_SESSION_COOKIE = "metabase.SESSION";
 export const METABASE_SEEN_ALERT_SPLASH_COOKIE = "metabase.SEEN_ALERT_SPLASH";
 
 // Handles management of Metabase cookie work
 let MetabaseCookies = {
-  // set the session cookie.  if sessionId is null, clears the cookie
-  setSessionCookie: function(sessionId) {
-    const options = {
-      path: window.MetabaseRoot || "/",
-      expires: 14,
-      secure: window.location.protocol === "https:",
-    };
-
-    try {
-      if (sessionId) {
-        // set a session cookie
-        Cookies.set(METABASE_SESSION_COOKIE, sessionId, options);
-      } else {
-        sessionId = Cookies.get(METABASE_SESSION_COOKIE);
-
-        // delete the current session cookie and Google Auth creds
-        Cookies.remove(METABASE_SESSION_COOKIE);
-        clearGoogleAuthCredentials();
-
-        return sessionId;
-      }
-    } catch (e) {
-      console.error("setSessionCookie:", e);
-    }
-  },
-
   setHasSeenAlertSplash: hasSeen => {
     const options = {
       path: window.MetabaseRoot || "/",
diff --git a/frontend/test/legacy-selenium/auth/login.spec.js b/frontend/test/legacy-selenium/auth/login.spec.js
index c2d77233c96..624be75d3ca 100644
--- a/frontend/test/legacy-selenium/auth/login.spec.js
+++ b/frontend/test/legacy-selenium/auth/login.spec.js
@@ -10,6 +10,8 @@ import {
   describeE2E,
 } from "../support/utils";
 
+import { METABASE_SESSION_COOKIE } from "metabase/lib/cookies";
+
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
 
 describeE2E("auth/login", () => {
@@ -36,7 +38,7 @@ describeE2E("auth/login", () => {
       await waitForUrl(driver, `${server.host}/`);
       const sessionCookie = await driver
         .manage()
-        .getCookie("metabase.SESSION_ID");
+        .getCookie(METABASE_SESSION_COOKIE);
       sessionId = sessionCookie.value;
     });
 
@@ -55,7 +57,7 @@ describeE2E("auth/login", () => {
     beforeEach(async () => {
       await driver.get(`${server.host}/`);
       await driver.manage().deleteAllCookies();
-      await driver.manage().addCookie("metabase.SESSION_ID", sessionId);
+      await driver.manage().addCookie(METABASE_SESSION_COOKIE, sessionId);
     });
 
     it("is logged in", async () => {
diff --git a/src/metabase/middleware/session.clj b/src/metabase/middleware/session.clj
index ecb64c7b7e7..4757e14593c 100644
--- a/src/metabase/middleware/session.clj
+++ b/src/metabase/middleware/session.clj
@@ -4,7 +4,8 @@
              [config :as config]
              [db :as mdb]
              [public-settings :as public-settings]]
-            [metabase.api.common :refer [*current-user* *current-user-id* *current-user-permissions-set* *is-superuser?*]]
+            [metabase.api.common :refer [*current-user* *current-user-id* *current-user-permissions-set*
+                                         *is-superuser?*]]
             [metabase.core.initialization-status :as init-status]
             [metabase.models
              [session :refer [Session]]
@@ -16,35 +17,63 @@
            java.util.UUID
            org.joda.time.DateTime))
 
-(def ^:private ^String metabase-session-cookie "metabase.SESSION_ID")
-(def ^:private ^String metabase-session-header "x-metabase-session")
+;; How do authenticated API requests work? Metabase first looks for a cookie called `metabase.SESSION`. This is the
+;; normal way of doing things; this cookie gets set automatically upon login. `metabase.SESSION` is an HttpOnly
+;; cookie and thus can't be viewed by FE code.
+;;
+;; If that cookie is isn't present, we look for the `metabase.SESSION_ID`, which is the old session cookie set in
+;; 0.31.x and older. Unlike `metabase.SESSION`, this cookie was set directly by the frontend and thus was not
+;; HttpOnly; for 0.32.x we'll continue to accept it rather than logging every one else out on upgrade. (We've
+;; switched to a new Cookie name for 0.32.x because the new cookie includes a `path` attribute, thus browsers consider
+;; it to be a different Cookie; Ring cookie middleware does not handle multiple cookies with the same name.)
+;;
+;; Finally we'll check for the presence of a `X-Metabase-Session` header. If that isn't present, you don't have a
+;; Session ID and thus are definitely not authenticated
+(def ^:private ^String metabase-session-cookie        "metabase.SESSION")
+(def ^:private ^String metabase-legacy-session-cookie "metabase.SESSION_ID") ; this can be removed in 0.33.x
+(def ^:private ^String metabase-session-header        "x-metabase-session")
+
+(defn- clear-cookie [response cookie-name]
+  (resp/set-cookie response cookie-name nil {:expires (DateTime. 0)}))
+
+(defn- wrap-body-if-needed
+  "You can't add a cookie (by setting the `:cookies` key of a response) if the response is an unwrapped JSON response;
+  wrap `response` if needed."
+  [response]
+  (if (and (map? response) (contains? response :body))
+    response
+    {:body response, :status 200}))
 
 (s/defn set-session-cookie
   "Add a `Set-Cookie` header to `response` to persist the Metabase session."
   [response, session-id :- UUID]
-  (if-not (and (map? response) (:body response))
-    (recur {:body response, :status 200} session-id)
-    (resp/set-cookie
-     response
-     metabase-session-cookie
-     (str session-id)
-     (merge
-      {:same-site :lax
-       :http-only true
-       :path      "/api"
-       :max-age   (config/config-int :max-session-age)}
-      ;; If Metabase is running over HTTPS (hopefully always except for local dev instances) then make sure to make this
-      ;; cookie HTTPS-only
-      (when (some-> (public-settings/site-url) URL. .getProtocol (= "https"))
-        {:secure true})))))
+  (-> response
+      wrap-body-if-needed
+      (clear-cookie metabase-legacy-session-cookie)
+      (resp/set-cookie
+       metabase-session-cookie
+       (str session-id)
+       (merge
+        {:same-site :lax
+         :http-only true
+         :path      "/api"
+         :max-age   (config/config-int :max-session-age)}
+        ;; If Metabase is running over HTTPS (hopefully always except for local dev instances) then make sure to
+        ;; make this cookie HTTPS-only
+        (when (some-> (public-settings/site-url) URL. .getProtocol (= "https"))
+          {:secure true})))))
 
 (defn clear-session-cookie
   "Add a header to `response` to clear the current Metabase session cookie."
   [response]
-  (resp/set-cookie response metabase-session-cookie nil {:expires (DateTime. 0)}))
+  (-> response
+      wrap-body-if-needed
+      (clear-cookie metabase-session-cookie)
+      (clear-cookie metabase-legacy-session-cookie)))
 
 (defn- wrap-session-id* [{:keys [cookies headers] :as request}]
   (let [session-id (or (get-in cookies [metabase-session-cookie :value])
+                       (get-in cookies [metabase-legacy-session-cookie :value])
                        (headers metabase-session-header))]
     (if (seq session-id)
       (assoc request :metabase-session-id session-id)
-- 
GitLab