Decoding Plug session manually

I need to decode the plug session manually for my websocket server (I used to only have API calls and that worked fine, but now that I need websockets, which don’t work with plug, I have to decode the session cookie without the conn struct). I have managed to get a bitstring, but there seems to be some kind of extra step to go from the bitstring to the map from session key to session value that I’m missing. Here’s the source code I have so far.

These 2 functions are taken almost straight from the plug source code:

defp derive(secret_key_base, key, key_opts) do
  secret_key_base
  |> KeyGenerator.generate(key, key_opts)
end

defp read_raw_cookie(raw_cookie, opts) do
  signing_salt = derive(opts.secret_key_base, opts.signing_salt, opts.key_opts)

  case opts do
    %{encryption_salt: nil} ->
      MessageVerifier.verify(raw_cookie, signing_salt)

    %{encryption_salt: _} ->
      encryption_salt = derive(opts.secret_key_base, opts.encryption_salt, opts.key_opts)
      MessageEncryptor.decrypt(raw_cookie, encryption_salt, signing_salt)
  end
  |> case do
    :error -> nil
    {:ok, result} -> result
  end
end

This is what I do to set up the options for decoding:

opts = %{
  key_opts: [
    length: 64
  ],
  secret_key_base: Application.fetch_env!(:llserver, :session_secret_key_base),
  encryption_salt: Application.fetch_env!(:llserver, :session_encryption_salt),
  signing_salt: Application.fetch_env!(:llserver, :session_signing_salt)
}

cookies = request.headers["cookie"]
ll_cookie = Cookies.decode(cookies)["_ll_session"]
session = read_raw_cookie(ll_cookie, opts)

This returns a session, which in the console prints like this:

<<131, 116, 0, 0, 0, 1, 109, 0, 0, 0, 13, 115, 101, 115, 115, 105, 111, 110, 95,
  116, 111, 107, 101, 110, 109, 0, 0, 0, 72, 100, 99, 56, 48, 49, 97, 100, 102,
  45, 54, 49, 99, 54, 45, 52, 48, 51, 49, 45, 97, 98, ...>>

This is of length 101, however my session keys are 2 concatenated UUIDs like this:

[
  %LLServer.Schema.UserSession{
    __meta__: #Ecto.Schema.Metadata<:loaded, "usersession">,
    id: 1,
    inserted_at: ~N[2023-01-16 13:48:04],
    session: "dc801adf-61c6-4031-ab0c-4fa93012e9d865754857-90d3-4e19-a101-845b4d1b13f5",
    updated_at: ~N[2023-01-16 13:48:04],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 1
  }
]

The decryption function seems to work, but I have no idea how to decode that bitstring into the actual session token. The way I put it into the cookies is by calling put_session with a key of :session_token, so I’m assuming that is also saved in the session. How can I retrieve the map just as if I was using Plug.Conn.get_session/1 ?

You need to call :erlang.binary_to_term/1 on that. Or, better: Plug.Crypto.non_executable_binary_to_term(session, [:safe])

2 Likes

Thanks a lot! I may do a simple library in the future with all the stuff in here just in case someone else also needs this kind of functionality because it feels weird to not see it already solved.

All of that code is in plug, so not sure why you feel this is “not solved”. It’s not really clear what you’re doing, but there’s reasons why cookies (and therefore sessioon data) are not just exposed to channels (over websockets) in phoenix – these cannot be properly secured.

Phoenix.Endpoint — Phoenix v1.7.0-rc.2 shows a (seemingly new) session option, which could allow you to access session data when using a CSRF token on the websocket request, which works around those security concerns.

You may want to take a look at the WebSock & WebSockAdapter libraries, along with the new upgrade_adapter/3 function added in Plug 1.14. This is the stack that Phoenix is based on as of 1.7, and comprises a fully open standard WebSocket support in both Bandit and Cowboy web servers.

For what you’re looking to do, you’d want to grab the relevant parts of the Plug.Conn request as part of your HTTP handler for your WebSocket upgrade, and then pass those to your WebSock implementation as the second element of the arg tuple to upgrade_adapter/3. Your WebSock implementation will then see those values as the value to its init/1 call.

Howto docs are a little bit sparse at the moment, but some of the test servers in the Bandit test suite (such as this one) should probably be pretty instructive.

I’m not using phoenix, and I’m just using this in the initial connection to the websocket route, not for sending messages once the websocket connection has been established.

Oh hey that looks pretty interesting; I’ll definitely check it out. Thanks!