I am trying to set up a simple way to:
- Log a user in (auth them against separate database server),
- Manage their activity while logged in (likely by one GenServer per user), and
- Keep track of all logged in users (add to global registry ie. Syn).
I have never built a server or done this before. I have read a few Elixir books and many websites, but that is all. So I don’t know if anything is rational.
I am wondering if I have the right idea and if so, (1) how can I upgrade their websocket and send JWT back at same time, and (2) how I can set use their WebSocket from their GenServer (set a reference for it into GenServer state?).
Starting with the EchoServer
type websocket design as in the Plug documentation here, my idea conceptually is to do it like this:
AUTHENTICATION
1) User makes HTTP request to login
- User sends HTTPS request to server to login with embedded username/password or valid JWT for login in header.
- This is received in
use Plug.Router
module from demo Plug code above underget "/login"
hypothetically.
(i) Username/password:
- If user/password received, inside this function, send a HTTP request (or other communication) to database server to verify it.
- If database server replies okay, we create a JWT auth token and respond from this
get "/login"
to the user with it so they have a copy. - We also upgrade their connection all at once (if we can do both at once).
(ii) JWT:
- If JWT received, verify it inside this
get "/login"
function. - If valid, upgrade to websocket.
NEXT STEPS?
Now we have a connected verified websocket user. Now we need to:
- Start a per-user GenServer to track their state and set into it their token.
- Register the username and this GenServer as “logged in” in global registry (eg. Syn).
- Put into their GenServer some reference also for the websocket connection (EchoServer, conn, whatever?) so the GenServer can send or respond to the user messages through it.
So I think this could look something like this:
get "/login" do
# check headers for if there is a username/pass supplied
is_user_pass_bool = is_user_pass(conn.req_headers)
# username/pass found
token = if (is_user_pass_bool) do
# send HTTP Request or other communication to database server to validate user/pass
db_response = HTTPoison.post(db_url, db_msg_body, db_msg_headers) # whatever
valid_login = check_db_response(db_response)
# valid login, make a token
if (valid_login) do
make_token(conn.req_headers) # make the token
else
nil # no token to give, invalid login attempt, or db server broken
end
else
# try to pull a token out of header
extract_token_from_header(conn.req_headers)
end
# check if token is valid
is_valid_token_bool = validate(token)
# manage valid login attempt
if (is_valid_token_bool) do
# send back token copy and upgrade connection (can we do all at once?)
conn
|> put_resp_header([{"token", token}]) # put JWT token into header response in some manner
|> put_resp_content_type("text/plain")
|> send_resp(200, "okay, starting websocket with this token")
# (WILL THIS WORK? OR HAVE I LOST THE CONNECTION NOW BY SEND_RESP?)
conn
|> WebSockAdapter.upgrade(EchoServer, [], timeout: 60_000)
|> halt()
# create GenServer to manage user
socket_reference = ??? # how do i keep a reference to their socket to continue sending things to them in their GenServer?
user_gen_server = {UserGenServer, {token, socket_reference}}
start_result = Supervisor.start_link([user_gen_server], [strategy: :one_for_one, name: MainServer.Supervisor])
# check result and register user if succeeded
case (start_result) do
{:ok, gen_server_pid} ->
# add user to Syn database with user name and pid of their gen server
user_name = get_user_name(token)
:syn.register(:user_registry, user_name, gen_server_pid) # maybe also check if already registered...
_ ->
# something went wrong starting UserGenServer, try again or figure out problem
end
end
# manage bad login attempts (more detail can be added)
if (!valid_token_bool && !is_user_pass_bool) do
# bad or expired token, cleint must discard token and try another user/pass
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "bad token, require new username/pass attempt")
end
if (!valid_token_bool && is_user_pass_bool) do
# bad user/pass or server down, let them know
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "bad user name or server is screwed up, try again")
end
end
What do you think? Is this all remotely rational or how people would do this? I have never built a server before so I don’t know.
Two main points come to mind for me:
(1) Can I reply to their valid login attempt with their token (so they have a copy of it) and also upgrade them to a websocket at the same time as I did? Or does sending the response with the token break the connection? How does this work?
(2) How do I reference or use their websocket from inside the UserGenServer I created? Ie. How do I run a function from the UserGenServer that sends the client a WebSocket message saying “Hello”? Or how do I based on a given WebSocket message the client sends me like “Get new data” trigger their GenServer to fun a certain function to accomplish this and reply?
Thanks for any further help or ideas.