Improve cookie access when running in iframe (#1888)

This commit is contained in:
Jonatan Kłosko 2023-04-28 13:50:40 +01:00 committed by GitHub
parent afe71a9bac
commit 8eb93f3e24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 37 deletions

View file

@ -62,17 +62,39 @@ function connect() {
} }
// When Livebook runs in a cross-origin iframe the browser may restrict access // 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" // to cookies. Without cookies access, the session is not stored, so CSRF tokens
// option enabled, which is the default. Without cookies access, the session // are invalid. Consequently, LV keeps reloading the page, as we try to connect
// is not stored, so CSRF tokens are invalid. Consequently, LV keeps reloading // the socket with invalid token. To work around this we tell the user to open
// the page, as we try to connect the socket with invalid token. To work around // Livebook outside the iframe.
// 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) { if (loadUserData() === null) {
document.hasStorageAccess().then((hasStorageAccess) => {
if (hasStorageAccess) {
connect();
} else {
const overlayEl = document.createElement("div"); const overlayEl = document.createElement("div");
overlayEl.innerHTML = ` overlayEl.innerHTML = `
@ -84,7 +106,8 @@ if (document.hasStorageAccess) {
<div class="mt-3 text-sm text-gray-300"> <div class="mt-3 text-sm text-gray-300">
It looks like Livebook does not have access to cookies. This usually happens when 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 it runs in an iframe. To make sure the app is fully functional open it in a new
tab directly. tab directly. Alternatively you can relax security settings for this page to allow
third-party cookies.
</div> </div>
<div class="mt-6"> <div class="mt-6">
<a id="open-app" class="button-base button-blue" target="_blank"> <a id="open-app" class="button-base button-blue" target="_blank">
@ -98,8 +121,6 @@ if (document.hasStorageAccess) {
overlayEl.querySelector("#open-app").href = window.location; overlayEl.querySelector("#open-app").href = window.location;
document.body.appendChild(overlayEl); document.body.appendChild(overlayEl);
}
});
} else { } else {
connect(); connect();
} }

View file

@ -38,6 +38,14 @@ function getCookieValue(key) {
} }
function setCookie(key, value, maxAge) { 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; document.cookie = cookie;
} }
function cookieOptions() {
if (document.body.hasAttribute("data-within-iframe")) {
return ";SameSite=None;Secure";
} else {
return ";SameSite=Lax";
}
}

View file

@ -20,6 +20,7 @@
<body <body
class="bg-white" class="bg-white"
data-feature-flags={Livebook.Config.enabled_feature_flags() |> Enum.join(",")} data-feature-flags={Livebook.Config.enabled_feature_flags() |> Enum.join(",")}
data-within-iframe={Livebook.Config.within_iframe?()}
> >
<%= @inner_content %> <%= @inner_content %>
</body> </body>

View file

@ -45,9 +45,23 @@ defmodule LivebookWeb.UserPlug do
else else
user_data = user_data(User.new()) user_data = user_data(User.new())
encoded = user_data |> Jason.encode!() |> Base.encode64() 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(
put_resp_cookie(conn, "lb:user_data", encoded, http_only: false, max_age: 157_680_000) 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
end end