defmodule Wampex.Authentication do
@moduledoc false
+
+ alias Wampex.Crypto
+
@enforce_keys [:authid, :authmethods, :secret]
defstruct [:authid, :authmethods, :secret]
) 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
--- /dev/null
+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
--- /dev/null
+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
@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 [
:name,
:goodbye,
:peer_information,
+ :challenge,
error: "wamp.error.protocol_violation",
+ authentication: Authentication,
roles: [Peer, Broker, Dealer],
registrations: [],
subscriptions: [],
goodbye: binary() | nil,
realm: Realm.t(),
name: module() | nil,
+ challenge: map() | nil,
roles: [module()],
registrations: [],
subscriptions: [],
@handle_init,
_,
@init,
- %SL{data: %Sess{transport_pid: t}} = data
+ data
) do
debug("Init")
{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}
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
@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"
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)
procedure: "com.actuator.#{@device}.light",
arg_list: [1],
arg_kw: %{color: "#FFFFFF"}
- }),
- 60
+ })
)
assert_receive {:invocation, _, _, _, _, _}