Sync vs Async channel connection authorization

I had a question about the authorization process in the Channel.join. It must be synchronous, and so have to hit the database to get the information with the given payload. Nothing else can provide information at this location of the code. Am I correct?

To avoid this, you can pubsub but it is async: send a message via the Endpoint to the Liveview (I am in this case) since the LV has the information “on mount” via the session. Then the LV can check, broadcast back a response captured in a handle_info in the custom channel module. If the user is not authorized on this channel, you can send a {:stop, :unauthorized, socket} and forward a broadcast to the client-side to terminate the WS with a channel.leave(). Are there alternatives for this authorization?

Sure, but can you elaborate as to why you see this as an issue?

I was wondering if I could get the authorization from elsewhere, and not hit the db, and the example in the docs shows that channel.join terminates with {:ok, socket} or {:error, reason}.

The function being synchronous is orthogonal to where the data comes from. You could take the connection params and do a signature verification if you wanted, or hit something in memory, or any other similar computation. And your async thing could choose amongst the same options, or also hit the DB. What you do in that function is unrelated to the bit where it is synchronous.

Verification against what? The LV socket (this is my case) is not available, only the db is the source of information. I don’t see any other option of doing verification and terminating the join function in the flow. Am I wrong?

I don’t really understand your question. You said: “it must be synchronous” which is a general statement, not “I think it needs to be synchronous for me in the context of my code and data”. If you only have info on the database then sure, you will need to hit the database.

You also said: “To avoid this” but you haven’t elaborated on why you are trying to avoid this.

No, not in the context of my code. In the context of channels, for any app, where else can you get information on authorizing users else than from the db? The reason for trying not to use the db is the cost of using it, my decision. So I was just checking if maybe someone had a more obvious solution as the docs left the “authorized” bit void. Now I understand that I shouldn’t have polluted this thread.

As I noted above, a signed token in the connection params, in memory data stores, network requests, etc.

You’re still using it, but using it async instead of sync. If you want to hit the DB it’s gonna be ~10ms, that doesn’t seem like a huge delay to add the join call on the front end, and is much simpler than the async flow.

No worries, moved it to a new thread!

a huge delay to add the join call on the front end, and is much simpler than the async flow.

Yes I agree, but Redis is VERY costly so I want to avoid it.

I am not using it, I passed the info to the LV, so I just send a message to the LV to get the info, not using the Redis db.

a signed token in the connection params, in memory data stores, network requests, etc

I receive a signed token, a Phoenix.Token more exactly but it is just authentication and doesn’t convey any information on authorization. If I use ETS, then I would have problems with load balancing and need sticky sessions. For the network requests, it is async, no more in the flow of the join() function. Maybe I presented this in a pompous way with async. Anyway, thanks for your time.

Hmm, the signed token could be extended to also include channel room/topic authorization information.

pipeline :browser do
  plug OurAuth
  plug :put_user_token

defp put_user_token(conn, _) do
  if current_user = conn.assigns[:current_user] do
    user_data = %{user_id:, channel_scope: ["room:foo"]}
    token = Phoenix.Token.sign(conn, "user socket", user_data)
    assign(conn, :user_token, token)

Yes. I didn’t pass this to the token because the scope can be dynamic but maybe it is not a problem after all.

Thinking again, is this safe?

Well if it’s very dynamic, you know what they say about the two hard problems in computer science: cache invalidation, naming things, and off-by-1 errors…

Regarding safety, Phoenix.Token.sign/4 will encode and sign but not encrypt while Phoenix.Token.encrypt/4 will encode, encrypt, and sign so that would be the better option if you wanted to keep users’ scopes confidential.

ah, Phoenix.Token.encrypt, looks good!

About the Plug: I adapt this to put into the session for the LV instead.