-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
this is based upon 0d44fc2 with the addition of Product Auth Tokens (needs some naming love)
- Loading branch information
Showing
10 changed files
with
237 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
defmodule NervesHub.Products.TokenAuth do | ||
use Ecto.Schema | ||
|
||
import Ecto.Changeset | ||
|
||
alias NervesHub.Products.Product | ||
|
||
@type t :: %__MODULE__{} | ||
|
||
@access_id_prefix "nhp" | ||
|
||
schema "product_auth_tokens" do | ||
belongs_to(:product, Product) | ||
|
||
field(:access_id, :string) | ||
field(:secret, :string) | ||
|
||
field(:deactivated_at, :utc_datetime) | ||
|
||
timestamps() | ||
end | ||
|
||
def create_changeset(%Product{} = product) do | ||
change(%__MODULE__{}, product_id: product.id) | ||
|> put_change(:access_id, "#{@access_id_prefix}_#{generate_token()}") | ||
|> put_change(:secret, generate_token()) | ||
|> validate_required([:product_id, :access_id, :secret]) | ||
|> validate_format(:access_id, ~r/^nhp_[a-zA-Z0-9\-\/\+]{43}$/) | ||
|> validate_format(:secret, ~r/^[a-zA-Z0-9\-\/\+]{43}$/) | ||
|> foreign_key_constraint(:product_id) | ||
|> unique_constraint(:access_id) | ||
|> unique_constraint(:secret) | ||
end | ||
|
||
defp generate_token() do | ||
:crypto.strong_rand_bytes(32) |> Base.encode64(padding: false) | ||
end | ||
end |
2 changes: 1 addition & 1 deletion
2
lib/nerves_hub_web/channels/device_socket.ex → ...b_web/channels/device_socket_cert_auth.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
lib/nerves_hub_web/channels/device_socket_token_auth.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
defmodule NervesHubWeb.DeviceSocketTokenAuth do | ||
use Phoenix.Socket | ||
|
||
alias NervesHub.Devices | ||
|
||
alias Plug.Crypto | ||
|
||
channel("console", NervesHubWeb.ConsoleChannel) | ||
channel("device", NervesHubWeb.DeviceChannel) | ||
|
||
@salt_headers [ | ||
"x-nh-key-digest", | ||
"x-nh-key-iterations", | ||
"x-nh-key-length", | ||
"x-nh-access-id", | ||
"x-nh-time" | ||
] | ||
|
||
# Default 15 min max age for the signature | ||
@default_max_age 900 | ||
|
||
def connect(_params, socket, %{x_headers: headers}) do | ||
parsed_data = parse_headers(headers) | ||
verify_opts = verify_options(parsed_data) | ||
salt = expected_salt(headers) | ||
|
||
with {:ok, access_id} <- Keyword.fetch(parsed_data, :access_id), | ||
{:ok, token_auth} <- get_product_token_auth(access_id), | ||
{:ok, signature} <- Keyword.fetch(parsed_data, :signature), | ||
{:ok, identifier} <- Crypto.verify(token_auth.secret, salt, signature, verify_opts), | ||
{:ok, device} <- | ||
Devices.get_or_create_device(token_auth: token_auth, identifier: identifier) do | ||
socket = | ||
socket | ||
|> assign(:device, device) | ||
|> assign(:reference_id, generate_reference_id()) | ||
|
||
{:ok, socket} | ||
else | ||
_ -> {:error, :invalud_auth} | ||
end | ||
end | ||
|
||
def connect(_params, _socket, _connect_info), do: :error | ||
|
||
def id(%{assigns: %{device: device}}), do: "device_socket:#{device.id}" | ||
def id(_socket), do: nil | ||
|
||
defp parse_headers(headers) do | ||
for {k, v} <- headers do | ||
case String.downcase(k) do | ||
"x-nh-time" -> | ||
{:signed_at, String.to_integer(v)} | ||
|
||
"x-nh-key-length" -> | ||
{:key_length, String.to_integer(v)} | ||
|
||
"x-nh-key-iterations" -> | ||
{:key_iterations, String.to_integer(v)} | ||
|
||
"x-nh-key-digest" -> | ||
"NH1-HMAC-" <> digest_str = v | ||
{:key_digest, String.to_existing_atom(String.downcase(digest_str))} | ||
|
||
"x-nh-access-id" -> | ||
{:access_id, v} | ||
|
||
"x-nh-signature" -> | ||
{:signature, v} | ||
|
||
_ -> | ||
# TODO: What should we do with this unknown? | ||
nil | ||
end | ||
end | ||
end | ||
|
||
defp verify_options(parsed_data) do | ||
Keyword.take(parsed_data, [:key_digest, :key_iterations, :key_length, :signed_at]) | ||
# TODO: Make max_age configurable? | ||
|> Keyword.put(:max_age, @default_max_age) | ||
end | ||
|
||
defp expected_salt(headers) do | ||
salt_headers = | ||
Enum.filter(headers, &(elem(&1, 0) in @salt_headers)) | ||
|> Enum.sort_by(fn {k, _} -> k end) | ||
|> Enum.map_join("\n", fn {k, v} -> "#{k}=#{v}" end) | ||
|
||
""" | ||
NH1:device:token:connect | ||
#{salt_headers} | ||
""" | ||
end | ||
|
||
defp get_product_token_auth(access_id) do | ||
case NervesHub.Products.get_token_auth(access_id: access_id) do | ||
nil -> {:error, :unknown_access_id} | ||
token_auth -> {:ok, token_auth} | ||
end | ||
end | ||
|
||
defp generate_reference_id() do | ||
Base.encode32(:crypto.strong_rand_bytes(2), padding: false) | ||
end | ||
|
||
def handle_error(conn, :invalud_auth), | ||
do: Plug.Conn.send_resp(conn, 403, "Invalid authorization") | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
priv/repo/migrations/20231209055552_create_product_auth_tokens.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
defmodule NervesHub.Repo.Migrations.CreateProductAuthTokens do | ||
use Ecto.Migration | ||
|
||
def change do | ||
create table(:product_auth_tokens) do | ||
add(:product_id, references(:products), null: false) | ||
|
||
add(:access_id, :string, null: false) | ||
add(:secret, :string, null: false) | ||
|
||
add(:deactivated_at, :utc_datetime) | ||
|
||
timestamps() | ||
end | ||
|
||
create index(:product_auth_tokens, [:access_id], unique: true) | ||
create index(:product_auth_tokens, [:secret], unique: true) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters