Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85832856
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
9 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 2, 9:18 AM (2 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1804151
Default Alt Text
(9 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline