Passing assigns to LiveView initial render without putting them in the session

Hello :wave: ,

looking for advise on this issue we’re having with LiveView + authentication. We have solutions, only I fear we’re missing something simple and obvious.

Situation: We have an authentication service and an app with a LiveView UI. Incoming requests to the app have already been sent to the auth service before and have received some tokens describing the authentication state (think user id) in the form of custom x- headers.

We’d like to verify them on every request, including of course within our LiveViews (see Security model in the docs). For the normal HTTP request (the initial rendering), we rely on a plug to verify the token, whereas in the websocket-connected LiveView process we use get_connect_info in combination with a x_headers option on the socket. Both work great in terms of authentication. However, we’d also like to be able to pass information from the token to the socket assigns for rendering. This works for the connected?-way with get_connect_info because that happens within an on_mount, yet for the plug-based authentication we’re struggling to find a satisfactory solution.

  • We know about live_session and its session option → That of course allows us to copy assigns from the conn in the initial render into the “LiveView session” which makes them show up in the sessions parameter to on_mount. However, it also causes them to be written to the HTML in the form of an attribute on the LiveView element, and then be sent along the websocket connection when the LiveView registers. We don’t want this behaviour as we don’t want to (needlessly) expose the token information to the client. We also don’t need the “session” to be sent along on the websocket connection anymore (because get_connect_info), all we want is to pass some assigns from the conn (in the initial render request) to the fake “socket” (still in the initial render request) available in on_mount. There has to be a way, right?
  • Idea 1: It would be most amazing if only we could have get_connect_info work on the conn in the initial render… Then we wouldn’t have to pass anything and exclusively authenticate in the on_mount
  • Idea 2: What also would be great would be if someone knew a way to remove entries from the “LiveView session” before rendering the HTML, so could use the session option without exposing anything.

Idea 3, passing the data through the process dictionary of course works fine, but there has to be a different way to achieve this, right? Does anyone have a solution for this that feels “right” in current LiveView best practises?

Thanks a lot in advance!
Best,
malte

related posts / article:

2 Likes

Recomendation I’ve seen and I would do it so myself same way, is that login page is not using LiveView. It will then create a http only auth cookie on successful login. Then when LiveView WebSocket connects you use that cookie.

Another option is to instead create client id cookie. A cookie that contains JWT token and that contains generated unique client id for the browser. Then you tie authorizations to that client id. This allows you to logout your sessions from other computers/browsers for example.

Hey @wanton7 , thanks for your hints.

I’m not sure I understand 100% what you mean, but with regards to the auth cookie you suggest: We had this for a while, i.e. a separate session that is created by the application itself and stored in its own cookie, but then we need to invalidate this session when the “primary session” (= the one provided by the auth service) is cleared, which adds significant complexity. We’re trying to have only one service responsible for session management, which overall works nicely except for the fact that we can’t access our auth headers anymore in the initial render job of the LiveView.

What I mean by client id is that it could be UUID for example. Then you create JWT token that has clientId field that contains that unique id (UUID). Then you put that JWT token inside a http only cookie. So when someone does request for site you check if they have that cookie and if they don’t you’ll create it. You might also need to add expiration sliding for the cookie as well. By that I mean if some time you select has passed you’ll recreate it with the same client id.
That client id is unique to browser you are currently using. That is only thing you store to browser other things you store on the server side using that client id. So let’s say some wants to login, they give login name, password and submit. Instead of creating cookie you create authorization to database for that client id, that it has access. I think in your case when you receive authorization response from your authentication server, you’ll create authorization for the client id. You can tie other things to that client id like a session store.

:thinking: Hmm I still think we misunderstand each other a bit

We have a working authentication scheme (cookie based): The user at some points goes to a login page or uses some identity provider to be authenticated towards the “authentication service”. The auth service then issues a session_id which it stores in a normal client-side cookie. Behind this authentication service, we have a handful of other services, some of them providing a UI on their own (all running on the same domain though, so the cookie is valid). Our load balancer routes every incoming request to the auth service first, which validates the cookie and fetches information about the user to return it in form of a x-foobar header. The original request with the added header is then dispatched (by the LB) to the target service. So in essence, authentication is already done, the target service only needs to be able to access the x-foobar header to determine the identity of the user (e.g., it may include the user_id). Which works perfectly fine for both normal controllers as well as LiveViews with established socket connection (with help of get_connect_info) - we only don’t really have a way to access the header in the initial rendering of the LiveView, because get_connect_info is nil and LiveView doesn’t seem to provide a way to copy data from the conn to the socket struct besides the session key (which we don’t like to use). So the auth setup works fine, only we have an issue with accessing data from the connection in a LiveView module.

