Move to discord oauth

This commit is contained in:
Logan Hunt 2023-01-31 14:01:35 -07:00
parent e5d97870a1
commit cac78a4f4e
No known key found for this signature in database
GPG Key ID: 8AC6A4B840C0EC49
15 changed files with 82 additions and 55 deletions

View File

@ -7,12 +7,15 @@ POSTGRES_PASSWORD=password
POSTGRES_DATABASE=chessh POSTGRES_DATABASE=chessh
WEB_PORT=8080 WEB_PORT=8080
REACT_APP_GITHUB_OAUTH=https://github.com/login/oauth/authorize?client_id=CLIENT_ID_HERE&redirect_uri=http://127.0.0.1:3000/api/oauth/redirect
REACT_APP_DISCORD_INVITE=https://discord.gg/ajsdlkfjaskldjf
REACT_APP_DISCORD_OAUTH=https://discord.com/api/oauth2/authorize?client_id=CLIENT_ID&redirect_uri=FRONTEND_REDIRECT_PORT_FROM_BELOW&response_type=code&scope=identify
CLIENT_REDIRECT_AFTER_OAUTH=http://127.0.0.1:3000/auth-successful CLIENT_REDIRECT_AFTER_OAUTH=http://127.0.0.1:3000/auth-successful
GITHUB_CLIENT_ID= DISCORD_CLIENT_ID=
GITHUB_CLIENT_SECRET= DISCORD_CLIENT_SECRET=
GITHUB_USER_AGENT= DISCORD_USER_AGENT=
JWT_SECRET=aVerySecretJwtSigningSecret JWT_SECRET=aVerySecretJwtSigningSecret

View File

@ -8,7 +8,7 @@ docker build . -t chessh/server
cd front cd front
docker build \ docker build \
--build-arg REACT_APP_GITHUB_OAUTH=${REACT_APP_GITHUB_OAUTH} \ --build-arg REACT_APP_DISCORD_OAUTH=${REACT_APP_DISCORD_OAUTH} \
--build-arg REACT_APP_SSH_SERVER=${REACT_APP_SSH_SERVER} \ --build-arg REACT_APP_SSH_SERVER=${REACT_APP_SSH_SERVER} \
--build-arg REACT_APP_SSH_PORT=${REACT_APP_SSH_PORT} \ --build-arg REACT_APP_SSH_PORT=${REACT_APP_SSH_PORT} \
. -t chessh/frontend . -t chessh/frontend

View File

@ -17,8 +17,9 @@ config :chessh, RateLimits,
create_game_rate: 2 create_game_rate: 2
config :chessh, Web, config :chessh, Web,
github_oauth_login_url: "https://github.com/login/oauth/access_token", discord_oauth_login_url: "https://discord.com/api/oauth2/token",
github_user_api_url: "https://api.github.com/user" discord_user_api_url: "https://discord.com/api/users/@me",
discord_scope: "identify"
config :joken, default_signer: "secret" config :joken, default_signer: "secret"

View File

@ -1,14 +1,14 @@
import Config import Config
config :chessh, config :chessh,
ssh_port: String.to_integer(System.get_env("SSH_PORT", "42069")) ssh_port: String.to_integer(System.get_env("SSH_PORT", "34355"))
config :chessh, Web, config :chessh, Web,
github_client_id: System.get_env("GITHUB_CLIENT_ID"), discord_client_id: System.get_env("DISCORD_CLIENT_ID"),
github_client_secret: System.get_env("GITHUB_CLIENT_SECRET"), discord_client_secret: System.get_env("DISCORD_CLIENT_SECRET"),
github_user_agent: System.get_env("GITHUB_USER_AGENT"), discord_user_agent: System.get_env("DISCORD_USER_AGENT"),
client_redirect_after_successful_sign_in: client_redirect_after_successful_sign_in:
System.get_env("CLIENT_REDIRECT_AFTER_OAUTH", "http://localhost:3000"), System.get_env("CLIENT_REDIRECT_AFTER_OAUTH", "http://127.0.0.1:3000/oauth-successfule"),
port: String.to_integer(System.get_env("WEB_PORT", "8080")) port: String.to_integer(System.get_env("WEB_PORT", "8080"))
config :joken, config :joken,

