Hi all!
Since I released plug_http_cache
, I’ve been asked a few times how to use it to cache LiveView’s initial responses. I’m not very well versed in LiveView, and I think there are many ways one could shoot himself in the foot doing that. However, it seems to be there are some reasonable use-cases for doing that.
One of them is to use http caching to improve SEO ranking for public pages that have little interactivity whether the user is logged in or not. For instance, one LiveView page could render financial charts in real-time as an aside, while the main content is almost static.
I tried plug_http_cache
and it almost works out-of-the box:
- initial mount replies with a
200
response, which is cached - second mount replies with a
101
HTTP response, which is not cacheable. The websocket connection is established
So far so good, but LiveView uses a CSRF token that is cached and sent along the second request - and LiveView WS connection fails when opening the same page in another browser with:
[debug] 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) Ensure the `protect_from_forgery` plug is in your router pipeline:
plug :protect_from_forgery
5) Define the CSRF meta tag inside the `<head>` tag in your layout:
<meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} />
6) 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 figured out it’s possible to disable checking of this CSRF token when configuring the live endpoint, at the cost of disabling live sessions:
endpoint.ex
before:
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
endpoint.ex
after:
socket "/live", Phoenix.LiveView.Socket, websocket: []
My first batch of questions would be:
- is it safe to do so?
- what are the other side-effects, other than disabling live session?
- is that possible to have 2 sockets: one where live sessions are disabled for this specific use-case, and another one with session information enabled?
Another thing I noticed is that some information related to LiveView is stored in the HTML upon initial rendering:
<div
data-phx-main
data-phx-session="SFMyNTY.g2gDaAJhBXQAAAAIdwJpZG0AAAAUcGh4LUY1Q0hZRndnM2I0N0lRSkN3B3Nlc3Npb250AAAAAHcKcGFyZW50X3BpZHcDbmlsdwR2aWV3dy1FbGl4aXIuSHR0cENhY2hlV2l0aExpdmV2aWV3V2ViLlVzZXJMaXZlLlNob3d3BnJvdXRlcncmRWxpeGlyLkh0dHBDYWNoZVdpdGhMaXZldmlld1dlYi5Sb3V0ZXJ3DGxpdmVfc2Vzc2lvbmgCdwdkZWZhdWx0bggAt3Fq2ugnkBd3CHJvb3RfcGlkdwNuaWx3CXJvb3Rfdmlld3ctRWxpeGlyLkh0dHBDYWNoZVdpdGhMaXZldmlld1dlYi5Vc2VyTGl2ZS5TaG93bgYA-_cJWYsBYgABUYA.25eSbZuNO6oxbt4AEcignF3HVsHExGqPcoc8BI8E5nI"
data-phx-static="SFMyNTY.g2gDaAJhBXQAAAADdwJpZG0AAAAUcGh4LUY1Q0hZRndnM2I0N0lRSkN3BWZsYXNodAAAAAB3CmFzc2lnbl9uZXdqbgYA-_cJWYsBYgABUYA.k0KyuD5mMq4arpXYUO2poQAdDrnFipHxVJ1D6B32tyI" id="phx-F5CHYFwg3b47IQJC">
After decoding it, it seems it doesn’t contain private session data, which remains stored in the cookie. However, is there any reason this data shouldn’t be served to users other than the one who initiated the request in the first place?
Lastly, I’d like to write guidance on how to handle authenticated LiveView’s with static content. The idea is to render only public data on initial mount, and load private, session-based data only when the LiveView goes live:
def mount(params, session, socket) do
if not connected?(socket) do
# Initial request: here we render only **public** data.
# The response can be cached by shared caches
products = Products.list(params)
{:ok, assign(socket, products: products)}
else
# Check user authentication
socket =
case session do
%{"user_id" => user_id} ->
assign(socket, user: Accounts.get_user!(user_id))
_ ->
socket
end
# Perform stateful stuff that don't depend on the user
Phoenix.PubSub.subscribe(MyApp.PubSub, "product-updates")
if socket.assigns.user do
# Prepare rendering for authenticated users
socket =
socket
|> assign(:pending_orders, User.get_pending_orders!(socket.assigns.user))
|> assign(:pending_notifications, User.get_pending_notifications(socket.assigns.user))
{:ok, socket}
else
# Prepare rendering for anonymous users
{:ok, socket}
end
end
end
What do you think? Did I miss something? Any security issue I didn’t anticipate?
Cheers!