But I realize it’s probably a pretty specific and narrow problem - as said, we can simply store the data in the process dictionary for the initial rendering, which works fine. Just doesn’t feel right, somehow :slight_smile:

thanks again for your hints

I understand what you mean. Client id that you can uniquely identify browser is just one option that you can for example create session store on the server side per browser.

But many web apps already store JTW tokens in the browser memory. I don’t see any harm if initial render creates an JWT token with authorization information LiveView session needs and embeds that into HTML it renders and then JavaScript passes that JWT token from HTML into LiveSocket’s params. Adding expiration time for that JWT token as well makes sense for security reasons. You could also encrypt the information if it’s something sensitive. You also have to take into account that WebSocket connections can drop and app has to reconnect. This will happen for sure with users using mobile connections. Solution you choose need handle this or you have reauthorized users with your external authentication service when WebSocket connection drops.

You could also encrypt the information if it’s something sensitive

Yes, I know, and I also don’t see that much harm in doing so; however, my point is that it is completely unnecessary. The information is exchanged between the auth service and the target service (that includes the websocket HTTP requests), hence the information is present within the target service and I don’t want to render it back to the HTML and re-read it from the JS payload if there’s absolutely no need to do so. As said, the information is already there, I merely need a way to read it within the on_mount function.

You also have to take into account that WebSocket connections can drop and app has to reconnect.

Yep, exactly, that’s the other reason why I don’t want the target service to issue its own sessions, because I’ll have to invalidate them when the auth service drops the actual session.

Sorry, I really appreciate your help, but storing the information in the (target service’s) session is exactly what I don’t want to do :slight_smile:

For others here: For the time being, I went with the process dictionary option. So essentially I have something like

# endpoint
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [:x_headers]]

# router
plug :remember_x_headers_for_live_view

defp remember_x_headers_for_live_view(conn, _opts) do
  Process.put(:x_headers, my_get_x_headers_func(conn))
end

# on_mount
defp x_headers(socket) do
  if connected?(socket) do
    get_connect_info(socket).x_headers
  else
    Process.get(:x_headers)
  end
end

Feels a bit clumsy, but it works :man_shrugging:

2 Likes

Maybe I didn’t understand your question after all, I thought it was about how to pass authorization information for LiveSocket. So if I now understood correctly your problem was in it’s simplicity, how to pass data from router to LiveView during initial request? There is an example how to do that here in LiveView docs Referencing parent assigns

controller

conn
|> assign(:current_user, user)
|> LiveView.Controller.live_render(MyLive, session: %{“user_id” => user.id})

LiveView mount

def mount(_params, %{“user_id” => user_id}, socket) do
{:ok, assign_new(socket, :current_user, fn → Accounts.get_user!(user_id) end)}
end

There is also previous thread talking about this LiveView lifecycle hook with access to Conn? but the link provided there by José doesn’t work anymore, this is the correct link Security considerations of the LiveView model — Phoenix LiveView v0.17.5

2 Likes

@wanton7

the assign_new magic does the trick! Genius! Thanks a lot. No idea how I missed this section in the docs so far…

It’s a bit confusing, don’t you think? If you inspect the socket.assigns before calling assign_new, they sure do not contain any assigns from the conn. Only after a call to assign_new they’ll be copied over to the socket.assigns… Odd, but great that it works :tada:

Thanks a lot!

3 Likes

I agree, it’s confusing. I was aware of assign_new and I had no idea that it would behave that way when called during the disconnected mount. IMO this is a functionality that should be made available through another function with a descriptive name.

The documentation does state that for a disconnected mount conn.assigns are taken into account as well.

Yes, re-reading the documentation I think it is as clear as it can be in that regard. Only it was a bit surprising to me, that assign_new makes something appear, which otherwise isn’t in the assigns.

just because the documentation says that it works that way, it doesn’t mean that this is necessarily the right place for that functionality