Skip to content

Commit

Permalink
Handle multiple auth headers correctly (#1240)
Browse files Browse the repository at this point in the history
Handle multiple auth headers correctly
  • Loading branch information
tomchristie authored Sep 1, 2020
1 parent fa7661b commit 33d339a
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 14 deletions.
22 changes: 14 additions & 8 deletions httpx/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,28 +112,34 @@ def auth_flow(self, request: Request) -> typing.Generator[Request, Response, Non
response = yield request

if response.status_code != 401 or "www-authenticate" not in response.headers:
# If the response is not a 401 WWW-Authenticate, then we don't
# If the response is not a 401 then we don't
# need to build an authenticated request.
return

challenge = self._parse_challenge(request, response)
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

challenge = self._parse_challenge(request, response, auth_header)
request.headers["Authorization"] = self._build_auth_header(request, challenge)
yield request

def _parse_challenge(
self, request: Request, response: Response
self, request: Request, response: Response, auth_header: str
) -> "_DigestAuthChallenge":
"""
Returns a challenge from a Digest WWW-Authenticate header.
These take the form of:
`Digest realm="[email protected]",qop="auth,auth-int",nonce="abc",opaque="xyz"`
"""
header = response.headers["www-authenticate"]
scheme, _, fields = auth_header.partition(" ")

scheme, _, fields = header.partition(" ")
if scheme.lower() != "digest":
message = "Header does not start with 'Digest'"
raise ProtocolError(message, request=request)
# This method should only ever have been called with a Digest auth header.
assert scheme.lower() == "digest"

header_dict: typing.Dict[str, str] = {}
for field in parse_http_list(fields):
Expand Down
21 changes: 15 additions & 6 deletions tests/client/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,21 @@ async def test_digest_auth_returns_no_auth_if_no_digest_header_in_response() ->
assert len(response.history) == 0


def test_digest_auth_returns_no_auth_if_alternate_auth_scheme() -> None:
url = "https://example.org/"
auth = DigestAuth(username="tomchristie", password="password123")
auth_header = b"Token ..."

client = httpx.Client(
transport=SyncMockTransport(auth_header=auth_header, status_code=401)
)
response = client.get(url, auth=auth)

assert response.status_code == 401
assert response.json() == {"auth": None}
assert len(response.history) == 0


@pytest.mark.asyncio
async def test_digest_auth_200_response_including_digest_auth_header() -> None:
url = "https://example.org/"
Expand Down Expand Up @@ -519,9 +534,6 @@ async def test_digest_auth_incorrect_credentials() -> None:
"auth_header",
[
b'Digest realm="[email protected]", qop="auth"', # missing fields
b'realm="[email protected]", qop="auth"', # not starting with Digest
b'DigestZ realm="[email protected]", qop="auth"'
b'qop="auth,auth-int",nonce="abc",opaque="xyz"',
b'Digest realm="[email protected]", qop="auth,au', # malformed fields list
],
)
Expand All @@ -542,9 +554,6 @@ async def test_async_digest_auth_raises_protocol_error_on_malformed_header(
"auth_header",
[
b'Digest realm="[email protected]", qop="auth"', # missing fields
b'realm="[email protected]", qop="auth"', # not starting with Digest
b'DigestZ realm="[email protected]", qop="auth"'
b'qop="auth,auth-int",nonce="abc",opaque="xyz"',
b'Digest realm="[email protected]", qop="auth,au', # malformed fields list
],
)
Expand Down

0 comments on commit 33d339a

Please sign in to comment.