braverhealth / phoenix-socket-dart

Cross-platform and stream-based implementation of Phoenix Sockets
https://pub.dev/packages/phoenix_socket
BSD 3-Clause "New" or "Revised" License
79 stars 41 forks source link

Add callback in PhoenixSocket to update `_options` when socket closes and try to reconnect #97

Open jaybe78 opened 5 days ago

jaybe78 commented 5 days ago

Hey Guys,

When I'm connecting to a socket, on the server side, I have to verify the passed jwt is valid. My signed JWT are mostly only valid a couple of minutes.

def connect(_params, socket, connect_info) do
    result = verify_jwt(_params["jwt"])

    case result do
      {:ok,  %{ "email" => email}} ->
        socket =
          assign(socket, :user_id, email)
          |> assign(:username, _params["username"])

        {:ok, socket}

      {:error, :token_expired} ->
         {:error, reason: result}
    end
  end

on the client side, I pass a jwt to PhoenixSocket to connect:

PhoenixSocketOptions(
      params: {
        "username": globalCubit.state.profile!.username,
        "jwt": credentials!.jwtToken,
        "user_pk": globalCubit.state.profile!.email,
      },
    );

So the issue is that, if for any reason the socket closes, it will try to reconnect using the JWT passed the first time which obviously might be expired by then.

So, I tried to find a way to update the _options but it's not exposed publicly

_options needs to be updated so that when connect is called the _buildMountPoint contaiuns the latestJWT

_mountPoint = await _buildMountPoint(_endpoint, _options);

So I'm thinking there should be a setter for

PhoenixSocketOptions _options

That way in our own implementation. when the "close" stream event occurs we can update any credentials required to connect to the socket

Just did a quick test and this is working.

class PhoenixSocket {

...
void set options(PhoenixSocketOptions options) {
    _options = options;
  }

)

_socket = socket.connect();
_socket?.closeStream.listen((event) {
   _socket.options = {new options with updated jwt}
});

EDIT: the above is not good enough because when socket connect fails, it gets directly to delayReconnect, so even if the jwt is updated afterwards (when its gets into the close event) it would still be the previous one.

So in addition to PhoenixOptions, why not also add an option for an async callback getSocketOptions

I tested the following and it works well.

PhoenixSocket(
...
    PhoenixSocketOptions? socketOptions,
    Future<PhoenixSocketOptions> Function()? getSocketOptions,

    /// The factory to use to create the WebSocketChannel.
    WebSocketChannel Function(Uri uri)? webSocketChannelFactory,
  })  : _endpoint = endpoint,
        ...{
    _options = socketOptions ?? PhoenixSocketOptions();
    _getSocketOptions = getSocketOptions != null ? getSocketOptions: () async => _options;

    late Future<PhoenixSocketOptions> Function() _getSocketOptions;

void _connect(Completer<PhoenixSocket?> completer) async {
   ...

    final socketOpts = await _getSocketOptions();
    _mountPoint = await _buildMountPoint(_endpoint, socketOpts);

That way in your getSocketOptions, you could for example always get the latest jwt when you refresh it using refreshJWT.

matehat commented 4 days ago

Hi @jaybe78, thanks for the write up. Would you feel comfortable making a PR with the changes you propose?

jaybe78 commented 3 days ago

Hey @matehat

Yes definitely.
Will do that tomorrow

Thanks