View File

@ -8,10 +8,10 @@ RUN npm ci
COPY . /usr/app COPY . /usr/app
ARG REACT_APP_GITHUB_OAUTH ARG REACT_APP_DISCORD_OAUTH
ARG REACT_APP_SSH_SERVER ARG REACT_APP_SSH_SERVER
ARG REACT_APP_SSH_PORT ARG REACT_APP_SSH_PORT
ENV REACT_APP_GITHUB_OAUTH $REACT_APP_GITHUB_OAUTH ENV REACT_APP_DISCORD_OAUTH $REACT_APP_DISCORD_OAUTH
ENV REACT_APP_SSH_SERVER $REACT_APP_SSH_SERVER ENV REACT_APP_SSH_SERVER $REACT_APP_SSH_SERVER
ENV REACT_APP_SSH_PORT $REACT_APP_SSH_PORT ENV REACT_APP_SSH_PORT $REACT_APP_SSH_PORT
RUN npm run build RUN npm run build

View File

@ -34,8 +34,11 @@ export const Root = () => {
</> </>
) : ( ) : (
<> <>
<a href={process.env.REACT_APP_GITHUB_OAUTH} className="button"> <a
🐙 Login w/ GitHub 🐙 href={process.env.REACT_APP_DISCORD_OAUTH}
className="button"
>
👾 Login w/ Discord 👾
</a> </a>
</> </>
)} )}

View File

@ -18,6 +18,14 @@ export const Home = () => {
<hr /> <hr />
<h3>Getting Started</h3> <h3>Getting Started</h3>
<ol> <ol>
<div>
<li>
Consider joining the{" "}
<a href={process.env.REACT_APP_DISCORD_INVITE}>CheSSH Discord</a>{" "}
to receive notifications when other players are looking for
opponents, or when it is your move in a game.
</li>
</div>
<div> <div>
<li> <li>
Add a <Link to="/keys">public key</Link>, or{" "} Add a <Link to="/keys">public key</Link>, or{" "}
@ -50,11 +58,14 @@ export const Home = () => {
/> />
</div> </div>
<div> <div>
<li>Finally, play chess!</li> <li>
<p>Ideally, keeping the following contols in mind:</p> Finally, play chess! Ideally, keeping the following contols in
mind:
</li>
<ul> <ul>
<li>Ctrl + b / Escape to return to the main menu.</li> <li>Ctrl + b / Escape to return to the main menu.</li>
<li>Ctrl + c / Ctrl + d to exit at any point.</li> <li>Ctrl + c / Ctrl + d to exit CheSSH at any point.</li>
<li>Arrow keys to move around the board.</li> <li>Arrow keys to move around the board.</li>
<li> <li>
Select a piece with "enter", and move it to a square by pressing Select a piece with "enter", and move it to a square by pressing

View File

@ -5,7 +5,7 @@ defmodule Chessh.Player do
@derive {Inspect, except: [:password]} @derive {Inspect, except: [:password]}
schema "players" do schema "players" do
field(:github_id, :integer) field(:discord_id, :string)
field(:username, :string) field(:username, :string)
@ -24,7 +24,7 @@ defmodule Chessh.Player do
defimpl Jason.Encoder, for: Chessh.Player do defimpl Jason.Encoder, for: Chessh.Player do
def encode(value, opts) do def encode(value, opts) do
Jason.Encode.map( Jason.Encode.map(
Map.take(value, [:id, :github_id, :username, :created_at, :updated_at]), Map.take(value, [:id, :discord_id, :username, :created_at, :updated_at]),
opts opts
) )
end end
@ -37,7 +37,7 @@ defmodule Chessh.Player do
def registration_changeset(player, attrs, opts \\ []) do def registration_changeset(player, attrs, opts \\ []) do
player player
|> cast(attrs, [:username, :password, :github_id]) |> cast(attrs, [:username, :password, :discord_id])
|> validate_username() |> validate_username()
|> validate_password(opts) |> validate_password(opts)
end end

View File

@ -1,7 +1,5 @@
defmodule Chessh.SSH.Client do defmodule Chessh.SSH.Client do
alias IO.ANSI alias IO.ANSI
require Logger
use GenServer use GenServer
@clear_codes [ @clear_codes [
@ -164,6 +162,10 @@ defmodule Chessh.SSH.Client do
"\e[B" -> :down "\e[B" -> :down
"\e[D" -> :left "\e[D" -> :left
"\e[C" -> :right "\e[C" -> :right
"\eOA" -> :up
"\eOB" -> :down
"\eOD" -> :left
"\eOC" -> :right
"\r" -> :return "\r" -> :return
x -> x x -> x
end end

View File

@ -2,7 +2,6 @@ defmodule Chessh.Web.Endpoint do
alias Chessh.{Player, Repo, Key, PlayerSession} alias Chessh.{Player, Repo, Key, PlayerSession}
alias Chessh.Web.Token alias Chessh.Web.Token
use Plug.Router use Plug.Router
require Logger
import Ecto.Query import Ecto.Query
plug(Plug.Logger) plug(Plug.Logger)
@ -17,27 +16,33 @@ defmodule Chessh.Web.Endpoint do
plug(:dispatch) plug(:dispatch)
get "/oauth/redirect" do get "/oauth/redirect" do
[github_login_url, client_id, client_secret, github_user_api_url, github_user_agent] = [
get_github_configs() discord_login_url,
discord_scope,
client_id,
client_secret,
discord_user_api_url,
discord_user_agent,
redirect_uri
] = get_discord_configs()
resp = resp =
case conn.params do case conn.params do
%{"code" => req_token} -> %{"code" => req_token} ->
case :httpc.request( case :httpc.request(
:post, :post,
{String.to_charlist( {String.to_charlist(discord_login_url), [], 'application/x-www-form-urlencoded',
"#{github_login_url}?client_id=#{client_id}&client_secret=#{client_secret}&code=#{req_token}" 'scope=#{discord_scope}&client_id=#{client_id}&client_secret=#{client_secret}&code=#{req_token}&grant_type=authorization_code&redirect_uri=#{redirect_uri}'},
), [], 'application/json', ''},
[], [],
[] []
) do ) do
{:ok, {{_, 200, 'OK'}, _, resp}} -> {:ok, {{_, 200, 'OK'}, _, resp}} ->
URI.decode_query(String.Chars.to_string(resp)) Jason.decode!(String.Chars.to_string(resp))
end end
end end
{status, body} = {status, body} =
create_player_from_github_response(resp, github_user_api_url, github_user_agent) create_player_from_discord_response(resp, discord_user_api_url, discord_user_agent)
conn conn
|> assign_jwt_and_redirect_or_encode(status, body) |> assign_jwt_and_redirect_or_encode(status, body)
@ -200,14 +205,16 @@ defmodule Chessh.Web.Endpoint do
end) end)
end end
defp get_github_configs() do defp get_discord_configs() do
Enum.map( Enum.map(
[ [
:github_oauth_login_url, :discord_oauth_login_url,
:github_client_id, :discord_scope,
:github_client_secret, :discord_client_id,
:github_user_api_url, :discord_client_secret,
:github_user_agent :discord_user_api_url,
:discord_user_agent,
:client_redirect_after_successful_sign_in
], ],
fn key -> Application.get_env(:chessh, Web)[key] end fn key -> Application.get_env(:chessh, Web)[key] end
) )
@ -246,27 +253,27 @@ defmodule Chessh.Web.Endpoint do
end end
end end
defp create_player_from_github_response(resp, github_user_api_url, github_user_agent) do defp create_player_from_discord_response(resp, discord_user_api_url, discord_user_agent) do
case resp do case resp do
%{"access_token" => access_token} -> %{"access_token" => access_token} ->
case :httpc.request( case :httpc.request(
:get, :get,
{String.to_charlist(github_user_api_url), {String.to_charlist(discord_user_api_url),
[ [
{'Authorization', String.to_charlist("Bearer #{access_token}")}, {'Authorization', String.to_charlist("Bearer #{access_token}")},
{'User-Agent', github_user_agent} {'User-Agent', discord_user_agent}
]}, ]},
[], [],
[] []
) do ) do
{:ok, {{_, 200, 'OK'}, _, user_details}} -> {:ok, {{_, 200, 'OK'}, _, user_details}} ->
%{"login" => username, "id" => github_id} = %{"username" => username, "id" => discord_id} =
Jason.decode!(String.Chars.to_string(user_details)) Jason.decode!(String.Chars.to_string(user_details))
%Player{id: id} = %Player{id: id} =
Repo.insert!(%Player{github_id: github_id, username: username}, Repo.insert!(%Player{discord_id: discord_id, username: username},
on_conflict: [set: [github_id: github_id]], on_conflict: [set: [discord_id: discord_id]],
conflict_target: :github_id conflict_target: :discord_id
) )
{200, {200,
@ -283,7 +290,7 @@ defmodule Chessh.Web.Endpoint do
end end
_ -> _ ->
{400, %{errors: "Failed to retrieve token from GitHub. Try again."}} {400, %{errors: "Failed to retrieve token from Discord. Try again."}}
end end
end end
end end

View File

@ -5,13 +5,13 @@ defmodule Chessh.Repo.Migrations.CreatePlayer do
execute("CREATE EXTENSION IF NOT EXISTS citext", "") execute("CREATE EXTENSION IF NOT EXISTS citext", "")
create table(:players) do create table(:players) do
add(:github_id, :integer, null: false) add(:discord_id, :string, null: false)
add(:username, :citext, null: false) add(:username, :citext, null: false)
add(:hashed_password, :string, null: true) add(:hashed_password, :string, null: true)
timestamps() timestamps()
end end
create(unique_index(:players, [:username])) create(unique_index(:players, [:username]))
create(unique_index(:players, [:github_id])) create(unique_index(:players, [:discord_id]))
end end
end end

View File

@ -2,7 +2,7 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do
use ExUnit.Case use ExUnit.Case
alias Chessh.{Player, Repo} alias Chessh.{Player, Repo}
@valid_user %{username: "logan", password: "password", github_id: 1} @valid_user %{username: "logan", password: "password", discord_id: "1"}
setup_all do setup_all do
Ecto.Adapters.SQL.Sandbox.checkout(Repo) Ecto.Adapters.SQL.Sandbox.checkout(Repo)

View File

@ -2,7 +2,7 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
use ExUnit.Case use ExUnit.Case
alias Chessh.{Key, Repo, Player} alias Chessh.{Key, Repo, Player}
@valid_user %{username: "logan", password: "password", github_id: 2} @valid_user %{username: "logan", password: "password", discord_id: "2"}
@valid_key %{ @valid_key %{
name: "The Gamer Machine", name: "The Gamer Machine",
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44" key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44"

View File

@ -3,10 +3,10 @@ defmodule Chessh.Auth.UserRegistrationTest do
use ExUnit.Case use ExUnit.Case
alias Chessh.{Player, Repo} alias Chessh.{Player, Repo}
@valid_user %{username: "logan", password: "password", github_id: 4} @valid_user %{username: "logan", password: "password", discord_id: "4"}
@invalid_username %{username: "a", password: "password", github_id: 7} @invalid_username %{username: "a", password: "password", discord_id: "7"}
@invalid_password %{username: "aasdf", password: "pass", github_id: 6} @invalid_password %{username: "aasdf", password: "pass", discord_id: "6"}
@repeated_username %{username: "LoGan", password: "password", github_id: 5} @repeated_username %{username: "LoGan", password: "password", discord_id: "5"}
test "Password must be at least 8 characters and username must be at least 2" do test "Password must be at least 8 characters and username must be at least 2" do
refute Player.registration_changeset(%Player{}, @invalid_password).valid? refute Player.registration_changeset(%Player{}, @invalid_password).valid?

View File

@ -5,7 +5,7 @@ defmodule Chessh.SSH.AuthTest do
@localhost '127.0.0.1' @localhost '127.0.0.1'
@localhost_inet {{127, 0, 0, 1}, 1} @localhost_inet {{127, 0, 0, 1}, 1}
@key_name "The Gamer Machine" @key_name "The Gamer Machine"
@valid_user %{username: "logan", password: "password", github_id: 3} @valid_user %{username: "logan", password: "password", discord_id: "3"}
@client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys") @client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys")
@client_pub_key 'id_ed25519.pub' @client_pub_key 'id_ed25519.pub'