diff --git a/assets/js/app.js b/assets/js/app.js
index f2ff42d11..b01999ae7 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -62,44 +62,65 @@ function connect() {
}
// When Livebook runs in a cross-origin iframe the browser may restrict access
-// to cookies. This is the case in Safari with the "Prevent cross-site tracking"
-// option enabled, which is the default. Without cookies access, the session
-// is not stored, so CSRF tokens are invalid. Consequently, LV keeps reloading
-// the page, as we try to connect the socket with invalid token. To work around
-// this we tell the user to open Livebook outside the iframe.
+// to cookies. Without cookies access, the session is not stored, so CSRF tokens
+// are invalid. Consequently, LV keeps reloading the page, as we try to connect
+// the socket with invalid token. To work around this we tell the user to open
+// Livebook outside the iframe.
+//
+// The behaviour varies across browsers and browsing modes (regular and private).
+// A few examples (at the time of writing):
+//
+// * Safari by default blocks all cross-origin cookies. This is controlled by
+// the "Prevent cross-site tracking" option
+//
+// * Chrome in incognito mode blocks all cross-origin cookies, can be relaxed
+// on per-site basis
+//
+// * Firefox implements state partitioning (1) and it is enabled for storage
+// by default since Firefox 103 (2). With storage partitioning, the embedded
+// site gets a separate storage bucket scoped by the top-level origin, so
+// the site generally works as expected
+//
+// * Brave also implements storage partitioning (3)
+//
+// To detect whether cookies are allowed, we check for the user data cookie,
+// which should be set by the server on the initial request and is accessible
+// from JavaScript (without HttpOnly).
+//
+// Also see the proposal (4), which may streamline this in the future.
+//
+// (1): https://developer.mozilla.org/en-US/docs/Web/Privacy/State_Partitioning#state_partitioning
+// (2): https://www.mozilla.org/en-US/firefox/103.0/releasenotes
+// (3): https://brave.com/privacy-updates/7-ephemeral-storage
+// (4): https://github.com/privacycg/CHIPS
-if (document.hasStorageAccess) {
- document.hasStorageAccess().then((hasStorageAccess) => {
- if (hasStorageAccess) {
- connect();
- } else {
- const overlayEl = document.createElement("div");
+if (loadUserData() === null) {
+ const overlayEl = document.createElement("div");
- overlayEl.innerHTML = `
-
-
-
- Action required
-
-
- It looks like Livebook does not have access to cookies. This usually happens when
- it runs in an iframe. To make sure the app is fully functional open it in a new
- tab directly.
-
-
-
+ overlayEl.innerHTML = `
+
+
+
+ Action required
- `;
+
+ It looks like Livebook does not have access to cookies. This usually happens when
+ it runs in an iframe. To make sure the app is fully functional open it in a new
+ tab directly. Alternatively you can relax security settings for this page to allow
+ third-party cookies.
+
+
+
+
+ `;
- overlayEl.querySelector("#open-app").href = window.location;
+ overlayEl.querySelector("#open-app").href = window.location;
- document.body.appendChild(overlayEl);
- }
- });
+ document.body.appendChild(overlayEl);
} else {
connect();
}
diff --git a/assets/js/lib/user.js b/assets/js/lib/user.js
index a9de85826..7929a2b82 100644
--- a/assets/js/lib/user.js
+++ b/assets/js/lib/user.js
@@ -38,6 +38,14 @@ function getCookieValue(key) {
}
function setCookie(key, value, maxAge) {
- const cookie = `${key}=${value};max-age=${maxAge};path=/`;
+ const cookie = `${key}=${value};max-age=${maxAge};path=/${cookieOptions()}`;
document.cookie = cookie;
}
+
+function cookieOptions() {
+ if (document.body.hasAttribute("data-within-iframe")) {
+ return ";SameSite=None;Secure";
+ } else {
+ return ";SameSite=Lax";
+ }
+}
diff --git a/lib/livebook_web/components/layouts/root.html.heex b/lib/livebook_web/components/layouts/root.html.heex
index 01625384c..a44002000 100644
--- a/lib/livebook_web/components/layouts/root.html.heex
+++ b/lib/livebook_web/components/layouts/root.html.heex
@@ -20,6 +20,7 @@
Enum.join(",")}
+ data-within-iframe={Livebook.Config.within_iframe?()}
>
<%= @inner_content %>
diff --git a/lib/livebook_web/plugs/user_plug.ex b/lib/livebook_web/plugs/user_plug.ex
index 1b51d4078..c764dcd34 100644
--- a/lib/livebook_web/plugs/user_plug.ex
+++ b/lib/livebook_web/plugs/user_plug.ex
@@ -45,9 +45,23 @@ defmodule LivebookWeb.UserPlug do
else
user_data = user_data(User.new())
encoded = user_data |> Jason.encode!() |> Base.encode64()
- # Set `http_only` to `false`, so that it can be accessed on the client
- # Set expiration in 5 years
- put_resp_cookie(conn, "lb:user_data", encoded, http_only: false, max_age: 157_680_000)
+
+ put_resp_cookie(
+ conn,
+ "lb:user_data",
+ encoded,
+ # We disable HttpOnly, so that it can be accessed on the client
+ # and set expiration to 5 years
+ [http_only: false, max_age: 157_680_000] ++ cookie_options()
+ )
+ end
+ end
+
+ defp cookie_options() do
+ if Livebook.Config.within_iframe?() do
+ [same_site: "None", secure: true]
+ else
+ [same_site: "Lax"]
end
end