Current section
Files
Jump to
Current section
Files
lib/wobserver/web/security.ex
defmodule Wobserver.Security do
@moduledoc ~S"""
Handles basic websocket authentication.
A different module with the following methods can be set as `:security` in the config:
- `authenticate(Conn.t) :: Conn.t`
- `authenticated?(Conn.t) :: boolean`
- `authenticated?(:cowboy_req.req) :: boolean`
"""
alias Plug.Conn
@secret Application.compile_env(:wobserver, :security_key, "secret-key-setting")
@doc ~S"""
Authenticates a given `conn`.
"""
@spec authenticate(Conn.t()) :: Conn.t()
def authenticate(conn) do
Conn.put_resp_cookie(
conn,
"_wobserver",
generate(conn.remote_ip, conn.req_headers)
)
end
@doc ~S"""
Checks whether a given `conn` or `req` is authenticated.
"""
@spec authenticated?(Conn.t()) :: boolean
def authenticated?(conn = %Conn{}) do
conn = Conn.fetch_cookies(conn)
case conn.cookies["_wobserver"] do
nil -> false
key -> key == generate(conn.remote_ip, conn.req_headers)
end
end
@spec authenticated?(:cowboy_req.req()) :: boolean
def authenticated?(req) do
{ip, _} = req[:peer]
headers = req[:headers]
cookies =
Enum.find_value(headers, "", fn
{"cookie", value} -> value
_ -> false
end)
String.contains?(cookies, "_wobserver=" <> generate(ip, headers))
end
@spec generate(tuple, list(tuple)) :: String.t()
defp generate(remote_ip, headers) do
ip =
remote_ip
|> Tuple.to_list()
|> Enum.map(&to_string/1)
|> Enum.join(".")
user_agent =
Enum.find_value(headers, "unknown", fn
{"user-agent", value} -> value
_ -> false
end)
@secret
|> hmac(ip)
|> hmac(user_agent)
|> Base.encode16()
|> String.downcase()
end
@spec hmac(String.t() | list(String.t()), String.t()) :: String.t()
defp hmac(encryption_key, data),
do: :crypto.mac(:hmac, :sha256, encryption_key, data)
end