architex/lib/architex_web/federation/authenticate_server.ex

99 lines
2.9 KiB
Elixir
Raw Normal View History

2021-09-01 12:43:55 +00:00
defmodule ArchitexWeb.Federation.AuthenticateServer do
import ArchitexWeb.Error
2021-09-01 12:43:55 +00:00
alias Architex.{SigningKey, ServerKeyInfo}
@auth_header_regex ~r/^X-Matrix origin=(?<origin>.*),key="(?<key>.*)",sig="(?<sig>.*)"$/
def authenticate(%Plug.Conn{
body_params: body_params,
req_headers: headers,
request_path: path,
method: method,
query_string: query_string
}) do
# TODO: This will break if request ends with '?'.
uri = URI.decode_www_form(path)
uri =
if String.length(query_string) > 0 do
uri <> "?" <> URI.decode_www_form(query_string)
else
uri
end
2021-08-10 16:02:53 +00:00
object_to_sign = %{
uri: uri,
2021-08-10 16:02:53 +00:00
method: method,
2021-09-01 12:43:55 +00:00
destination: Architex.server_name()
2021-08-10 16:02:53 +00:00
}
2021-08-10 16:02:53 +00:00
object_to_sign =
if method != "GET", do: Map.put(object_to_sign, :content, body_params), else: object_to_sign
2021-08-10 16:02:53 +00:00
object_fun = &Map.put(object_to_sign, :origin, &1)
2021-08-10 16:02:53 +00:00
authenticate_with_headers(headers, object_fun)
end
2021-08-10 16:02:53 +00:00
defp authenticate_with_headers(headers, object_fun) do
# TODO: Only query once per origin.
2021-08-10 16:02:53 +00:00
headers
|> parse_authorization_headers()
|> Enum.find(:error, fn {origin, _, sig} ->
object = object_fun.(origin)
2021-09-01 12:43:55 +00:00
with {:ok, raw_sig} <- Architex.decode_base64(sig),
{:ok, encoded_object} <- Architex.encode_canonical_json(object),
{:ok, %ServerKeyInfo{signing_keys: keys}} <-
ServerKeyInfo.with_fresh_signing_keys(origin) do
Enum.find_value(keys, false, fn %SigningKey{signing_key: signing_key} ->
2021-09-01 12:43:55 +00:00
with {:ok, decoded_key} <- Architex.decode_base64(signing_key) do
Architex.sign_verify(raw_sig, encoded_object, decoded_key)
else
_ -> false
end
end)
else
_ -> false
end
2021-08-10 16:02:53 +00:00
end)
end
def parse_authorization_headers(headers) do
headers
|> Enum.filter(&(elem(&1, 0) == "authorization"))
|> Enum.map(fn {_, auth_header} ->
Regex.named_captures(@auth_header_regex, auth_header)
end)
|> Enum.reject(&Kernel.is_nil/1)
2021-08-10 16:02:53 +00:00
|> Enum.map(fn %{"origin" => origin, "key" => key, "sig" => sig} ->
{origin, key, sig}
end)
2021-08-10 16:02:53 +00:00
|> Enum.filter(fn {_, key, _} -> String.starts_with?(key, "ed25519:") end)
end
defmacro __using__(opts) do
except = Keyword.get(opts, :except) || []
quote do
def action(conn, _) do
action = action_name(conn)
2021-08-10 16:02:53 +00:00
if action not in unquote(except) do
2021-09-01 12:43:55 +00:00
case ArchitexWeb.Federation.AuthenticateServer.authenticate(conn) do
2021-08-10 16:02:53 +00:00
{origin, _key, _sig} ->
conn = Plug.Conn.assign(conn, :origin, origin)
apply(__MODULE__, action, [conn, conn.params])
2021-08-10 16:02:53 +00:00
:error ->
put_error(conn, :unauthorized, "Signature verification failed.")
end
else
apply(__MODULE__, action, [conn, conn.params])
end
end
end
end
end