Attempting to use live_view in Chrome Extension

I’m attempting to use live_view in a chrome extension. It worked fine for some time, then stopped in Chrome (presumably due to a security update). It continued to work in Edge for some time, then stopped (presumably because they trail on security updates). Now when I attempt to run my application, I get:

LiveView session was misconfigured or the user token is outdated.

1) Ensure your session configuration in your endpoint is in a module attribute:

    @session_options [
      ...
    ]

2) Change the `plug Plug.Session` to use said attribute:

    plug Plug.Session, @session_options

3) Also pass the `@session_options` to your LiveView socket:

    socket "/live", Phoenix.LiveView.Socket,
      websocket: [connect_info: [session: @session_options]]

4) Define the CSRF meta tag inside the `<head>` tag in your layout:

    <%= csrf_meta_tag() %>

5) Pass it forward in your app.js:

    let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
    let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}});

I’ve been able to make this work by bundling up my js in Webpack like this:

"chrome_app": "webpack js/chrome_app.js -o js/bundle.js --mode development --watch"

And get the web app working by making a seperate chrome_app.js, like this:

var xhr = new XMLHttpRequest();
xhr.responseType = 'document';
xhr.open('GET', 'http://localhost:4000/process_administrator', true)
xhr.onload = function(e) {
  document.documentElement.replaceChild(this.response.head, document.head)
  document.documentElement.replaceChild(this.response.body, document.body)

  let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
  console.log(csrfToken)
  let liveSocket = new LiveSocket("ws://localhost:4000/live", Socket, {
    params: { _csrf_token: csrfToken},
    hooks: Hooks
  })
  
  window.addEventListener("phx:page-loading-start", info => NProgress.start())
  window.addEventListener("phx:page-loading-stop", info => NProgress.done())
  
  liveSocket.connect()
  
  window.liveSocket = liveSocket
}
xhr.send()

It’s a little cheesy but it’s been working. I’ve validated that it passes in the csrf token in the websocket transaction, but phoenix always returns stale, like:

["4", "4", "lv:phx-Fj-Fa5KtHwBF1jOB", "phx_reply", {response: {reason: "stale"}, status: "error"}]

I’m starting to dig in to the guts of live_view and I’ve found that in LiveView’s mount function that the phx_socket value contains a nil session.

I’m trying to trace that back into the bowels of live view, and I’m wondering if anyone can help me out.

I’ve also started logging the plug, and I found a difference in my headers between the extension and the “normal” browser. Here’s the headers from the browser (working):

  req_headers: [
    {"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"},
    {"accept-encoding", "gzip, deflate, br"},
    {"accept-language", "en-US,en;q=0.9"},
    {"cache-control", "max-age=0"},
    {"connection", "keep-alive"},
    {"cookie",
     "_userdocs_web_key=SFMyNTY.g3QAAAACbQAAAAtfY3NyZl90b2tlbm0AAAAYNFZZNzBSLVlCNjdHNmE2SUVlZWVZTFdWbQAAABF1c2VyZG9jc193ZWJfYXV0aG0AAAB2U0ZNeU5UWS5kWE5sY21SdlkzTmZkMlZpWDJNd05qVmlNR1kwTFRjeU16WXROREZtT0MxaE9HVXhMV0UyT0dGbE1USmpPVGMwWXcubUlsZ3pseGVVck5heXBxZ21MZ2p4Vkl0b2RnblRtNy1DWGg0OVBCVlJQOA.-IIyIbAN2-362-wIU4e69Wjw_-jjRxdaqfjx1vUeF6Q"},
    {"host", "localhost:4000"},
    {"sec-fetch-dest", "document"},
    {"sec-fetch-mode", "navigate"},
    {"sec-fetch-site", "none"},
    {"sec-fetch-user", "?1"},
    {"upgrade-insecure-requests", "1"},
    {"user-agent",
     "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36"}
  ],

Here’s the headers from the extension (not working):

req_headers: [
    {"accept", "*/*"},
    {"accept-encoding", "gzip, deflate, br"},
    {"accept-language", "en-US,en;q=0.9"},
    {"connection", "keep-alive"},
    {"cookie",
     "_userdocs_web_key=SFMyNTY.g3QAAAACbQAAAAtfY3NyZl90b2tlbm0AAAAYc3UySngzZXZZM2JrMDg3WFZFRlF6QzdNbQAAABF1c2VyZG9jc193ZWJfYXV0aG0AAAB2U0ZNeU5UWS5kWE5sY21SdlkzTmZkMlZpWDJKaU5XUmlNamxsTFdNNE1tVXRORGcyTXkxaFkySmhMV1l4T1dWaVpEaGxPVGcxTVEuaHNHNWFzSGNuMWxhOGtXeFEyZ0JfTVBHTDRFc3I5OFlHWTdkSVBHcXhXcw.eZk4nKTljgEr2BLm0QRBTR4hhu8Om8vB7N1dRBme4Is"},
    {"host", "localhost:4000"},
    {"sec-fetch-dest", "empty"},
    {"sec-fetch-mode", "cors"},
    {"sec-fetch-site", "none"},
    {"user-agent",
     "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36 Edg/86.0.622.43"}
  ],

I made some progress on this. When javascript app calls for it’s upgrade, it calls with a cookie in it’s request headers. That makes it into the socket on the initial handle_info call into live_view, like this:

%Phoenix.Socket{
  assigns: %{},
  private: %{
    connect_info: %{
      session: %{
        "_csrf_token" => "su2Jx3evY3bk087XVEFQzC7M",
        "app_web_auth" => "SFMyNTY.dXstuffpCU"
      }
    }
  },
}

For some reason, when called from my chrome app, it doesn’t put app_web_auth in the headers when it calls the websocket upgrade.

I made some more progress on this. It looks like the cookie is being set in the browser. The cookie is being passed in to the http call, but it’s not being passed into the call to websockets.