Implement basic shared key auth
This commit is contained in:
parent
f31bd0b3b4
commit
f7ed1fae6f
@ -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.
|
||||||
|
|||||||
42
lib/message_server/auth.ex
Normal file
42
lib/message_server/auth.ex
Normal 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
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user