Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for live views #11

Open
brianmay opened this issue Nov 7, 2021 · 13 comments
Open

Support for live views #11

brianmay opened this issue Nov 7, 2021 · 13 comments

Comments

@brianmay
Copy link

brianmay commented Nov 7, 2021

Live views do not get the conn value, only the session.

I imagine this would be implemented through the on_mount LiveView server lifecycle hooks that was introduced in Pheonix 1.6.

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1

@brianmay
Copy link
Author

brianmay commented Nov 7, 2021

I plan to look at this after #7.

@tanguilp
Copy link
Owner

tanguilp commented Nov 7, 2021

Interesting. Given the example, you go through your plugs before mounting the liveview, including Plugoid if configured. So you know your user is authenticated. That said as far as I remember you have to check again when mounting the liveview that the user is authenticated for security reasons (because you can make the websocket request bypassing the plugs or something like that).

Now one problem I see is that session data is passed to the callback, but Plugoid uses its own session cookie separated from the default session (for security reasons: the authentication cookie has stricter settings).

Another is that https://hexdocs.pm/plugoid/Plugoid.html#module-session will not work inside a liveview. Plugoid has an internal max session lifetime, after which it redirects to the OpenID Provider (OP). If the user is already authenticated on the OP, he's redirected back immediately to the page he was browsing (with new tokens, but it's semaless). In a liveview where a user would stay a long time, this mechanisms would not work.

@brianmay
Copy link
Author

brianmay commented Nov 7, 2021

Not getting the session information might be a problem. Not sure, can't think of a solution off hand.

I have seen live view sessions expire. But I have no idea how this works. The docs talk about logging out: https://hexdocs.pm/phoenix_live_view/security-model.html#disconnecting-all-instances-of-a-live-user but not expiring sessions.

@brianmay
Copy link
Author

brianmay commented Nov 7, 2021

Thanks to your callback in #12, can now add the following line to the callback (as per Pheonix docs cited above):

conn = put_session(conn, :live_socket_id, "users_socket:#{sub}")

Am starting to think that live view logic is mostly application specific and should be left to the application. But we probably can add documentation/sample code on how to do this.

Haven't yet got a logout URL, about to look at that now.

@brianmay
Copy link
Author

brianmay commented Nov 8, 2021

For reference, here is the current live view setup I have:

Token call back, as per #12:

defmodule RoboticaFaceWeb.TokenCallback do
  import Plug.Conn

  @spec callback(
          Plug.Conn.t(),
          OIDC.Auth.OPResponseSuccess.t(),
          String.t(),
          String.t(),
          keyword()
        ) :: Plug.Conn.t()
  def callback(conn, response, _issuer, _client_id, _opts) do
    sub = response.id_token_claims["sub"]
    claims = Map.take(response.id_token_claims, ["groups", "name", "sub"])

    conn
    |> fetch_session()
    |> put_session(:live_socket_id, "users_socket:#{sub}")
    |> put_session(:claims, claims)
  end
end

Logout controller function:

  def logout(conn, _params) do
    user = RoboticaFaceWeb.Auth.current_user(conn)

    if user != nil do
      sub = user["sub"]
      RoboticaFaceWeb.Endpoint.broadcast("users_socket:#{sub}", "disconnect", %{})
    end

    conn
    |> Plugoid.logout()
    |> put_session(:claims, nil)
    |> put_flash(:danger, "You are now logged out.")
    |> redirect(to: Routes.page_path(conn, :index))
  end

Obviously this requires anonymous access to the path it redirects too (or it could get very confusing for the user if the get redirected to the login page and only see the message after they log in).

Where current_user(conn) is a simple wrapper function:

defmodule RoboticaFaceWeb.Auth do
  @moduledoc """
  Helper functions for authorization
  """

  @spec current_user(Plug.Conn.t()) :: map()
  def current_user(conn) do
    Plug.Conn.get_session(conn, :claims)
  end

  @spec user_is_admin?(map()) :: boolean()
  def user_is_admin?(user) do
    case user do
      %{"groups" => groups} -> Enum.member?(groups, "admin")
      _ -> false
    end
  end
end

And the LiveView InitAssigns hook code:

defmodule RoboticaFaceWeb.InitAssigns do
  import Phoenix.LiveView

  alias RoboticaFaceWeb.Router.Helpers, as: Routes

  def mount(_params, session, socket) do
    user = session["claims"]

    if user == nil do
      socket = put_flash(socket, :danger, "Your session has expired.")
      socket = redirect(socket, to: Routes.page_path(socket, :index))
      {:halt, socket}
    else
      socket = assign(socket, :user, user)
      {:cont, socket}
    end
  end
end

I am a bit puzzled here, I had to define a mount function here, but the Phoenix documentation say I should use on_mount, with an extra first parameter. Should try to chase up on this discrepancy.

Ideally would like to redirect to the same URL, so that the user goes back to the same place after logging in again.
But I couldn't work out how to do this from the "mount" function in a hook. But this may not work terrible well anyway, especially if you (like me) have a number of tabs open to the website that get logged out at the same time. I think plugoid can only keep track of one redirect after login url per session.

@brianmay
Copy link
Author

brianmay commented Nov 8, 2021

In typical fashion, my website stops working immediately after I turn my computer off for night... So might be an error in the code I quoted, will check tomorrow.

Later: I suspect that might be an unrelated issue, will file #13.

@brianmay
Copy link
Author

brianmay commented Dec 8, 2021

I am seeing some unexpected behaviour with Firefox/android. It seems Firefox is quite happy to reconnect to a previous live session. Including one that has been idle for hours - i.e. Firefox in the background for hours. Not 100% sure but I suspect Firefox on Android doesn't support web sockets in the background. So I would have expected the live session to have disconnected some time ago.

But if I click any buttons, including live_patch links to the same live view, it will force a login.

The behaviour has recently changed. Before Firefox would force a reload of the page when I first reopen it. Which would typically force a login. But clicking back several times would get me to a live view that would work without a login (huh?). But now it just connects to the previous live view.

Still trying to work this out.

@tanguilp
Copy link
Owner

tanguilp commented Dec 9, 2021

Thanks for reporting!

Couldn't be due to a change in Liveview? Have you updated it recently?

@brianmay
Copy link
Author

brianmay commented Dec 9, 2021

I have been consistently making changes, so I can't rule that out. But not changed anything significant that I can think of regarding live views, sessions or authentication. Not for a while now.

I do have a theory that maybe the plugoid session is somehow expiring before the plugoid session. Looking at the plugoid code again, it looks like it has a default session time it imposes of session_lifetime=1 hour, and I think Phoenix might not have any session time-outs by default (???).

So if I revisit a page that is already open (i.e. by reopening Firefox), it creates a new live view, which calls my mount function. Which checks the phoenix session. But when I click a link, it is causing the page to be reloaded, which causes the plugoid auth to check its session which has expired, so it requires the user login.

One flaw in this theory is that live_patch links shouldn't require the page to be reloaded. But maybe this is not working on Firefox/Android.

Another flaw is that I only have really noticed this issue on Firefox/Android. Firefox on Linux doesn't seem to follow the same pattern. I probably should retest though.

I miss not having convenient debug tools on Firefox/Android.

I had this theory, discarded it, and coming back to it again.

If this is the case, I guess there are several possible solutions, depending on paranoia level:

  • Turn off plugoid session expiration.
  • Having the live view check the plugoid session would be good, but I think the live view doesn't have access to this information. Might be possible to give it access, not sure here.
  • Add the plugoid session time-out to the phoenix session, check this time in the mount function, and set a timer or something to kick the user out after this time.

@brianmay
Copy link
Author

brianmay commented Dec 9, 2021

For that matter, I really have not noticed any session time-outs on Firefox on Linux. Wonder if the default 1 hour isn't working for some reason here. Might try to debug when I get a chance.

LATER: never mind, this is working perfectly on Firefox, except for live views, which was expected.

@brianmay
Copy link
Author

brianmay commented Dec 14, 2021

Thinking this through a bit more:

  • It does not appear to be possible to turn off plugoid session expiration. Possibly this is something that could be added?
  • Would be rather useful if the session expiration time - auth_session_info.auth_time_monotonic + opts[:session_lifetime] was made available to the app. Maybe by storing in conn object and helper function to retrieve.

That would then allow the application to put the expiration time into the Phoenix session (maybe in a custom plug - or maybe in yet another callback), which will get passed to the life view, and the live view then has an indication of when the session is going to expire, and take appropriate action when the session does expire.

Hmmm. But the session expiration the live view has has might be old, the session might have been renewed. So correct behaviour might be to disconnect and force a reload the live view if the session expiration time has expired, rather then actively reject the user.

@tanguilp
Copy link
Owner

It does not appear to be possible to turn off plugoid session expiration. Possibly this is something that could be added?

It should be done through cookie lifetime.

To have Liveview's expiration meet, indeed you need to transmit how long the cookie is valid and manually force a redirect to the same URL.

That has been a long time since then, and I only started working with Liveview recently.

I think putting user id in the session (put_session) is a bad idea, because it sets the value in Phoenix's own session cookie. Maybe using x-.... headers could be useful. Have to think about it.

@tanguilp
Copy link
Owner

No, I think one should use the :session option: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live_session/3-options

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants