Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
9 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/tesla/middleware/digest_auth.ex b/lib/tesla/middleware/digest_auth.ex
index 109f3cc..283630a 100644
--- a/lib/tesla/middleware/digest_auth.ex
+++ b/lib/tesla/middleware/digest_auth.ex
@@ -1,126 +1,131 @@
defmodule Tesla.Middleware.DigestAuth do
@behaviour Tesla.Middleware
@moduledoc """
Digest access authentication middleware
[Wiki on the topic](https://en.wikipedia.org/wiki/Digest_access_authentication)
**NOTE**: Currently the implementation is incomplete and works only for MD5 algorithm
and auth qop.
### Example
```
defmodule MyClient do
use Tesla
def client(username, password, opts \\ %{}) do
Tesla.build_client [
{Tesla.Middleware.DigestAuth, Map.merge(%{username: username, password: password}, opts)}
]
end
end
```
### Options
- `:username` - username (defaults to `""`)
- `:password` - password (defaults to `""`)
- `:cnonce_fn` - custom function generating client nonce (defaults to `&Tesla.Middleware.DigestAuth.cnonce/0`)
- `:nc` - nonce counter (defaults to `"00000000"`)
"""
def call(env, next, opts) do
if env.opts && Keyword.get(env.opts, :digest_auth_handshake) do
Tesla.run(env, next)
else
opts = opts || %{}
- env
- |> Tesla.put_headers(authorization_header(env, opts))
- |> Tesla.run(next)
+ with {:ok, headers} <- authorization_header(env, opts) do
+ env
+ |> Tesla.put_headers(headers)
+ |> Tesla.run(next)
+ end
end
end
defp authorization_header(env, opts) do
- env
- |> authorization_vars(opts)
- |> calculated_authorization_values
- |> create_header
+ with {:ok, vars} <- authorization_vars(env, opts) do
+ {:ok,
+ vars
+ |> calculated_authorization_values
+ |> create_header}
+ end
end
defp authorization_vars(env, opts) do
- unauthorized_response =
- env.__module__.get(
- env.__client__,
- env.url,
- opts: Keyword.put(env.opts || [], :digest_auth_handshake, true)
- )
-
- %{
- username: opts[:username] || "",
- password: opts[:password] || "",
- path: URI.parse(env.url).path,
- auth:
- Tesla.get_header(unauthorized_response, "www-authenticate")
- |> parse_www_authenticate_header,
- method: env.method |> to_string |> String.upcase(),
- client_nonce: (opts[:cnonce_fn] || &cnonce/0).(),
- nc: opts[:nc] || "00000000"
- }
+ with {:ok, unauthorized_response} <-
+ env.__module__.get(
+ env.__client__,
+ env.url,
+ opts: Keyword.put(env.opts || [], :digest_auth_handshake, true)
+ ) do
+ {:ok,
+ %{
+ username: opts[:username] || "",
+ password: opts[:password] || "",
+ path: URI.parse(env.url).path,
+ auth:
+ Tesla.get_header(unauthorized_response, "www-authenticate")
+ |> parse_www_authenticate_header,
+ method: env.method |> to_string |> String.upcase(),
+ client_nonce: (opts[:cnonce_fn] || &cnonce/0).(),
+ nc: opts[:nc] || "00000000"
+ }}
+ end
end
defp calculated_authorization_values(%{auth: auth}) when auth == %{}, do: []
defp calculated_authorization_values(auth_vars) do
[
{"username", auth_vars.username},
{"realm", auth_vars.auth["realm"]},
{"uri", auth_vars[:path]},
{"nonce", auth_vars.auth["nonce"]},
{"nc", auth_vars.nc},
{"cnonce", auth_vars.client_nonce},
{"response", response(auth_vars)},
# hard-coded, will not work for MD5-sess
{"algorithm", "MD5"},
# hard-coded, will not work for auth-int or unspecified
{"qop", "auth"}
]
end
defp single_header_val({k, v}) when k in ~w(nc qop algorithm), do: "#{k}=#{v}"
defp single_header_val({k, v}), do: "#{k}=\"#{v}\""
defp create_header([]), do: []
defp create_header(calculated_authorization_values) do
vals =
calculated_authorization_values
|> Enum.reduce([], fn val, acc -> [single_header_val(val) | acc] end)
|> Enum.join(", ")
[{"authorization", "Digest #{vals}"}]
end
defp ha1(%{username: username, auth: %{"realm" => realm}, password: password}) do
md5("#{username}:#{realm}:#{password}")
end
defp ha2(%{method: method, path: path}) do
md5("#{method}:#{path}")
end
defp response(%{auth: %{"nonce" => nonce}, nc: nc, client_nonce: client_nonce} = auth_vars) do
md5("#{ha1(auth_vars)}:#{nonce}:#{nc}:#{client_nonce}:auth:#{ha2(auth_vars)}")
end
defp parse_www_authenticate_header(nil), do: %{}
defp parse_www_authenticate_header(header) do
Regex.scan(~r/(\w+?)="(.+?)"/, header)
|> Enum.reduce(%{}, fn [_, key, val], acc -> Map.merge(acc, %{key => val}) end)
end
defp md5(data), do: Base.encode16(:erlang.md5(data), case: :lower)
defp cnonce, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
end
diff --git a/test/tesla/middleware/digest_auth_test.exs b/test/tesla/middleware/digest_auth_test.exs
index 15f2a0e..65394fb 100644
--- a/test/tesla/middleware/digest_auth_test.exs
+++ b/test/tesla/middleware/digest_auth_test.exs
@@ -1,103 +1,107 @@
defmodule Tesla.Middleware.DigestAuthTest do
use ExUnit.Case, async: false
defmodule DigestClient do
use Tesla
adapter fn env ->
- cond do
- env.url == "/no-digest-auth" ->
- env
-
- Tesla.get_header(env, "authorization") ->
- env
-
- true ->
- Tesla.put_headers(env, [
- {"www-authenticate",
- """
- Digest realm="testrealm@host.com",
- qop="auth,auth-int",
- nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
- opaque="5ccc069c403ebaf9f0171e9517f40e41"
- """}
- ])
- end
+ {:ok,
+ cond do
+ env.url == "/no-digest-auth" ->
+ env
+
+ Tesla.get_header(env, "authorization") ->
+ env
+
+ true ->
+ Tesla.put_headers(env, [
+ {"www-authenticate",
+ """
+ Digest realm="testrealm@host.com",
+ qop="auth,auth-int",
+ nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
+ opaque="5ccc069c403ebaf9f0171e9517f40e41"
+ """}
+ ])
+ end}
end
def client(username, password, opts \\ %{}) do
Tesla.build_client([
{
Tesla.Middleware.DigestAuth,
Map.merge(
%{
username: username,
password: password,
cnonce_fn: fn -> "0a4f113b" end,
nc: "00000001"
},
opts
)
}
])
end
end
defmodule DigestClientWithDefaults do
use Tesla
def client do
Tesla.build_client([
{Tesla.Middleware.DigestAuth, nil}
])
end
end
test "sends request with proper authorization header" do
- request =
- DigestClient.client("Mufasa", "Circle Of Life") |> DigestClient.get("/dir/index.html")
+ assert {:ok, request} =
+ DigestClient.client("Mufasa", "Circle Of Life")
+ |> DigestClient.get("/dir/index.html")
auth_header = Tesla.get_header(request, "authorization")
assert auth_header =~ ~r/^Digest /
assert auth_header =~ "username=\"Mufasa\""
assert auth_header =~ "realm=\"testrealm@host.com\""
assert auth_header =~ "algorithm=MD5"
assert auth_header =~ "qop=auth"
assert auth_header =~ "uri=\"/dir/index.html\""
assert auth_header =~ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""
assert auth_header =~ "nc=00000001"
assert auth_header =~ "cnonce=\"0a4f113b\""
assert auth_header =~ "response=\"6629fae49393a05397450978507c4ef1\""
end
test "has default values for username and cn" do
- response = DigestClientWithDefaults.client() |> DigestClient.get("/")
+ assert {:ok, response} = DigestClientWithDefaults.client() |> DigestClient.get("/")
auth_header = Tesla.get_header(response, "authorization")
assert auth_header =~ "username=\"\""
assert auth_header =~ "nc=00000000"
end
test "generates different cnonce with each request by default" do
- request = fn -> DigestClientWithDefaults.client() |> DigestClient.get("/") end
+ assert {:ok, env} = DigestClientWithDefaults.client() |> DigestClient.get("/")
+ [_, cnonce_1 | _] = Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(env, "authorization"))
- cnonce_1 =
- Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
-
- cnonce_2 =
- Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(request.(), "authorization")) |> Enum.at(1)
+ assert {:ok, env} = DigestClientWithDefaults.client() |> DigestClient.get("/")
+ [_, cnonce_2 | _] = Regex.run(~r/cnonce="(.*?)"/, Tesla.get_header(env, "authorization"))
assert cnonce_1 != cnonce_2
end
test "works when passing custom opts" do
- request = DigestClientWithDefaults.client() |> DigestClient.get("/", opts: [hodor: "hodor"])
+ assert {:ok, request} =
+ DigestClientWithDefaults.client() |> DigestClient.get("/", opts: [hodor: "hodor"])
+
assert request.opts == [hodor: "hodor"]
end
test "ignores digest auth when server doesn't respond with www-authenticate header" do
- response = DigestClientWithDefaults.client() |> DigestClient.get("/no-digest-auth")
+ assert {:ok, response} =
+ DigestClientWithDefaults.client() |> DigestClient.get("/no-digest-auth")
+
refute Tesla.get_header(response, "authorization")
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 2, 9:18 AM (3 h, 59 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1804151
Default Alt Text
(9 KB)

Event Timeline