Skip to content

Commit

Permalink
[GH-640] Endpoint authentication (#665)
Browse files Browse the repository at this point in the history
* Add guardian dependency and configuration for private/public key pairs

* Endpoint to fetch public key

* Add JWT verification to matchmaking websocket connection

* Add JWT verification to match websocket connection

* Replace GoogleUser association with User

* Remove usage of guardian for joken

* fixup google_user

* Modify GatewaySigner to actually fetch from gateway the public key

* Slight renaming and properly verifying and validating tokens

* Guest user sign in

* Formatting and credo checks

* Add refresh-token endpoint and add client_id to JWT

* Don't show play buttons unless logged in

* Add user_id to refresh response

* Add JWT_PRIVATE_KEY env var

* Fixing CI

* Fixing CI 2

* Add JWT_PRIVATE_KEY_BASE_64

* Fix env var decode

* Fix signer not found

* Fix signer not found for game_socket_handler

* Expect client_id in path

* game_client_web to send client_id

* Remove JWT_PRIVATE_KEY

* Handle JWT in quick_game_handler

* Minor naming fixes
  • Loading branch information
AminArria authored Jun 13, 2024
1 parent 38879a3 commit 1605aa4
Show file tree
Hide file tree
Showing 33 changed files with 328 additions and 80 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/central-brazil-testing-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
BOT_MANAGER_PORT: ${{ vars.BOT_MANAGER_PORT }}
BOT_MANAGER_HOST: ${{ vars.LOADTEST_CLIENT_HOST }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_PRIVATE_KEY_BASE_64: ${{ secrets.JWT_PRIVATE_KEY_BASE_64 }}
SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME }}
NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }}
Expand All @@ -68,6 +69,7 @@ jobs:
BOT_MANAGER_HOST=${BOT_MANAGER_HOST} \
DATABASE_URL=${DATABASE_URL} \
SECRET_KEY_BASE=${SECRET_KEY_BASE} \
JWT_PRIVATE_KEY_BASE_64=${JWT_PRIVATE_KEY_BASE_64} \
NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \
NEWRELIC_KEY=${NEWRELIC_KEY} \
/home/${SSH_USERNAME}/deploy-script/deploy.sh
2 changes: 2 additions & 0 deletions .github/workflows/central-europe-testing-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
BOT_MANAGER_HOST: ${{ vars.LOADTEST_CLIENT_HOST }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}
JWT_PRIVATE_KEY_BASE_64: ${{ secrets.JWT_PRIVATE_KEY_BASE_64 }}
NEWRELIC_APP_NAME: ${{ vars.NEWRELIC_APP_NAME }}
NEWRELIC_KEY: ${{ secrets.NEWRELIC_KEY }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
Expand All @@ -70,6 +71,7 @@ jobs:
BOT_MANAGER_HOST=${BOT_MANAGER_HOST} \
DATABASE_URL=${DATABASE_URL} \
SECRET_KEY_BASE=${SECRET_KEY_BASE} \
JWT_PRIVATE_KEY_BASE_64=${JWT_PRIVATE_KEY_BASE_64} \
NEWRELIC_APP_NAME=${NEWRELIC_APP_NAME} \
NEWRELIC_KEY=${NEWRELIC_KEY} \
/home/${SSH_USERNAME}/deploy-script/deploy.sh
1 change: 1 addition & 0 deletions apps/arena/lib/arena/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule Arena.Application do
Arena.GameLauncher,
Arena.GameBountiesFetcher,
Arena.GameTracker,
Arena.Authentication.GatewaySigner,
# Start a worker by calling: Arena.Worker.start_link(arg)
# {Arena.Worker, arg},
# Start to serve requests, typically the last entry
Expand Down
47 changes: 47 additions & 0 deletions apps/arena/lib/arena/authentication/gateway_signer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule Arena.Authentication.GatewaySigner do
@moduledoc """
GenServer that calls gateway to fetch public key used for JWT authentication
The public key is converted into a Joken.Signer and cached for internal app usage
"""
use GenServer

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

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

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

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

@impl true
def handle_info(:fetch_signer, state) do
gateway_url = Application.get_env(:arena, :gateway_url)

result =
Finch.build(:get, "#{gateway_url}/auth/public-key", [{"content-type", "application/json"}])
|> Finch.request(Arena.Finch)

case result do
{:ok, %Finch.Response{status: 200, body: body}} ->
Process.send_after(self(), :fetch_signer, 3_600_000)
%{"jwk" => jwk} = Jason.decode!(body)
signer = Joken.Signer.create("Ed25519", jwk)
{:noreply, Map.put(state, :signer, signer)}

_else_error ->
Process.send_after(self(), :fetch_signer, 5_000)
{:noreply, state}
end
end
end
12 changes: 12 additions & 0 deletions apps/arena/lib/arena/authentication/gateway_token_manager.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Arena.Authentication.GatewayTokenManager do
@moduledoc """
Module responsible to verify and validate the JWT emitted by gateway app.
"""
use Joken.Config, default_signer: nil

@impl Joken.Config
def token_config do
default_exp = Application.get_env(:joken, :default_exp)
default_claims(default_exp: default_exp)
end
end
16 changes: 15 additions & 1 deletion apps/arena/lib/arena/game_socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule Arena.GameSocketHandler do
Module that handles cowboy websocket requests
"""
require Logger
alias Arena.Authentication.GatewaySigner
alias Arena.Authentication.GatewayTokenManager
alias Arena.Utils
alias Arena.Serialization
alias Arena.GameUpdater
Expand All @@ -14,7 +16,19 @@ defmodule Arena.GameSocketHandler do

@impl true
def init(req, _opts) do
client_id = :cowboy_req.binding(:client_id, req)
## 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])

Expand Down
8 changes: 6 additions & 2 deletions apps/arena/lib/arena/quick_game_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule Arena.QuickGameHandler do
@moduledoc """
Module that handles cowboy websocket requests
"""
alias Arena.Authentication.GatewayTokenManager
alias Arena.Authentication.GatewaySigner
alias Arena.GameLauncher
alias Arena.Serialization.GameState
alias Arena.Serialization.JoinedLobby
Expand All @@ -11,10 +13,12 @@ defmodule Arena.QuickGameHandler do

@impl true
def init(req, _opts) do
client_id = :cowboy_req.binding(:client_id, req)
[{"gateway_jwt", jwt}] = :cowboy_req.parse_qs(req)
signer = GatewaySigner.get_signer()
{:ok, %{"sub" => user_id}} = GatewayTokenManager.verify_and_validate(jwt, signer)
character_name = :cowboy_req.binding(:character_name, req)
player_name = :cowboy_req.binding(:player_name, req)
{:cowboy_websocket, req, %{client_id: client_id, character_name: character_name, player_name: player_name}}
{:cowboy_websocket, req, %{client_id: user_id, character_name: character_name, player_name: player_name}}
end

@impl true
Expand Down
8 changes: 6 additions & 2 deletions apps/arena/lib/arena/socket_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule Arena.SocketHandler do
Module that handles cowboy websocket requests
"""
require Logger
alias Arena.Authentication.GatewaySigner
alias Arena.Authentication.GatewayTokenManager
alias Arena.GameLauncher
alias Arena.Serialization.GameState
alias Arena.Serialization.JoinedLobby
Expand All @@ -14,10 +16,12 @@ defmodule Arena.SocketHandler do

@impl true
def init(req, _opts) do
client_id = :cowboy_req.binding(:client_id, req)
[{"gateway_jwt", jwt}] = :cowboy_req.parse_qs(req)
signer = GatewaySigner.get_signer()
{:ok, %{"sub" => user_id}} = GatewayTokenManager.verify_and_validate(jwt, signer)
character_name = :cowboy_req.binding(:character_name, req)
player_name = :cowboy_req.binding(:player_name, req)
{:cowboy_websocket, req, %{client_id: client_id, character_name: character_name, player_name: player_name}}
{:cowboy_websocket, req, %{client_id: user_id, character_name: character_name, player_name: player_name}}
end

@impl true
Expand Down
3 changes: 2 additions & 1 deletion apps/arena/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ defmodule Arena.MixProject do
{:exbase58, "~> 1.0.2"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:plug_cowboy, "~> 2.5"},
{:toxiproxy_ex, "~> 1.1.1"}
{:toxiproxy_ex, "~> 1.1.1"},
{:joken, "~> 2.6"}
]
end

