From 5fe604c5a26cc9a0d61b5b47bc0a1d7427245ac5 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sun, 27 Jun 2021 17:28:28 +0200 Subject: [PATCH] Implement login endpoint --- lib/matrix_server/account.ex | 8 +- lib/matrix_server/device.ex | 13 ++- lib/matrix_server_web/api/login.ex | 42 ++++++++ lib/matrix_server_web/api/register.ex | 4 +- .../controllers/auth_controller.ex | 101 ++++++++++++++++-- lib/matrix_server_web/plug/error.ex | 1 + 6 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 lib/matrix_server_web/api/login.ex diff --git a/lib/matrix_server/account.ex b/lib/matrix_server/account.ex index bb5a493..b5a505e 100644 --- a/lib/matrix_server/account.ex +++ b/lib/matrix_server/account.ex @@ -34,17 +34,17 @@ defmodule MatrixServer.Account do end end - def register(account, params) do + def register(params) do Multi.new() - |> Multi.insert(:account, changeset(account, params)) + |> Multi.insert(:account, changeset(%Account{}, params)) |> Multi.insert(:device, fn %{account: account} -> - device_id = Device.generate_device_id(account) + device_id = Device.generate_device_id(account.localpart) Ecto.build_assoc(account, :devices) |> Map.put(:device_id, device_id) |> Device.changeset(params) end) - |> Multi.run(:device_with_access_token, &Device.generate_access_token/2) + |> Multi.run(:device_with_access_token, &Device.insert_new_access_token/2) end def get_by_access_token(access_token) do diff --git a/lib/matrix_server/device.ex b/lib/matrix_server/device.ex index 3e1da5e..867f717 100644 --- a/lib/matrix_server/device.ex +++ b/lib/matrix_server/device.ex @@ -18,23 +18,26 @@ defmodule MatrixServer.Device do def changeset(device, params \\ %{}) do device - |> cast(params, [:localpart, :device_id, :access_token, :display_name]) + |> cast(params, [:display_name, :device_id]) |> validate_required([:localpart, :device_id]) |> unique_constraint([:localpart, :device_id], name: :devices_pkey) end - def generate_access_token(repo, %{ + def insert_new_access_token(repo, %{ device: %Device{localpart: localpart, device_id: device_id} = device }) do - access_token = - Phoenix.Token.encrypt(MatrixServerWeb.Endpoint, "access_token", {localpart, device_id}) + access_token = generate_access_token(localpart, device_id) device |> change(%{access_token: access_token}) |> repo.update() end - def generate_device_id(%Account{localpart: localpart}) do + def generate_access_token(localpart, device_id) do + Phoenix.Token.encrypt(MatrixServerWeb.Endpoint, "access_token", {localpart, device_id}) + end + + def generate_device_id(localpart) do time_string = DateTime.utc_now() |> DateTime.to_unix() diff --git a/lib/matrix_server_web/api/login.ex b/lib/matrix_server_web/api/login.ex new file mode 100644 index 0000000..322e683 --- /dev/null +++ b/lib/matrix_server_web/api/login.ex @@ -0,0 +1,42 @@ +# https://gist.github.com/char0n/6fca76e886a2cfbd3aaa05526f287728 +defmodule MatrixServerWeb.API.Login do + use Ecto.Schema + + import Ecto.Changeset + + defmodule MatrixServerWeb.API.Login.Identifier do + use Ecto.Schema + + import Ecto.Changeset + + @primary_key false + embedded_schema do + field :type, :string + field :user, :string + end + + def changeset(identifier, attrs) do + identifier + |> cast(attrs, [:type, :user]) + |> validate_required([:type, :user]) + end + end + + alias MatrixServerWeb.API.Login.Identifier + + @primary_key false + embedded_schema do + field :type, :string + field :password, :string + field :device_id, :string + field :initial_device_display_name, :string + embeds_one :identifier, Identifier + end + + def changeset(attrs) do + %__MODULE__{} + |> cast(attrs, [:type, :password, :device_id, :initial_device_display_name]) + |> cast_embed(:identifier, with: &Identifier.changeset/2, required: true) + |> validate_required([:type, :password]) + end +end diff --git a/lib/matrix_server_web/api/register.ex b/lib/matrix_server_web/api/register.ex index 09c996d..25a6b74 100644 --- a/lib/matrix_server_web/api/register.ex +++ b/lib/matrix_server_web/api/register.ex @@ -4,9 +4,9 @@ defmodule MatrixServerWeb.API.Register do import Ecto.Changeset import MatrixServerWeb.Plug.Error - alias __MODULE__ alias Ecto.Changeset + @primary_key false embedded_schema do field :device_id, :string field :initial_device_display_name, :string @@ -16,7 +16,7 @@ defmodule MatrixServerWeb.API.Register do end def changeset(params) do - %Register{} + %__MODULE__{} |> cast(params, [ :device_id, :initial_device_display_name, diff --git a/lib/matrix_server_web/controllers/auth_controller.ex b/lib/matrix_server_web/controllers/auth_controller.ex index 0f4ec4e..da5b73f 100644 --- a/lib/matrix_server_web/controllers/auth_controller.ex +++ b/lib/matrix_server_web/controllers/auth_controller.ex @@ -4,9 +4,10 @@ defmodule MatrixServerWeb.AuthController do import MatrixServer import MatrixServerWeb.Plug.Error import Ecto.Changeset, only: [apply_changes: 1] + import Ecto.Query - alias MatrixServer.{Repo, Account} - alias MatrixServerWeb.API.Register + alias MatrixServer.{Repo, Account, Device} + alias MatrixServerWeb.API.{Register, Login} alias Ecto.Changeset @register_type "m.login.dummy" @@ -18,11 +19,11 @@ defmodule MatrixServerWeb.AuthController do input = apply_changes(cs) |> Map.from_struct() - |> update_map_entry(:initial_device_display_name, :device_name) + |> update_map_entry(:initial_device_display_name, :display_name) |> update_map_entry(:username, :localpart) |> update_map_entry(:password, :password_hash, &Bcrypt.hash_pwd_salt/1) - case Account.register(%Account{}, input) |> Repo.transaction() do + case Account.register(input) |> Repo.transaction() do {:ok, %{device_with_access_token: device}} -> data = %{user_id: get_mxid(device.localpart)} @@ -73,14 +74,94 @@ defmodule MatrixServerWeb.AuthController do |> json(data) end - def login(conn, %{"type" => "m.login.password"}) do - conn - |> put_status(200) - |> json(%{}) + def login( + conn, + %{"type" => @login_type, "identifier" => %{"type" => "m.id.user"}} = params + ) do + case Login.changeset(params) do + %Changeset{valid?: true} = cs -> + input = + apply_changes(cs) + |> Map.from_struct() + |> update_map_entry(:initial_device_display_name, :display_name) + |> update_map_entry(:identifier, :localpart, fn + %{user: "@" <> rest} -> + case String.split(rest) do + [localpart, _] -> localpart + # Empty string will never match in the database. + _ -> "" + end + + %{user: user} -> + user + end) + + case Repo.transaction(login_transaction(input)) do + {:ok, device} -> + data = %{ + user_id: get_mxid(device.localpart), + access_token: device.access_token, + device_id: device.device_id + } + + conn + |> put_status(200) + |> json(data) + + {:error, error} -> + put_error(conn, error) + end + + _ -> + put_error(conn, :bad_json) + end end def login(conn, _params) do - # Login type m.login.token is unsupported for now. - put_error(conn, :forbidden) + # Other login types and identifiers are unsupported for now. + put_error(conn, :unknown) + end + + defp login_transaction(%{localpart: localpart, password: password} = params) do + fn repo -> + case repo.one(from a in Account, where: a.localpart == ^localpart) do + %Account{password_hash: hash} = account -> + if Bcrypt.verify_pass(password, hash) do + device_id = Map.get(params, :device_id, Device.generate_device_id(localpart)) + access_token = Device.generate_access_token(localpart, device_id) + + update_query = + from(d in Device) + |> update(set: [access_token: ^access_token, device_id: ^device_id]) + + update_query = + if params[:display_name] != nil do + update(update_query, set: [display_name: ^params.display_name]) + else + update_query + end + + result = + Ecto.build_assoc(account, :devices) + |> Map.put(:device_id, device_id) + |> Map.put(:access_token, access_token) + |> Device.changeset(params) + |> repo.insert(on_conflict: update_query, conflict_target: [:localpart, :device_id]) + + case result do + {:ok, device} -> + device + + {:error, _cs} -> + repo.rollback(:forbidden) + end + else + repo.rollback(:forbidden) + end + + nil -> + repo.rollback(:forbidden) + end + end end end diff --git a/lib/matrix_server_web/plug/error.ex b/lib/matrix_server_web/plug/error.ex index 9d0b038..ad2d5db 100644 --- a/lib/matrix_server_web/plug/error.ex +++ b/lib/matrix_server_web/plug/error.ex @@ -8,6 +8,7 @@ defmodule MatrixServerWeb.Plug.Error do invalid_username: {400, "M_INVALID_USERNAME", "Invalid username."}, forbidden: {400, "M_FORBIDDEN", "The requested action is forbidden."}, unrecognized: {400, "M_UNRECOGNIZED", "Unrecognized request."}, + unknown: {400, "M_UNKNOWN", "An unknown error occurred."}, unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."}, missing_token: {401, "M_MISSING_TOKEN", "Access token required."} }