Skip to content

Support DigestAuth with "x-www-authenticate" in first response header #3306

@kiryph

Description

@kiryph

The Fronius inverter for photovoltaic systems uses for its local REST API digest authentication.

It uses in its initial response header "X-WWW-Authenticate" instead of "www-authenticate" and hence make the httpx request fail.

Here an initial curl command with the response header:

❯ curl -I "http://192.168.1.141/config/emrs"
HTTP/1.1 401 Unauthorized
X-WWW-Authenticate: Digest realm="Webinterface area", charset="UTF-8", algorithm=MD5, nonce="66ec2f8d:36a65537553cf788606762854336a0b3", qop="auth"
Content-Type: text/html
Content-Length: 347
Date: Thu, 19 Sep 2024 14:05:01 GMT
Server: webserver

httpx 0.27.2 fails to extract X-WWW-Authenticate with nonce, qop, algorithm, realm in

httpx/httpx/_auth.py

Lines 201 to 212 in 87713d2

if response.status_code != 401 or "www-authenticate" not in response.headers:
# If the response is not a 401 then we don't
# need to build an authenticated request.
return
for auth_header in response.headers.get_list("www-authenticate"):
if auth_header.lower().startswith("digest "):
break
else:
# If the response does not include a 'WWW-Authenticate: Digest ...'
# header, then we don't need to build an authenticated request.
return

My script

import httpx

url = "http://192.168.1.141/config/emrs" # adjust IP to that one of your inverter
user = "technician"
pw = <INSERT PW>

auth = httpx.DigestAuth(user,pw)

with httpx.Client(auth=auth) as client:
    r = client.get(url)
    print(r.status_code)
    print(r.json()['priorities'])

    r = client.get(url)
    print(r.status_code)
    print(r.json()['priorities'])

runs only with following quick diff

❯ git diff
diff --git a/httpx/_auth.py b/httpx/_auth.py
index b03971a..fb6ad69 100644
--- a/httpx/_auth.py
+++ b/httpx/_auth.py
@@ -198,12 +198,22 @@ class DigestAuth(Auth):

         response = yield request

-        if response.status_code != 401 or "www-authenticate" not in response.headers:
+        auth_strings = [ "www-authenticate", "x-www-authenticate" ]
+        auth_string = False
+        for a in auth_strings:
+            if a in response.headers:
+                if auth_string == False:
+                    auth_string = a
+                else:
+                    message = "Malformed Digest WWW-Authenticate response header"
+                    raise ProtocolError(message, request=request)
+
+        if response.status_code != 401 or auth_string == False:
             # If the response is not a 401 then we don't
             # need to build an authenticated request.
             return

-        for auth_header in response.headers.get_list("www-authenticate"):
+        for auth_header in response.headers.get_list(auth_string):
             if auth_header.lower().startswith("digest "):
                 break
         else:

Note, this diff is only to show that it works. I did not follow any coding guidelines of this project.

For reference, here a couple of projects which have written their custom digest authorization to handle the Fronius response header using the request library:

I am not sure how common it is to use X-WWW-Authenticate. Google did not return many results:

https://community.smartbear.com/discussions/readyapi-questions/digest-authentication-with-x-www-authenticate/252961

However, I still think supporting this deviation might be acceptable. If Fronius screwed this up more heavily than I think, I understand that httpx cannot support all quirks of all digest implementations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions