Implement basic shared key auth

This commit is contained in:
Broks Randolfs Gailītis 2025-08-20 11:33:40 +03:00
parent f31bd0b3b4
commit f7ed1fae6f
5 changed files with 88 additions and 20 deletions

View File

@ -2,7 +2,7 @@
## Running ## Running
### Starting servers ### Starting servers
Server ID (`SERVER_ID`) and remote servers (`SERVERS`) are provided as environment variables. Server ID (`SERVER_ID`) and remote servers (`SERVERS`) are provided as environment variables along with shared auth key (`AUTH_KEY`).
#### Server 1 #### Server 1
```bash ```bash
@ -28,3 +28,9 @@ curl -X POST http://localhost:4000/api/messages -H "Content-Type: application/js
```curl ```curl
curl -X POST http://localhost:4001/api/messages -H "Content-Type: application/json" -d '{"from": "2-nibbler", "to": "1-bender", "message": "I am Nibbler, agent of the Nibblonian fleet."}' curl -X POST http://localhost:4001/api/messages -H "Content-Type: application/json" -d '{"from": "2-nibbler", "to": "1-bender", "message": "I am Nibbler, agent of the Nibblonian fleet."}'
``` ```
## Security
This application uses a shared auth key (`AUTH_KEY`) to authenticate requests between servers. The key is provided as an environment variable and must be the same on all servers.
### Next steps
HTTPS with client certificates should be implemented to ensure secure communication between servers and prevent unauthorized access and possible man-in-the-middle attacks.

View File

@ -0,0 +1,42 @@
defmodule MessageServer.Auth do
@spec generate_auth_token(String.t()) :: String.t()
def generate_auth_token(server_id) do
timestamp = System.system_time(:second)
payload = "#{server_id}:#{timestamp}"
signature = :crypto.mac(:hmac, :sha256, get_shared_secret(), payload) |> Base.encode64()
"#{payload}:#{signature}"
end
@spec verify_auth_token(String.t()) :: {:ok, String.t()} | {:error, String.t()}
def verify_auth_token(token) do
case String.split(token, ":") do
[server_id, timestamp_str, signature] ->
payload = "#{server_id}:#{timestamp_str}"
expected_signature =
:crypto.mac(:hmac, :sha256, get_shared_secret(), payload) |> Base.encode64()
timestamp = String.to_integer(timestamp_str)
current_time = System.system_time(:second)
cond do
signature != expected_signature ->
{:error, :auth_failure, "Invalid signature"}
# 5 minute window
current_time - timestamp > 300 ->
{:error, :auth_failure, "Token expired"}
true ->
{:ok, server_id}
end
_ ->
{:error, :auth_failure, "Invalid token format"}
end
end
def get_shared_secret do
System.get_env("AUTH_KEY", "default-secret")
end
end

View File

@ -1,7 +1,7 @@
defmodule MessageServer.RemoteClient do defmodule MessageServer.RemoteClient do
require Logger require Logger
alias MessageServer.{MessageRequest, ServerRegistry} alias MessageServer.{MessageRequest, ServerRegistry, Auth}
@spec send_message_to_server(String.t(), MessageRequest.t()) :: :ok | {:error, String.t()} @spec send_message_to_server(String.t(), MessageRequest.t()) :: :ok | {:error, String.t()}
def send_message_to_server(target_server_id, payload) do def send_message_to_server(target_server_id, payload) do
@ -32,10 +32,15 @@ defmodule MessageServer.RemoteClient do
@spec make_request(map(), binary()) :: {:ok, Req.Response.t()} | {:error, String.t()} @spec make_request(map(), binary()) :: {:ok, Req.Response.t()} | {:error, String.t()}
defp make_request(%{host: host, port: port}, serialized_payload) do defp make_request(%{host: host, port: port}, serialized_payload) do
url = "http://#{host}:#{port}/api/remote/messages" url = "http://#{host}:#{port}/api/remote/messages"
local_server_id = ServerRegistry.get_server_id()
auth_token = Auth.generate_auth_token(local_server_id)
case Req.post(url, case Req.post(url,
body: serialized_payload, body: serialized_payload,
headers: [{"content-type", "application/octet-stream"}] headers: [
{"content-type", "application/octet-stream"},
{"authorization", "Bearer #{auth_token}"}
]
) do ) do
{:ok, %Req.Response{status: status} = response} when status in 200..299 -> {:ok, %Req.Response{status: status} = response} when status in 200..299 ->
{:ok, response} {:ok, response}

View File

@ -5,7 +5,8 @@ defmodule MessageServer.Router do
alias MessageServer.{ alias MessageServer.{
MessageRequest, MessageRequest,
RemoteHandler, RemoteHandler,
MessageHandler MessageHandler,
Auth
} }
plug(:match) plug(:match)
@ -41,23 +42,28 @@ defmodule MessageServer.Router do
end end
post "/api/remote/messages" do post "/api/remote/messages" do
case handle_etf_body(conn) do with {:ok, _server_id} <- authenticate_request(conn),
{:ok, payload} -> {:ok, payload} <- handle_etf_body(conn) do
payload payload
|> RemoteHandler.handle_remote_message() |> RemoteHandler.handle_remote_message()
|> case do |> case do
:ok -> :ok ->
conn conn
|> put_resp_content_type("application/json") |> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(%{status: "success"})) |> send_resp(200, Jason.encode!(%{status: "success"}))
{:error, reason} -> {:error, reason} ->
Logger.warning("Remote message handling failed: #{inspect(reason)}") Logger.warning("Remote message handling failed: #{inspect(reason)}")
conn conn
|> put_resp_content_type("application/json") |> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{error: reason})) |> send_resp(400, Jason.encode!(%{error: reason}))
end end
else
{:error, :auth_failure, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(401, Jason.encode!(%{error: "Unauthorized", message: reason}))
{:error, reason} -> {:error, reason} ->
conn conn
@ -98,6 +104,16 @@ defmodule MessageServer.Router do
end end
end end
defp authenticate_request(conn) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
Auth.verify_auth_token(token)
_ ->
{:error, :unauthorized}
end
end
@spec validate_message_request(map()) :: {:ok, map()} | {:error, String.t()} @spec validate_message_request(map()) :: {:ok, map()} | {:error, String.t()}
defp validate_message_request(params) do defp validate_message_request(params) do
required_fields = ["from", "to", "message"] required_fields = ["from", "to", "message"]

View File

@ -17,7 +17,6 @@ defmodule MessageServer.StorageTest do
end end
test "creates storage directory on startup", %{storage_dir: storage_dir} do test "creates storage directory on startup", %{storage_dir: storage_dir} do
IO.puts(storage_dir)
assert File.exists?(storage_dir) assert File.exists?(storage_dir)
assert File.dir?(storage_dir) assert File.dir?(storage_dir)
end end