We need to figure out a standard way for authentication/authorization in Channels and API's. Our suggestion today is "use tokens" but users are going to implement custom and thus broken solutions. This issue is to scope out our options and what we want to do.
I've implemented a couple and also looked into what third parties are also using. I'll lay them out with pros/cons here with the Example:
Authentication: Bearer auth_token headers for api requests. Allowing them to circumvent cor's cookie restrictions.What we need to decide on is, how to generate.
https://gist.github.com/jeregrine/beba5dd6f83a9e07254d#file-channel_auth-ex
I'm not sure thats super idiomatic elixir as I am generating an encryptor and everything everytime but it works okay and shows the point.
_Pros_
_Cons_
https://gist.github.com/jeregrine/beba5dd6f83a9e07254d#file-joken_auth-ex
Again this file is kind of messy but I just kinda tossed it together.
_Pros_
_Cons_
I haven't personally implemented this one but Hex.pm currently uses this https://github.com/hexpm/hex_web/blob/master/lib/hex_web/api/key.ex
I cannot really speak to the pros and cons of this approach over just generating bearer tokens. Would be helpful if @ericmj could chime in on this approach.
Again this is an issue to get the discussion going. Let me know what your opinions are, I am exploring this as we speak for my projects and it can benefit the entire Phoenix Project as well.
Also feel free to burn me on my coding style. Only way to get better is through feedback!
A couple notes:
I feel like we should ship with something like 1/2 and have guides on how to do 3.
Btw, Hex needs to store them in the database because they are lifetime and stored in the users computer and not something we can use just for a single session like channel tokens.
1 - You mean like cookie sessions?
2 - Yea we could, but I鈥檇 prefer to not define our own spec when one exists. I鈥檓 pretty confident we could work with the creator of Joken to remove all deps but poison. He only used timex for time math convenience.聽
I agree we should ship with 1/2 and have guides for 3.聽
My gut leans towards JWT because its a standard open spec and used all over. People will have less reason to complain about the tokens not being server site useable like we had with cookies (even though I agree that this is not a good argument for using them).
That said the plug stuff was fine too. I鈥檓 conflicted.
On Fri, Mar 13, 2015 at 4:14 PM, Jos茅 Valim [email protected]
wrote:
Btw, Hex needs to store them in the database because they are lifetime and stored in the users computer and not something we can use just for a single session like tokens.
Reply to this email directly or view it on GitHub:
https://github.com/phoenixframework/phoenix/issues/699#issuecomment-79405405
So @jeregrine and I have been chatting about this on IRC, and this is a braindump of my thoughts and the types of authorization we have thought about it. Regardless of our choice of Plug vs JWT, we need to figure out the high-level API and usecases.
Two use-cases around tokens/encrypted data have come up:
allowed_topics: [], which is constructed server side. This bag of encrypted data is then passed to multiple channel/topic joins, decrypted and verified, then using the user_id, the channel join performs whatever authorization it needs. (this is similar to how controllers authorize resources based on a get_session(conn, :user_id).Here's an example focusing on token-per-topic auth so we can discuss these things around code, as well as my general thoughts.
From the beginning, Channel authorization has been all about the topic in my mind (number 1 above). A message is routed to a channel, and you return {:ok, socket} only if the payload is authorized for the topic. We need to keep this in mind for channel token generation and authorization.
_But_, I have also said that you can think of channels like bidirectional controllers (number 2 above). So they both have use-cases merits I think, we just need to sync ideas.
Example:
Jason and I are in the global "rooms:lobby" chat room and Jason and he wants to PM me. He sends chan.send("pm", {user: "chrismccord"}), which gets picked up:
defmodule RoomChannel do
def handle_in("pm", %{"user" => user}, socket) do
%{id: id} = PrivateRoom.create() # some store create the room and returns the id
broadcast! socket, "pm", %{
to: user,
from: socket.assigns["user"].
id: id,
topic: "pms:" <> id, # tell the client which topic to join
token: gen_token(socket, "pms:" <> id) # gen token based off private room topic
}
end
def handle_out("pm", %{to: user} = msg, %Socket{assigns: %{user: user}) do
reply socket, "pm:open", msg
end
def handle_out("pm", %{from: user} = msg, %Socket{assigns: %{user: user}) do
reply socket, "pm:open", Map.merge(msg, sent_by_me: true)
end
def handle_out("pm", _, socket), do: {:ok, socket}
end
defmodule PMChannel do
def join("pms:" <> id = topic, %{token: token}, socket) do
case verify_token(socket, token, topic) do
:ok -> {:ok, socket}
_ -> :ignore
end
end
end
socket.join("rooms:lobby", {}, chan => {
$privateMessage.click( e => {
chan.send("pm", {user: $(e.target).data("user")})
chan.on("pm:open", msg => {
socket.join(msg.topic, msg.token, pmChan => {
// we can just join with the topic/token we got
// handle the PM UI stuffs and bindings
})
})
})
})
Can we make both these things work, and work cleanly? Different approach?
@chrismccord Thanks for the write up and the code samples. :+1: from here.
Both use cases are the same in my opinion. In the first, you will just encrypt the topic you want to access. In the second, you are adding a payload (in this case, the user id) along side the topics you are allowed to access.
Here are the signatures:
generate_token(conn | socket, topic | [topic], payload) - generates a token giving access to the given topics. Once verified, it returns the given payload (we can default to nil or an empty map). topic can be "foo:bar" or "foo:*".verify_token(conn | socket, token, topic) - verifies the given token and that the token gives access to the given topic. Returns the payload.We should also consider if we want to add expiry dates for tokens (and we likely do). If so, it is just a matter of encoding the timestamp of when it was generated and verifying it on verify.
However, there is another approach to your example, which is to simply rely on the database. For example, in the private room we would do:
def handle_in("pm", %{"user" => user, "friend" => friend}, socket) do
%{id: id} = PrivateRoom.create(user, friend) # some store create the room and returns the id
broadcast! socket, "pm", %{
to: user,
from: socket.assigns["user"].
id: id,
topic: "pm:" <> id, # tell the client which topic to join
token: socket.assigns["token"] # same token as before
}
end
Now when we join the private message channel:
def join("pm:" <> id, %{token: token}, socket) do
user = get_user(socket, token)
query = PrivateRoom.with_user(user)
if Repo.get(query, id) do
# good
else
# bad
end
end
This is exactly the same code we will have in our controllers. We usually _authenticate_ the user based on external information (token or session) but rely on the database for _authorization_. So I would rather try to have an approach where we generate a single token per user and push the authorization to be based on the DB (as you will likely need the private room information inside the channel anyway).
If this is the case, I would even promote storing the token somewhere in Phoenix.Socket and automatically including it as part of the payload (similar to how browsers store the session and send it automatically too). Basically we would have something like this:
socket = new Phoenix.Socket()
socket.connect({"token": get_token_from_meta_or_js_whatever_just_get_it()}) // default payload
socket.on(...)
Its not uncommon for a token to encode what data it has access to. See google macroons paper on distributed auth 聽http://hackingdistributed.com/2014/05/16/macaroons-are-better-than-cookies/
I'm still on the side of letting the database or letting the user define a system of access. And use just giving arbitraury ways of encoding, decoding and verifying tokens.聽
Going with the plug route is fine for now. I think we could add these utility to functions to plug because they have a more broad appeal there too (just like plug has basic cookie sessions). Or just utility functions a user can import or call directly.
On Sat, Mar 14, 2015 at 8:41 AM, Jos茅 Valim [email protected]
wrote:
@chrismccord Thanks for the write up and the code samples. :+1: from here.
Both use cases are the same in my opinion. In the first, you will just encrypt the topic you want to access. In the second, you are adding a payload (in this case, the user id) along side the topics you are allowed to access.
Here are the signatures:
generate_token(conn | socket, topic | [topic], payload)- generates a token given access to the given topics, once verified, it returns the given payload (we can default to nil or an empty map). topic can be "foo:bar" or "foo:*".verify_token(conn | socket, token, topic)- verifies the given token and that it gives access to the given topic. Returns the payload.
We should also consider if we want to add expiry dates for tokens, if so, it is just a matter of encoding the timestamp of when it was generated and verifying it on verify (as an extra argument).
However, there is another approach for this, which is to simply rely on the database. For example, for the private room, one would do:def handle_in("pm", %{"user" => user, "friend" => friend}, socket) do %{id: id} = PrivateRoom.create(user, friend) # some store create the room and returns the id broadcast! socket, "pm", %{ to: user, from: socket.assigns["user"]. id: id, topic: "pm:" <> id, # tell the client which topic to join token: socket.assigns["token"] # same token as before } endNow when we join the private message channel:
def join("pm:" <> id, %{token: token}, socket) do user = get_user(socket, token) query = PrivateRoom.with_user(user) if Repo.get(query, id) do # authenticate in else # no room end endThis is exactly the same code we will have in our controllers. We usually _authenticate_ the user based on external information (token or session) but rely on the database for _authorization_. So I would rather try to have an approach where we generate a single token per user and push the authorization to be based on the DB (as you will likely need the private room information inside the channel anyway).
If this is the case, I would even promote storing the token somewhere in Phoenix.Socket and automatically including it as part of the payload (similar to how browsers store the session and send it automatically too). Basically we would have something like this:
socket = new Phoenix.Socket()
socket.connect({"token": get_token_from_meta_or_js_whatever_just_get_it()}) // pass default payloadsocket.on(...)
Reply to this email directly or view it on GitHub:
https://github.com/phoenixframework/phoenix/issues/699#issuecomment-80481826
Don't know if it still matters or not, but I did create an update for Joken with no dependencies and allows the selection of a json module via implementing a behaviour. Looking for any feedback but I think this will end up being the next version.
Just a heads up for all those following along. We're going to just use Plug based tokens. If anyone else has feedback let me know. Plan on implementing this for 0.12.0
Most helpful comment
Its not uncommon for a token to encode what data it has access to. See google macroons paper on distributed auth 聽http://hackingdistributed.com/2014/05/16/macaroons-are-better-than-cookies/
I'm still on the side of letting the database or letting the user define a system of access. And use just giving arbitraury ways of encoding, decoding and verifying tokens.聽
Going with the plug route is fine for now. I think we could add these utility to functions to plug because they have a more broad appeal there too (just like plug has basic cookie sessions). Or just utility functions a user can import or call directly.
On Sat, Mar 14, 2015 at 8:41 AM, Jos茅 Valim [email protected]
wrote: