From 110eb0b1990d5d5ee77f9368a9b7653cfdc07131 Mon Sep 17 00:00:00 2001 From: Simponic Date: Mon, 19 Dec 2022 20:45:01 -0700 Subject: [PATCH] Implement public key and add tests --- config/config.exs | 4 ++- lib/chessh/auth/keys.ex | 17 +++++++++--- lib/chessh/auth/password.ex | 4 +-- lib/chessh/schema/key.ex | 1 + lib/chessh/schema/player.ex | 2 +- test/auth/password_test.exs | 27 +++++++++++++++++++ test/auth/pubkey_test.exs | 35 ++++++++++++++++++++++++ test/schema/key_test.exs | 1 - test/schema/register_test.exs | 51 +++++++++++++++++++++++++++++++++++ test/support/repo_case.ex | 2 -- 10 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 test/auth/password_test.exs create mode 100644 test/auth/pubkey_test.exs create mode 100644 test/schema/register_test.exs diff --git a/config/config.exs b/config/config.exs index bafbdfe..6fb4258 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,6 +8,8 @@ config :esshd, handler: {Chessh.Shell, :on_shell, 4}, port: 42069, password_authenticator: Chessh.Auth.PasswordAuthenticator, - public_key_authenticator: Chessh.Auth.KeyAuthenticator + public_key_authenticator: Chessh.Auth.KeyAuthenticator, + # TODO - benchmark + max_sessions: 128 import_config "#{config_env()}.exs" diff --git a/lib/chessh/auth/keys.ex b/lib/chessh/auth/keys.ex index d85f4a4..a948fdf 100644 --- a/lib/chessh/auth/keys.ex +++ b/lib/chessh/auth/keys.ex @@ -1,10 +1,19 @@ defmodule Chessh.Auth.KeyAuthenticator do + alias Chessh.Key + alias Chessh.Repo use Sshd.PublicKeyAuthenticator require Logger + import Ecto.Query - def authenticate(username, public_key, _opts) do - Logger.debug("#{inspect(username)}") - Logger.debug("#{inspect(public_key)}") - true + def authenticate(username, public_key) do + !!Repo.one( + from(k in Key, + join: p in assoc(k, :player), + where: k.key == ^Key.encode_key(public_key), + where: p.username == ^String.Chars.to_string(username) + ) + ) end + + def authenticate(username, public_key, _opts), do: authenticate(username, public_key) end diff --git a/lib/chessh/auth/password.ex b/lib/chessh/auth/password.ex index a6fa73d..48a4d62 100644 --- a/lib/chessh/auth/password.ex +++ b/lib/chessh/auth/password.ex @@ -2,11 +2,11 @@ defmodule Chessh.Auth.PasswordAuthenticator do alias Chessh.Player alias Chessh.Repo use Sshd.PasswordAuthenticator + require Logger def authenticate(username, password) do case Repo.get_by(Player, username: String.Chars.to_string(username)) do - nil -> false - x -> Player.valid_password?(x, password) + x -> Player.valid_password?(x, String.Chars.to_string(password)) end end end diff --git a/lib/chessh/schema/key.ex b/lib/chessh/schema/key.ex index f8c14cf..765c83b 100644 --- a/lib/chessh/schema/key.ex +++ b/lib/chessh/schema/key.ex @@ -32,6 +32,7 @@ defmodule Chessh.Key do if is_tuple(key) do case key do {pub, [opts]} -> [{pub, [opts]}] + {pub, []} -> [{pub, [comment: '']}] key -> [{key, [comment: '']}] end |> :ssh_file.encode(:openssh_key) diff --git a/lib/chessh/schema/player.ex b/lib/chessh/schema/player.ex index d04ed3e..5b7b904 100644 --- a/lib/chessh/schema/player.ex +++ b/lib/chessh/schema/player.ex @@ -49,7 +49,7 @@ defmodule Chessh.Player do defp validate_username(changeset) do changeset |> validate_required([:username]) - |> validate_length(:username, min: 2, max: 12) + |> validate_length(:username, min: 2, max: 16) |> validate_format(:username, ~r/^[a-zA-Z0-9_\-]*$/, message: "only letters, numbers, underscores, and hyphens allowed" ) diff --git a/test/auth/password_test.exs b/test/auth/password_test.exs new file mode 100644 index 0000000..974f2fa --- /dev/null +++ b/test/auth/password_test.exs @@ -0,0 +1,27 @@ +defmodule Chessh.Auth.PasswordAuthenticatorTest do + use ExUnit.Case + alias Chessh.Player + alias Chessh.Repo + + @valid_user %{username: "logan", password: "password"} + + setup do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo) + + {:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) + + :ok + end + + test "User can sign in with their password" do + assert Chessh.Auth.PasswordAuthenticator.authenticate( + String.to_charlist(@valid_user.username), + String.to_charlist(@valid_user.password) + ) + + refute Chessh.Auth.PasswordAuthenticator.authenticate( + String.to_charlist(@valid_user.username), + String.to_charlist("a_bad_password") + ) + end +end diff --git a/test/auth/pubkey_test.exs b/test/auth/pubkey_test.exs new file mode 100644 index 0000000..78eecfb --- /dev/null +++ b/test/auth/pubkey_test.exs @@ -0,0 +1,35 @@ +defmodule Chessh.Auth.PublicKeyAuthenticatorTest do + use ExUnit.Case + alias Chessh.Key + alias Chessh.Repo + alias Chessh.Player + + @valid_user %{username: "logan", password: "password"} + @valid_key %{ + name: "The Gamer Machine", + key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/2LOJGGEd/dhFgRxJ5MMv0jJw4s4pA8qmMbZyulN44" + } + + setup do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo) + + {:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) + + {:ok, _key} = + Repo.insert( + Key.changeset(%Key{}, @valid_key) + |> Ecto.Changeset.put_assoc(:player, player) + ) + + :ok + end + + test "User can sign in with their ssh key from raw string" do + assert Chessh.Auth.KeyAuthenticator.authenticate(@valid_user.username, @valid_key.key) + end + + test "User can sign in with erlang decoded ssh key" do + [key] = :ssh_file.decode(@valid_key.key, :openssh_key) + assert Chessh.Auth.KeyAuthenticator.authenticate(@valid_user.username, key) + end +end diff --git a/test/schema/key_test.exs b/test/schema/key_test.exs index 2c5409d..6dbb574 100644 --- a/test/schema/key_test.exs +++ b/test/schema/key_test.exs @@ -28,7 +28,6 @@ defmodule Chessh.Schema.KeyTest do @empty_attrs %{} test "changeset with valid attributes" do - IO.puts(inspect(Key.changeset(%Key{}, @valid_attrs))) assert Key.changeset(%Key{}, @valid_attrs).valid? assert Key.changeset(%Key{}, @valid_key_attrs).valid? end diff --git a/test/schema/register_test.exs b/test/schema/register_test.exs new file mode 100644 index 0000000..5705d31 --- /dev/null +++ b/test/schema/register_test.exs @@ -0,0 +1,51 @@ +defmodule Chessh.Auth.UserRegistrationTest do + use Chessh.RepoCase + use ExUnit.Case + alias Chessh.Player + alias Chessh.Repo + + @valid_user %{username: "logan", password: "password"} + @invalid_username %{username: "a", password: "password"} + @invalid_password %{username: "aasdf", password: "pass"} + @repeated_username %{username: "LoGan", password: "password"} + + 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_username).valid? + end + + test "Password changeset must match" do + refute Player.password_changeset( + %Player{}, + Map.put(@valid_user, :password_confirmation, + password_confirmation: @valid_user.password <> "a" + ) + ).valid? + + valid_user_changed_password = Map.put(@valid_user, :password, "a_new_password") + + assert Player.password_changeset( + %Player{}, + Map.put( + valid_user_changed_password, + :password_confirmation, + valid_user_changed_password.password + ) + ).valid? + end + + test "Password is hashed" do + changeset = Player.registration_changeset(%Player{}, @valid_user) + assert_raise KeyError, fn -> changeset.changes.password end + assert changeset.changes.hashed_password + refute changeset.changes.hashed_password == @valid_user.password + end + + test "Username is uniquely case insensitive" do + assert Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) + + assert {:error, + %{errors: [{:username, {_, [{:constraint, :unique}, {:constraint_name, _}]}}]}} = + Repo.insert(Player.registration_changeset(%Player{}, @repeated_username)) + end +end diff --git a/test/support/repo_case.ex b/test/support/repo_case.ex index 73abcff..e5f4a6f 100644 --- a/test/support/repo_case.ex +++ b/test/support/repo_case.ex @@ -8,8 +8,6 @@ defmodule Chessh.RepoCase do import Ecto import Ecto.Query import Chessh.RepoCase - - # and any other stuff end end