Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-678] Fix bot authentication #703

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions apps/arena/lib/arena/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,10 @@ defmodule Arena.GameSocketHandler do

@impl true
def init(req, _opts) do
## TODO: The only reason we need this is because bots are broken, we should fix bots in a way that
## we don't need to pass a real user_id (or none at all). Ideally we could have JWT that says "Bot Sever".
client_id =
case :cowboy_req.parse_qs(req) do
[{"gateway_jwt", jwt}] ->
signer = GatewaySigner.get_signer()
{:ok, %{"sub" => user_id}} = GatewayTokenManager.verify_and_validate(jwt, signer)
user_id

_ ->
:cowboy_req.binding(:client_id, req)
end

game_id = :cowboy_req.binding(:game_id, req)
game_pid = game_id |> Base58.decode() |> :erlang.binary_to_term([:safe])

{:cowboy_websocket, req, %{client_id: client_id, game_pid: game_pid, game_id: game_id}}
user_id = get_user_id(req)
{:cowboy_websocket, req, %{client_id: user_id, game_pid: game_pid, game_id: game_id}}
end

@impl true
Expand Down Expand Up @@ -244,5 +231,21 @@ defmodule Arena.GameSocketHandler do
end
end

defp handle_decoded_message(_, _), do: nil
defp handle_decoded_message(_, _),
do: nil

defp get_user_id(req) do
signer = GatewaySigner.get_signer()

case :cowboy_req.parse_qs(req) do
[{"gateway_jwt", jwt}] ->
{:ok, %{"sub" => user_id}} = GatewayTokenManager.verify_and_validate(jwt, signer)
user_id

[{"bot_token", token}, {"bot_secret", secret}] ->
hashed_secret = :crypto.hash(:sha256, secret) |> Base.url_encode64()
{:ok, %{"bot" => ^hashed_secret}} = GatewayTokenManager.verify_and_validate(token, signer)
:cowboy_req.binding(:client_id, req)
end
end
end
3 changes: 2 additions & 1 deletion apps/arena_load_test/lib/arena_load_test/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ defmodule ArenaLoadTest.Application do
def start(_type, _args) do
children = [
ArenaLoadTest.SocketSupervisor,
ArenaLoadTest.LoadtestManager
ArenaLoadTest.LoadtestManager,
{Finch, name: ArenaLoadTest.Finch}
# Starts a worker by calling: ArenaLoadTest.Worker.start_link(arg)
# {ArenaLoadTest.Worker, arg}
]
Expand Down
13 changes: 8 additions & 5 deletions apps/arena_load_test/lib/arena_load_test/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ defmodule ArenaLoadTest.GameSocketHandler do
alias ArenaLoadTest.Utils
use WebSockex, restart: :transient

def start_link({client_id, game_id}) do
ws_url = ws_url(client_id, game_id)
def start_link({client_id, user_token, game_id}) do
ws_url = ws_url(client_id, user_token, game_id)

WebSockex.start_link(
ws_url,
__MODULE__,
%{
client_id: client_id,
user_token: user_token,
game_id: game_id
}
)
Expand Down Expand Up @@ -109,15 +110,17 @@ defmodule ArenaLoadTest.GameSocketHandler do
])
end

defp ws_url(client_id, game_id) do
defp ws_url(client_id, user_token, game_id) do
query_params = "gateway_jwt=#{user_token}"

case System.get_env("TARGET_SERVER") do
nil ->
"ws://localhost:4000/play/#{game_id}/#{client_id}"
"ws://localhost:4000/play/#{game_id}/#{client_id}?#{query_params}"

target_server ->
# TODO Replace this for a SSL connection using erlang credentials.
# TODO https://github.com/lambdaclass/mirra_backend/issues/493
"ws://#{Utils.get_server_ip(target_server)}:4000/play/#{game_id}/#{client_id}"
"ws://#{Utils.get_server_ip(target_server)}:4000/play/#{game_id}/#{client_id}?#{query_params}"
end
end

Expand Down
55 changes: 35 additions & 20 deletions apps/arena_load_test/lib/arena_load_test/socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ defmodule ArenaLoadTest.SocketHandler do
alias ArenaLoadTest.Utils

def start_link(client_id) do
ws_url = ws_url(client_id)
user_token = create_user(client_id)
ws_url = ws_url(client_id, user_token)

