From b8bd95268192c992b0bec4be39cfe889aa71a8ea Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 13 Jan 2026 13:43:43 -0600 Subject: [PATCH] agent token changes --- lib/towerops/agents.ex | 6 +-- lib/towerops/agents/agent_token.ex | 49 +++++++++---------- lib/towerops_web/live/agent_live/index.ex | 38 ++++++++++++++ .../live/agent_live/index.html.heex | 48 +++++++++++------- ...192744_change_agent_token_to_encrypted.exs | 14 ++++++ test/towerops/agents_test.exs | 3 +- test/towerops_web/live/agent_live_test.exs | 4 +- 7 files changed, 111 insertions(+), 51 deletions(-) create mode 100644 priv/repo/migrations/20260113192744_change_agent_token_to_encrypted.exs diff --git a/lib/towerops/agents.ex b/lib/towerops/agents.ex index 52f82ac7..3c186c34 100644 --- a/lib/towerops/agents.ex +++ b/lib/towerops/agents.ex @@ -114,10 +114,10 @@ defmodule Towerops.Agents do {:error, :invalid_token} """ - def verify_agent_token(encoded_token) do - with {:ok, token_hash} <- AgentToken.verify_token(encoded_token), + def verify_agent_token(token) do + with {:ok, token_string} <- AgentToken.verify_token(token), %AgentToken{} = agent_token <- - Repo.get_by(AgentToken, token_hash: token_hash, enabled: true) do + Repo.get_by(AgentToken, token: token_string, enabled: true) do {:ok, agent_token} else _ -> {:error, :invalid_token} diff --git a/lib/towerops/agents/agent_token.ex b/lib/towerops/agents/agent_token.ex index 4c73178e..f629b688 100644 --- a/lib/towerops/agents/agent_token.ex +++ b/lib/towerops/agents/agent_token.ex @@ -3,20 +3,20 @@ defmodule Towerops.Agents.AgentToken do Schema for agent authentication tokens. Agent tokens are used to authenticate remote SNMP polling agents that run on customer networks. - Tokens are generated with cryptographically secure random bytes, and only the SHA256 hash is - stored in the database for security. + Tokens are generated with cryptographically secure random bytes (48 bytes = 384 bits) and + stored in plaintext in the database. Tokens can be retrieved and viewed by authorized users. """ use Ecto.Schema import Ecto.Changeset - @hash_algorithm :sha256 - @rand_size 32 + # 48 bytes = 384 bits of cryptographically secure randomness + @rand_size 48 @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "agent_tokens" do - field :token_hash, :binary + field :token, :string field :name, :string field :last_seen_at, :utc_datetime field :last_ip, :string @@ -29,22 +29,24 @@ defmodule Towerops.Agents.AgentToken do end @doc """ - Builds a new agent token with secure random bytes. + Builds a new agent token with cryptographically secure random bytes. - Returns a tuple of {encoded_token, changeset}. The encoded_token should be - returned to the user ONCE and never stored in plaintext. + Returns a tuple of {token_string, changeset}. The token_string is stored in the database + and can be retrieved later for display to authorized users. ## Examples iex> {token, changeset} = AgentToken.build_token(org_id, "My Agent") iex> is_binary(token) true + iex> byte_size(Base.url_decode64!(token, padding: false)) + 48 """ def build_token(organization_id, name) do + # Generate 48 bytes (384 bits) of cryptographically secure random data token = :crypto.strong_rand_bytes(@rand_size) encoded_token = Base.url_encode64(token, padding: false) - hashed_token = :crypto.hash(@hash_algorithm, token) changeset = %__MODULE__{} @@ -52,38 +54,33 @@ defmodule Towerops.Agents.AgentToken do %{ organization_id: organization_id, name: name, - token_hash: hashed_token + token: encoded_token }, - [:organization_id, :name, :token_hash] + [:organization_id, :name, :token] ) - |> validate_required([:organization_id, :name, :token_hash]) + |> validate_required([:organization_id, :name, :token]) + |> unique_constraint(:token) {encoded_token, changeset} end @doc """ - Verifies an encoded token by hashing it. + Verifies a token by checking if it exists in the database. - Returns {:ok, token_hash} if the token can be decoded and hashed, - otherwise {:error, :invalid_token}. + Returns {:ok, token} if valid, otherwise {:error, :invalid_token}. ## Examples - iex> AgentToken.verify_token("valid_base64_token") - {:ok, <>} + iex> AgentToken.verify_token("valid_token") + {:ok, "valid_token"} iex> AgentToken.verify_token("invalid") {:error, :invalid_token} """ - def verify_token(encoded_token) do - case Base.url_decode64(encoded_token, padding: false) do - {:ok, token} -> - hashed = :crypto.hash(@hash_algorithm, token) - {:ok, hashed} - - _ -> - {:error, :invalid_token} - end + def verify_token(token) when is_binary(token) do + {:ok, token} end + + def verify_token(_), do: {:error, :invalid_token} end diff --git a/lib/towerops_web/live/agent_live/index.ex b/lib/towerops_web/live/agent_live/index.ex index a82aa6ec..4d43571a 100644 --- a/lib/towerops_web/live/agent_live/index.ex +++ b/lib/towerops_web/live/agent_live/index.ex @@ -58,6 +58,44 @@ defmodule ToweropsWeb.AgentLive.Index do |> assign(:new_token, nil)} end + @impl true + def handle_event("show_setup", %{"id" => id}, socket) do + agent_token = Agents.get_agent_token!(id) + + {:noreply, + socket + |> assign(:new_token, %{agent_token: agent_token, token: agent_token.token}) + |> assign(:show_token_modal, true)} + end + + @impl true + def handle_event("regenerate_token", %{"id" => id}, socket) do + agent_token = Agents.get_agent_token!(id) + organization = socket.assigns.current_organization + + # Revoke the old token and create a new one + case Agents.revoke_agent_token(id) do + {:ok, _} -> + case Agents.create_agent_token(organization.id, agent_token.name) do + {:ok, new_agent_token, token} -> + agent_tokens = Agents.list_organization_agent_tokens(organization.id) + + {:noreply, + socket + |> assign(:agent_tokens, agent_tokens) + |> assign(:new_token, %{agent_token: new_agent_token, token: token}) + |> assign(:show_token_modal, true) + |> put_flash(:info, "New token generated successfully")} + + {:error, _changeset} -> + {:noreply, put_flash(socket, :error, "Failed to generate new token")} + end + + {:error, _} -> + {:noreply, put_flash(socket, :error, "Failed to revoke old token")} + end + end + @impl true def handle_event("revoke_agent", %{"id" => id}, socket) do case Agents.revoke_agent_token(id) do diff --git a/lib/towerops_web/live/agent_live/index.html.heex b/lib/towerops_web/live/agent_live/index.html.heex index 1325c7ed..cdc82a86 100644 --- a/lib/towerops_web/live/agent_live/index.html.heex +++ b/lib/towerops_web/live/agent_live/index.html.heex @@ -106,15 +106,25 @@ <:action :let={agent}> <%= if agent.enabled do %> - +
+ + +
<% else %> Revoked <% end %> @@ -128,21 +138,23 @@
-
+

- Agent Created Successfully + Agent Setup Instructions

- Copy this token now - it will never be shown again! + Use the token and Docker Compose configuration below to deploy this agent.

-