]> Entropealabs - wampex.git/commitdiff
initial router authentication
authorChristopher <chris@entropealabs.com>
Tue, 17 Mar 2020 02:24:53 +0000 (21:24 -0500)
committerChristopher <chris@entropealabs.com>
Tue, 17 Mar 2020 02:24:53 +0000 (21:24 -0500)
lib/authentication.ex
lib/crypto.ex [new file with mode: 0644]
lib/router/authentication.ex [new file with mode: 0644]
lib/router/session.ex
test/wampex_test.exs

index fbe743cc70e2cdaa2f418598ddf2c95f73b8d921..419fb2d04ca0248b48f323359497a94a3b4b22fd 100644 (file)
@@ -1,5 +1,8 @@
 defmodule Wampex.Authentication do
   @moduledoc false
+
+  alias Wampex.Crypto
+
   @enforce_keys [:authid, :authmethods, :secret]
   defstruct [:authid, :authmethods, :secret]
 
@@ -17,24 +20,13 @@ defmodule Wampex.Authentication do
       ) do
     {auth
      |> Map.get(:secret)
-     |> pbkdf2(salt, it, len)
-     |> hash_challenge(ch), %{}}
+     |> Crypto.pbkdf2(salt, it, len)
+     |> Crypto.hash_challenge(ch), %{}}
   end
 
   def handle({"wampcra", %{"challenge" => ch}}, auth) do
     {auth
      |> Map.get(:secret)
-     |> hash_challenge(ch), %{}}
-  end
-
-  defp hash_challenge(secret, challenge) do
-    :sha256
-    |> :crypto.hmac(secret, challenge)
-    |> :base64.encode()
-  end
-
-  defp pbkdf2(secret, salt, iterations, keylen) do
-    {:ok, derived} = :pbkdf2.pbkdf2(:sha256, secret, salt, iterations, keylen)
-    Base.encode64(derived)
+     |> Crypto.hash_challenge(ch), %{}}
   end
 end
diff --git a/lib/crypto.ex b/lib/crypto.ex
new file mode 100644 (file)
index 0000000..a88dcf0
--- /dev/null
@@ -0,0 +1,19 @@
+defmodule Wampex.Crypto do
+  @moduledoc false
+  def hash_challenge(secret, challenge) do
+    :sha256
+    |> :crypto.hmac(secret, challenge)
+    |> :base64.encode()
+  end
+
+  def pbkdf2(secret, salt, iterations, keylen) do
+    {:ok, derived} = :pbkdf2.pbkdf2(:sha256, secret, salt, iterations, keylen)
+    Base.encode64(derived)
+  end
+
+  def random_string(length) do
+    length
+    |> :crypto.strong_rand_bytes()
+    |> Base.encode64()
+  end
+end
diff --git a/lib/router/authentication.ex b/lib/router/authentication.ex
new file mode 100644 (file)
index 0000000..d7eb2ca
--- /dev/null
@@ -0,0 +1,59 @@
+defmodule Wampex.Router.Authentication do
+  @moduledoc false
+
+  alias Wampex.Crypto
+  alias Wampex.Serializers.JSON
+
+  @wampcra "wampcra"
+  @salt_length 32
+  @key_length 64
+  @auth_provider "userdb"
+  @auth_role "user"
+
+  def authenticate?(methods) do
+    can_auth(methods)
+  end
+
+  def method, do: @wampcra
+
+  def challenge(authid, session_id) do
+    nonce = Crypto.random_string(@salt_length)
+    salt = Crypto.random_string(@salt_length)
+    now = DateTime.to_iso8601(DateTime.utc_now())
+
+    %{
+      challenge:
+        JSON.serialize!(%{
+          nonce: nonce,
+          authprovider: @auth_provider,
+          authid: authid,
+          timestamp: now,
+          authrole: @auth_role,
+          authmethod: @wampcra,
+          session: session_id
+        }),
+      salt: salt,
+      keylen: @key_length,
+      iterations: Enum.random(10_000..22_000)
+    }
+  end
+
+  def authenticate(signature, authid, %{
+        challenge: challenge,
+        salt: salt,
+        keylen: keylen,
+        iterations: iters
+      }) do
+    authid
+    |> get_secret()
+    |> Crypto.pbkdf2(salt, iters, keylen)
+    |> Crypto.hash_challenge(challenge)
+    |> :pbkdf2.compare_secure(signature)
+  end
+
+  defp get_secret(_authid), do: "test1234"
+
+  defp can_auth([]), do: false
+  defp can_auth([@wampcra | _]), do: true
+  defp can_auth([_ | t]), do: can_auth(t)
+end
index b7c19f4487638a1bc87e925d4b5f74dc1f697167..db54c401fba9d482c9c42ff16be670a177ff9b9c 100644 (file)
@@ -6,14 +6,16 @@ defmodule Wampex.Router.Session do
 
   @max_id 9_007_199_254_740_992
 
+  alias __MODULE__, as: Sess
   alias StatesLanguage, as: SL
   alias Wampex.Realm
   alias Wampex.Roles.{Broker, Dealer, Peer}
   alias Wampex.Roles.Peer.{Challenge, Welcome, Abort}
   alias Wampex.Router
-  alias __MODULE__, as: Sess
+  alias Wampex.Serializers.JSON
   alias Broker.{Subscribed, Published, Event}
   alias Dealer.{Invocation, Registered, Result, Unregistered}
+  alias Router.Authentication
 
   @enforce_keys [:transport, :transport_pid]
   defstruct [
@@ -36,7 +38,9 @@ defmodule Wampex.Router.Session do
     :name,
     :goodbye,
     :peer_information,
+    :challenge,
     error: "wamp.error.protocol_violation",
+    authentication: Authentication,
     roles: [Peer, Broker, Dealer],
     registrations: [],
     subscriptions: [],
@@ -65,6 +69,7 @@ defmodule Wampex.Router.Session do
           goodbye: binary() | nil,
           realm: Realm.t(),
           name: module() | nil,
+          challenge: map() | nil,
           roles: [module()],
           registrations: [],
           subscriptions: [],
@@ -115,7 +120,7 @@ defmodule Wampex.Router.Session do
         @handle_init,
         _,
         @init,
-        %SL{data: %Sess{transport_pid: t}} = data
+        data
       ) do
     debug("Init")
 
@@ -145,7 +150,7 @@ defmodule Wampex.Router.Session do
 
     {data, actions} =
       case handle_message(msg, roles) do
-        {actions, id, response} ->
+        {actions, _id, response} ->
           {data, response} = maybe_update_response(data, response)
           debug("Response: #{inspect(response)}")
           {data, actions}
@@ -169,23 +174,98 @@ defmodule Wampex.Router.Session do
               transport: tt,
               transport_pid: t,
               hello: {:hello, realm, dets},
-              hello_received: false
+              hello_received: false,
+              authentication: auth
             } = data
         } = sl
       ) do
-    # TODO check for authentication request
-    send_to_peer(
-      Peer.welcome(%Welcome{
-        session_id: id,
-        options: %{agent: "WAMPex Router", roles: %{broker: %{}, dealer: %{}}}
-      }),
-      tt,
-      t
-    )
+    {actions, challenge} =
+      case dets do
+        %{"authid" => ai, "authmethods" => am} ->
+          case auth.authenticate?(am) do
+            true ->
+              ch = auth.challenge(ai, id)
+
+              send_to_peer(
+                Peer.challenge(%Challenge{
+                  auth_method: auth.method,
+                  options: ch
+                }),
+                tt,
+                t
+              )
+
+              {[], ch}
+
+            false ->
+              {[{:next_event, :internal, :abort}], nil}
+          end
+
+        %{} ->
+          {[{:next_event, :internal, :abort}], nil}
+      end
 
     {:ok,
-     %SL{sl | data: %Sess{data | hello_received: true, realm: realm, peer_information: dets}},
-     [{:next_event, :internal, :transition}]}
+     %SL{
+       sl
+       | data: %Sess{
+           data
+           | challenge: challenge,
+             hello_received: true,
+             realm: realm,
+             peer_information: dets
+         }
+     }, [{:next_event, :internal, :transition}] ++ actions}
+  end
+
+  @impl true
+  def handle_resource(
+        @handle_authenticate,
+        _,
+        @authenticate,
+        %SL{
+          data: %Sess{
+            transport: tt,
+            transport_pid: t,
+            authentication: auth,
+            challenge: challenge,
+            authenticate: {:authenticate, sig, dets}
+          }
+        } = sl
+      ) do
+    ch = JSON.deserialize!(challenge.challenge)
+    session_id = get_in(ch, ["session"])
+    authid = get_in(ch, ["authid"])
+    authrole = get_in(ch, ["authrole"])
+    authmethod = get_in(ch, ["authmethod"])
+    authprovider = get_in(ch, ["authprovider"])
+
+    actions =
+      case auth.authenticate(sig, get_in(ch, ["authid"]), challenge) do
+        true ->
+          send_to_peer(
+            Peer.welcome(%Welcome{
+              session_id: session_id,
+              options: %{
+                agent: "WAMPex Router",
+                authid: authid,
+                authrole: authrole,
+                authmethod: authmethod,
+                authprovider: authprovider,
+                roles: %{broker: %{}, dealer: %{}}
+              }
+            }),
+            tt,
+            t
+          )
+
+          [{:next_event, :internal, :transition}]
+
+        false ->
+          [{:next_event, :internal, :transition}, {:next_event, :internal, :abort}]
+      end
+
+    {:ok, sl, actions}
   end
 
   @impl true
index 30cd19ab7bea4d9ea790d17481e24f3c7c306db3..c5b562e32706bce84873bc0b8d1b5d3eb5bf1c5e 100644 (file)
@@ -19,7 +19,7 @@ defmodule WampexTest do
 
   @url "ws://localhost:4000/ws"
   @auth %Authentication{authid: "entone", authmethods: ["wampcra"], secret: "test1234"}
-  @realm %Realm{name: "org.entropealabs.iot"}
+  @realm %Realm{name: "org.entropealabs.iot", authentication: @auth}
   @roles [Callee, Caller, Publisher, Subscriber]
   @device "as987d9a8sd79a87ds"
 
@@ -272,15 +272,6 @@ defmodule WampexTest do
     assert_receive {:registered, id}
   end
 
-  @tag :client
-  test "authentication" do
-    callee_name = TestCalleeRespondAuthenticated
-    session = %Session{@session | realm: %Realm{@session.realm | authentication: @auth}}
-    Client.start_link(name: callee_name, session: session)
-    TestCallee.start_link(self(), callee_name, @device)
-    assert_receive {:registered, id}
-  end
-
   @tag :abort
   test "abort" do
     Process.flag(:trap_exit, true)
@@ -306,8 +297,7 @@ defmodule WampexTest do
           procedure: "com.actuator.#{@device}.light",
           arg_list: [1],
           arg_kw: %{color: "#FFFFFF"}
-        }),
-        60
+        })
       )
 
     assert_receive {:invocation, _, _, _, _, _}