WebSockex.start_link(
ws_url,
__MODULE__,
%{
client_id: client_id
client_id: client_id,
user_token: user_token
}
)
end
Expand All @@ -29,43 +31,44 @@ defmodule ArenaLoadTest.SocketHandler do
end

@impl true
def handle_frame({:binary, game_state}, state) do
game_id = Serialization.GameState.decode(game_state).game_id
def handle_frame({:binary, lobby_event}, state) do
case Serialization.LobbyEvent.decode(lobby_event) do
%{event: {:game, %{game_id: game_id}}} ->
case :ets.lookup(:clients, state.client_id) do
[{client_id, _}] ->
:ets.delete(:clients, client_id)

case :ets.lookup(:clients, state.client_id) do
[{client_id, _}] ->
:ets.delete(:clients, client_id)
[] ->
raise KeyError, message: "Client with ID #{state.client_id} doesn't exist."
end

[] ->
raise KeyError, message: "Client with ID #{state.client_id} doesn't exist."
end
{:ok, pid} = SocketSupervisor.add_new_player(state.client_id, state.user_token, game_id)

{:ok, pid} =
SocketSupervisor.add_new_player(
state.client_id,
game_id
)
true = :ets.insert(:players, {state.client_id, game_id})

true = :ets.insert(:players, {state.client_id, game_id})
Process.send(pid, :send_action, [])

Process.send(pid, :send_action, [])
_ ->
:nothing
end

{:ok, state}
end

# Private
defp ws_url(player_id) do
defp ws_url(player_id, user_token) do
character = get_random_active_character()
player_name = "Player_#{player_id}"
query_params = "gateway_jwt=#{user_token}"

case System.get_env("TARGET_SERVER") do
nil ->
"ws://localhost:4000/join/#{player_id}/#{character}/#{player_name}"
"ws://localhost:4000/join/#{player_id}/#{character}/#{player_name}?#{query_params}"

target_server ->
# TODO Replace this for a SSL connection using erlang credentials.
# TODO https://github.com/lambdaclass/mirra_backend/issues/493
"ws://#{Utils.get_server_ip(target_server)}:4000/join/#{player_id}/#{character}/#{player_name}"
"ws://#{Utils.get_server_ip(target_server)}:4000/join/#{player_id}/#{character}/#{player_name}?#{query_params}"
end
end

Expand All @@ -75,4 +78,16 @@ defmodule ArenaLoadTest.SocketHandler do
["muflus", "h4ck", "uma"]
|> Enum.random()
end

defp create_user(client_id) do
gateway_url = Application.get_env(:arena_load_test, :gateway_url)
payload = Jason.encode!(%{client_id: to_string(client_id)})

{:ok, %{status: 200, body: body}} =
Finch.build(:post, "#{gateway_url}/curse/users", [{"content-type", "application/json"}], payload)
|> Finch.request(ArenaLoadTest.Finch)

Jason.decode!(body)
|> Map.get("gateway_jwt")
end
end
4 changes: 2 additions & 2 deletions apps/arena_load_test/lib/arena_load_test/socket_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ defmodule ArenaLoadTest.SocketSupervisor do
@doc """
Initializes a websocket that handles the client connection in-game.
"""
def add_new_player(client_id, game_id) do
def add_new_player(client_id, user_token, game_id) do
DynamicSupervisor.start_child(
__MODULE__,
{GameSocketHandler, {client_id, game_id}}
{GameSocketHandler, {client_id, user_token, game_id}}
)
end

Expand Down
3 changes: 2 additions & 1 deletion apps/arena_load_test/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ defmodule ArenaLoadTest.MixProject do
defp deps do
[
{:websockex, "~> 0.4.3"},
{:protobuf, "~> 0.12.0"}
{:protobuf, "~> 0.12.0"},
{:finch, "~> 0.13"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
# {:sibling_app_in_umbrella, in_umbrella: true}
Expand Down
4 changes: 3 additions & 1 deletion apps/bot_manager/lib/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ defmodule BotManager.Application do
def start(_type, _args) do
children = [
BotManager.BotSupervisor,
{Plug.Cowboy, Application.get_env(:bot_manager, :end_point_configuration)}
{Plug.Cowboy, Application.get_env(:bot_manager, :end_point_configuration)},
{Finch, name: BotManager.Finch},
BotManager.TokenFetcher
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
5 changes: 3 additions & 2 deletions apps/bot_manager/lib/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,12 @@ defmodule BotManager.GameSocketHandler do
"arena_host" => arena_host
}) do
Logger.info("Connecting bot with client: #{bot_client} to game: #{game_id} in the server: #{arena_host}")
%{token: token, secret: secret} = BotManager.TokenFetcher.get_auth()

if arena_host == "localhost" do
"ws://localhost:4000/play/#{game_id}/#{bot_client}"
"ws://localhost:4000/play/#{game_id}/#{bot_client}?bot_token=#{token}&bot_secret=#{secret}"
else
"wss://#{arena_host}/play/#{game_id}/#{bot_client}"
"wss://#{arena_host}/play/#{game_id}/#{bot_client}?bot_token=#{token}&bot_secret=#{secret}"
end
end
end
47 changes: 47 additions & 0 deletions apps/bot_manager/lib/token_fetcher.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule BotManager.TokenFetcher do
@moduledoc """
GenServer that calls gateway to create and refresh a JWT token used by the bots
"""
use GenServer

def get_auth() do
GenServer.call(__MODULE__, :get_auth)
end

def start_link(_args) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end

@impl true
def init(_) do
Process.send_after(self(), :fetch_token, 500)
{:ok, %{}}
end

@impl true
def handle_call(:get_auth, _, state) do
{:reply, state, state}
end

@impl true
def handle_info(:fetch_token, state) do
gateway_url = Application.get_env(:bot_manager, :gateway_url)
secret = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
payload = Jason.encode!(%{"bot_secret" => secret})

result =
Finch.build(:post, "#{gateway_url}/auth/generate-bot-token", [{"content-type", "application/json"}], payload)
|> Finch.request(BotManager.Finch)

case result do
{:ok, %Finch.Response{status: 200, body: body}} ->
Process.send_after(self(), :fetch_token, 1_800_000)
%{"token" => token} = Jason.decode!(body)
{:noreply, %{token: token, secret: secret}}

_else_error ->
Process.send_after(self(), :fetch_token, 5_000)
{:noreply, state}
end
end
end
3 changes: 2 additions & 1 deletion apps/bot_manager/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ defmodule BotManager.MixProject do
{:jason, "~> 1.2"},
{:websockex, "~> 0.4.3"},
{:exbase58, "~> 1.0.2"},
{:protobuf, "~> 0.12.0"}
{:protobuf, "~> 0.12.0"},
{:finch, "~> 0.13"}
]
end
end
10 changes: 10 additions & 0 deletions apps/gateway/lib/gateway/auth/token_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ defmodule Gateway.Auth.TokenManager do
token
end

def generate_bot_token(bot_secret) do
hash_bot_secret =
:crypto.hash(:sha256, bot_secret)
|> Base.url_encode64()

extra_claims = %{"bot" => hash_bot_secret}
{:ok, token, _claims} = generate_and_sign(extra_claims)
token
end

@impl Joken.Config
def token_config do
default_exp = Application.get_env(:joken, :default_exp)
Expand Down
5 changes: 5 additions & 0 deletions apps/gateway/lib/gateway/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@ defmodule Gateway.Controllers.AuthController do
send_resp(conn, 400, Jason.encode!(%{error: "bad_request"}))
end
end

def generate_bot_token(conn, %{"bot_secret" => bot_secret}) do
token = TokenManager.generate_bot_token(bot_secret)
send_resp(conn, 200, Jason.encode!(%{token: token}))
end
end
1 change: 1 addition & 0 deletions apps/gateway/lib/gateway/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ defmodule Gateway.Router do
get "/auth/:provider/token/:token_id/:client_id", Controllers.AuthController, :validate_token
get "/auth/public-key", Controllers.AuthController, :public_key
post "/auth/refresh-token", Controllers.AuthController, :refresh_token
post "/auth/generate-bot-token", Controllers.AuthController, :generate_bot_token

put "/users/:user_id", Controllers.UserController, :update
end
Expand Down
8 changes: 8 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,17 @@ end

bot_manager_port = String.to_integer(System.get_env("BOT_MANAGER_PORT") || "4003")

config :bot_manager, :gateway_url, System.get_env("GATEWAY_URL") || "http://localhost:4001"

config :bot_manager, :end_point_configuration,
scheme: :http,
plug: BotManager.Endpoint,
options: [port: bot_manager_port]

###################################
# App configuration: Bot Manager #
###################################

config :arena_load_test, :gateway_url, System.get_env("GATEWAY_URL") || "http://localhost:4001"

###################################
Loading