Expand Down
42 changes: 19 additions & 23 deletions apps/game_backend/lib/game_backend/curse_of_mirra/matches.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule GameBackend.CurseOfMirra.Matches do
def create_arena_match_results(match_id, results) do
Multi.new()
|> create_arena_match_results(match_id, results)
|> add_google_users_to_multi(results)
|> add_users_to_multi(results)
|> give_prestige(results)
|> maybe_complete_quests()
|> complete_or_fail_bounties(results)
Expand All @@ -29,34 +29,30 @@ defmodule GameBackend.CurseOfMirra.Matches do

defp create_arena_match_results(multi, match_id, results) do
Enum.reduce(results, multi, fn result, multi ->
attrs =
Map.put(result, "google_user_id", result["user_id"])
|> Map.put("match_id", match_id)

attrs = Map.put(result, "match_id", match_id)
changeset = ArenaMatchResult.changeset(%ArenaMatchResult{}, attrs)
Multi.insert(multi, {:insert, result["user_id"]}, changeset)
end)
end

defp add_google_users_to_multi(multi, results) do
Multi.run(multi, :get_google_users, fn repo, _changes_so_far ->
google_users =
defp add_users_to_multi(multi, results) do
Multi.run(multi, :get_users, fn repo, _changes_so_far ->
users =
Enum.map(results, fn result -> result["user_id"] end)
|> Users.get_google_users_with_todays_daily_quests(repo)
|> Users.get_users_with_todays_daily_quests(repo)

{:ok, google_users}
{:ok, users}
end)
end

defp give_prestige(multi, results) do
prestige_config = Application.get_env(:game_backend, :arena_prestige)

Enum.reduce(results, multi, fn result, transaction_acc ->
Multi.run(transaction_acc, {:update_prestige, result["user_id"]}, fn repo, %{get_google_users: google_users} ->
google_user =
Enum.find(google_users, fn google_user -> google_user.id == result["user_id"] end)
Multi.run(transaction_acc, {:update_prestige, result["user_id"]}, fn repo, %{get_users: users} ->
user = Enum.find(users, fn user -> user.id == result["user_id"] end)

Enum.find(google_user.user.units, fn unit -> unit.character.name == result["character"] end)
Enum.find(user.units, fn unit -> unit.character.name == result["character"] end)
|> case do
nil ->
{:error, :unit_not_found}
Expand All @@ -75,14 +71,14 @@ defmodule GameBackend.CurseOfMirra.Matches do
defp maybe_complete_quests(multi) do
Multi.run(multi, :insert_completed_quests_result, fn _,
%{
get_google_users: google_users
get_users: users
} ->
correctly_updated_list =
Enum.map(google_users, fn
google_user ->
Quests.get_google_user_daily_quests_completed(google_user)
Enum.map(users, fn
user ->
Quests.get_user_daily_quests_completed(user)
|> Enum.map(fn %UserQuest{} = daily_quest ->
complete_quest_and_insert_currency(daily_quest, google_user.user.id)
complete_quest_and_insert_currency(daily_quest, user.id)
end)
end)
|> List.flatten()
Expand All @@ -99,17 +95,17 @@ defmodule GameBackend.CurseOfMirra.Matches do
Enum.filter(results, fn result -> result["bounty_quest_id"] != nil end)
|> Enum.reduce(multi, fn result, multi ->
Multi.run(multi, {:complete_or_fail_bounty, result["user_id"]}, fn repo,
%{get_google_users: google_users} =
%{get_users: users} =
changes_so_far ->
google_user = Enum.find(google_users, fn google_user -> google_user.id == result["user_id"] end)
user = Enum.find(users, fn user -> user.id == result["user_id"] end)

inserted_result =
Map.get(changes_so_far, {:insert, result["user_id"]})

user_quest_attrs =
%{
quest_id: result["bounty_quest_id"],
user_id: google_user.user.id,
user_id: user.id,
status: "available"
}

Expand All @@ -120,7 +116,7 @@ defmodule GameBackend.CurseOfMirra.Matches do
|> repo.preload([:quest])

if Quests.completed_quest?(user_quest, [inserted_result]) do
complete_quest_and_insert_currency(user_quest, google_user.user.id)
complete_quest_and_insert_currency(user_quest, user.id)
else
UserQuest.changeset(user_quest, %{status: "failed"})
|> repo.update()
Expand Down
5 changes: 2 additions & 3 deletions apps/game_backend/lib/game_backend/curse_of_mirra/quests.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ defmodule GameBackend.CurseOfMirra.Quests do
alias GameBackend.Utils
alias GameBackend.Users.Currencies.CurrencyCost
alias GameBackend.Users.Currencies
alias GameBackend.Users.GoogleUser
alias GameBackend.Users.User
alias GameBackend.Quests.UserQuest
alias GameBackend.Repo
Expand Down Expand Up @@ -176,9 +175,9 @@ defmodule GameBackend.CurseOfMirra.Quests do
end
end

def get_google_user_daily_quests_completed(%GoogleUser{
def get_user_daily_quests_completed(%User{
arena_match_results: arena_match_results,
user: %User{user_quests: user_quests}
user_quests: user_quests
}) do
user_quests
|> Enum.reduce([], fn user_quest, acc ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule GameBackend.Matches.ArenaMatchResult do
field(:duration_ms, :integer)
timestamps()

belongs_to(:google_user, GameBackend.Users.GoogleUser)
belongs_to(:user, GameBackend.Users.User)
end

@required [
Expand All @@ -30,7 +30,7 @@ defmodule GameBackend.Matches.ArenaMatchResult do
:deaths,
:character,
:match_id,
:google_user_id,
:user_id,
:position,
:damage_done,
:damage_taken,
Expand Down
27 changes: 18 additions & 9 deletions apps/game_backend/lib/game_backend/users.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ defmodule GameBackend.Users do
|> Repo.insert()
end

def create_guest_user() do
CurseUsers.create_user_params()
|> register_user()
end

@doc """
Gets a single user.
Returns {:ok, User}.
Expand All @@ -60,10 +65,10 @@ defmodule GameBackend.Users do
## Examples
iex> get_google_users_with_todays_daily_quests(["51646f3a-d9e9-4ce6-8341-c90b8cad3bdf"])
iex> get_users_with_todays_daily_quests(["51646f3a-d9e9-4ce6-8341-c90b8cad3bdf"])
[%GoogleUser{}]
"""
def get_google_users_with_todays_daily_quests(ids, repo \\ Repo) do
def get_users_with_todays_daily_quests(ids, repo \\ Repo) do
naive_today = NaiveDateTime.utc_now()
start_of_date = NaiveDateTime.beginning_of_day(naive_today)
end_of_date = NaiveDateTime.end_of_day(naive_today)
Expand All @@ -84,15 +89,13 @@ defmodule GameBackend.Users do
)

q =
from(u in GoogleUser,
from(u in User,
where: u.id in ^ids,
preload: [
arena_match_results: ^arena_match_result_subquery,
user: [
currencies: :currency,
units: :character,
user_quests: ^daily_quest_subquery
]
currencies: :currency,
units: :character,
user_quests: ^daily_quest_subquery
]
)

Expand Down Expand Up @@ -170,7 +173,13 @@ defmodule GameBackend.Users do
{:ok, %GoogleUser{}}
"""
def find_or_create_google_user_by_email(email) do
case Repo.get_by(GoogleUser, email: email) do
q =
from(gu in GoogleUser,
where: gu.email == ^email,
preload: [:user]
)

case Repo.one(q) do
nil -> create_google_user_by_email(email)
user -> {:ok, user}
end
Expand Down
Loading

0 comments on commit 1605aa4

Please sign in to comment.