Repository: Colin-b/httpx_auth

Issue:
AWS auth fails for temporary cedentials
I tested with the latest versions and the aws auth is failing for temporary credentials ( those that require a session token).  The version from the original PR I pushed still works so its not a change in AWS.  I will take a look soon ( next couple of days) to see if I can figure out what broke it. 

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 46dceed..6f9a825 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,10 +6,10 @@ python:
 install:
   - pip install .[testing]
 script:
-  - pytest --cov=httpx_auth --cov-fail-under=100
+  - pytest --cov=httpx_auth --cov-fail-under=100 --cov-report=term-missing
 deploy:
   provider: pypi
   username: __token__
   edge: true
   distributions: "sdist bdist_wheel"
-  skip_existing: true
\ No newline at end of file
+  skip_existing: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3df921..29a08b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+## [0.5.1] - 2020-08-31
+### Fixed
+- `AWSAuth` authentication class now handles empty path. Thanks to [`Michael E. Martinka`](https://github.com/martinka). This class is still considered as under development and subject to breaking changes without notice.
+
+### Changed
+- All methods within `AWSAuth` are now private. They were never meant to be exposed anyway.
+
 ## [0.5.0] - 2020-08-19
 ### Added
 - Allow to provide an `httpx.Client` instance for `*AuthorizationCode` flows (even `PKCE`), `*ClientCredentials` and `*ResourceOwnerPasswordCredentials` flows.
@@ -47,7 +54,8 @@ Note that a few changes were made:
 ### Added
 - Placeholder for port of requests_auth to httpx
 
-[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.5.0...HEAD
+[Unreleased]: https://github.com/Colin-b/httpx_auth/compare/v0.5.1...HEAD
+[0.5.1]: https://github.com/Colin-b/httpx_auth/compare/v0.5.0...v0.5.1
 [0.5.0]: https://github.com/Colin-b/httpx_auth/compare/v0.4.0...v0.5.0
 [0.4.0]: https://github.com/Colin-b/httpx_auth/compare/v0.3.0...v0.4.0
 [0.3.0]: https://github.com/Colin-b/httpx_auth/compare/v0.2.0...v0.3.0
diff --git a/httpx_auth/aws.py b/httpx_auth/aws.py
index a486b83..9bef332 100644
--- a/httpx_auth/aws.py
+++ b/httpx_auth/aws.py
@@ -9,11 +9,10 @@ import re
 import shlex
 import datetime
 from urllib.parse import urlparse, parse_qs, quote, unquote
+from typing import Generator, List, Tuple
 
 import httpx
 
-from typing import Generator, List, Tuple
-
 
 class AWS4Auth(httpx.Auth):
     """
@@ -77,11 +76,11 @@ class AWS4Auth(httpx.Auth):
         if self.security_token:
             request.headers["x-amz-security-token"] = self.security_token
 
-        cano_headers, signed_headers = self.get_canonical_headers(
+        cano_headers, signed_headers = self._get_canonical_headers(
             request, self.include_headers
         )
-        cano_req = self.get_canonical_request(request, cano_headers, signed_headers)
-        sig_string = self.get_sig_string(request, cano_req, scope)
+        cano_req = self._get_canonical_request(request, cano_headers, signed_headers)
+        sig_string = self._get_sig_string(request, cano_req, scope)
         sig_string = sig_string.encode("utf-8")
         signature = hmac.new(signing_key, sig_string, hashlib.sha256).hexdigest()
 
@@ -92,7 +91,7 @@ class AWS4Auth(httpx.Auth):
         request.headers["Authorization"] = auth_str
         yield request
 
-    def get_canonical_request(
+    def _get_canonical_request(
         self, req: httpx.Request, cano_headers: str, signed_headers: str
     ) -> str:
         """
@@ -105,12 +104,12 @@ class AWS4Auth(httpx.Auth):
         """
         url_str = str(req.url)
         url = urlparse(url_str)
-        path = self.amz_cano_path(url.path)
+        path = self._amz_cano_path(url.path)
         # AWS handles "extreme" querystrings differently to urlparse
         # (see post-vanilla-query-nonunreserved test in aws_testsuite)
         split = url_str.split("?", 1)
         qs = split[1] if len(split) == 2 else ""
-        qs = self.amz_cano_querystring(qs)
+        qs = self._amz_cano_querystring(qs)
         payload_hash = req.headers["x-amz-content-sha256"]
         req_parts = [
             req.method.upper(),
@@ -123,7 +122,7 @@ class AWS4Auth(httpx.Auth):
         return "\n".join(req_parts)
 
     @classmethod
-    def get_canonical_headers(
+    def _get_canonical_headers(
         cls, req: httpx.Request, include: List[str]
     ) -> Tuple[str, str]:
         """
@@ -142,11 +141,6 @@ class AWS4Auth(httpx.Auth):
         """
         include = [x.lower() for x in include]
         headers = req.headers.copy()
-        # Temporarily include the host header - AWS requires it to be included
-        # in the signed headers, but Requests doesn't include it in a
-        # PreparedRequest
-        if "host" not in headers:
-            headers["host"] = req.url.host
         # Aggregate for upper/lowercase header name collisions in header names,
         # AMZ requires values of colliding headers be concatenated into a
         # single header with lowercase name.  Although this is not possible with
@@ -155,7 +149,7 @@ class AWS4Auth(httpx.Auth):
         cano_headers_dict = {}
         for hdr, val in headers.items():
             hdr = hdr.strip().lower()
-            val = cls.amz_norm_whitespace(val).strip()
+            val = cls._amz_norm_whitespace(val).strip()
             if (
                 hdr in include
                 or "*" in include
@@ -179,7 +173,7 @@ class AWS4Auth(httpx.Auth):
         return cano_headers, signed_headers
 
     @staticmethod
-    def get_sig_string(req: httpx.Request, cano_req: str, scope: str) -> str:
+    def _get_sig_string(req: httpx.Request, cano_req: str, scope: str) -> str:
         """
         Generate the AWS4 auth string to sign for the request.
         req      -- This should already include an x-amz-date header.
@@ -192,13 +186,15 @@ class AWS4Auth(httpx.Auth):
         sig_string = "\n".join(sig_items)
         return sig_string
 
-    def amz_cano_path(self, path):
+    def _amz_cano_path(self, path):
         """
         Generate the canonical path as per AWS4 auth requirements.
         Not documented anywhere, determined from aws4_testsuite examples,
         problem reports and testing against the live services.
         path -- request path
         """
+        if len(path) == 0:
+            path = "/"
         safe_chars = "/~"
         fixed_path = path
         fixed_path = posixpath.normpath(fixed_path)
@@ -212,7 +208,7 @@ class AWS4Auth(httpx.Auth):
         return quote(full_path, safe=safe_chars)
 
     @staticmethod
-    def amz_cano_querystring(qs):
+    def _amz_cano_querystring(qs):
         """
         Parse and format querystring as per AWS4 auth requirements.
         Perform percent quoting as needed.
@@ -237,7 +233,7 @@ class AWS4Auth(httpx.Auth):
         return qs
 
     @staticmethod
-    def amz_norm_whitespace(text):
+    def _amz_norm_whitespace(text):
         """
         Replace runs of whitespace with a single space.
         Ignore text enclosed in quotes.
diff --git a/httpx_auth/oauth2_authentication_responses_server.py b/httpx_auth/oauth2_authentication_responses_server.py
index 43ef2d4..9ea4688 100644
--- a/httpx_auth/oauth2_authentication_responses_server.py
+++ b/httpx_auth/oauth2_authentication_responses_server.py
@@ -6,7 +6,12 @@ from socket import socket
 
 import httpx
 
-from httpx_auth.errors import *
+from httpx_auth.errors import (
+    InvalidGrantRequest,
+    GrantNotProvided,
+    StateNotProvided,
+    TimeoutOccurred,
+)
 
 logger = logging.getLogger(__name__)
 
diff --git a/httpx_auth/oauth2_tokens.py b/httpx_auth/oauth2_tokens.py
index d71e076..618eee3 100644
--- a/httpx_auth/oauth2_tokens.py
+++ b/httpx_auth/oauth2_tokens.py
@@ -5,7 +5,7 @@ import datetime
 import threading
 import logging
 
-from httpx_auth.errors import *
+from httpx_auth.errors import InvalidToken, TokenExpiryNotProvided, AuthenticationFailed
 
 logger = logging.getLogger(__name__)
 
diff --git a/httpx_auth/version.py b/httpx_auth/version.py
index c4b9ff7..4e62895 100644
--- a/httpx_auth/version.py
+++ b/httpx_auth/version.py
@@ -3,4 +3,4 @@
 # Major should be incremented in case there is a breaking change. (eg: 2.5.8 -> 3.0.0)
 # Minor should be incremented in case there is an enhancement. (eg: 2.5.8 -> 2.6.0)
 # Patch should be incremented in case there is a bug fix. (eg: 2.5.8 -> 2.5.9)
-__version__ = "0.5.0"
+__version__ = "0.5.1"
diff --git a/setup.py b/setup.py
index d27afc4..87d44b4 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@ setup(
             # Used to generate test tokens
             "pyjwt==1.*",
             # Used to mock httpx
-            "pytest_httpx==0.7.*",
+            "pytest_httpx==0.8.*",
             # Used to check coverage
             "pytest-cov==2.*",
         ]


Test patch:
diff --git a/tests/test_aws4auth.py b/tests/test_aws4auth.py
index d78c71d..f86d2d6 100644
--- a/tests/test_aws4auth.py
+++ b/tests/test_aws4auth.py
@@ -356,10 +356,8 @@ def test_aws_auth_without_content_in_request(httpx_mock: HTTPXMock, mock_aws_dat
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=b26b1ba261652e67fee5174c7fa1de1ef8f74e9d8e427528e197ce5e64d52d74"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ce708380ee69b1a9558b9b0dddd4d15f35a2a5e5ea3534b541247f1a746626db"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -380,10 +378,8 @@ def test_aws_auth_with_content_in_request(httpx_mock: HTTPXMock, mock_aws_dateti
         == "fb65c1441d6743274738fe3b3042a73167ba1fb2d34679d8dd16433473758f97"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, "
-        "Signature=a70f3cf3c14bd0e2cc048dfb7ddf63f9b2c12615476ebcb75f224f7a0192e383"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=5f4f832a19fc834d4f34047289ad67d96da25bd414a70f02ce6b85aef9ab8068"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -407,10 +403,8 @@ def test_aws_auth_with_security_token_and_without_content_in_request(
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, "
-        "Signature=be2b7efe21f69856b1dae871064627909cc1cac0749f3237dee0df99123e21a3"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=2ae27ce5e8dcc005736c97ff857e4f44401fc3a33d8358b1d67c079f0f5a8b3e"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
     assert headers["x-amz-security-token"] == "security_token"
@@ -435,10 +429,8 @@ def test_aws_auth_with_security_token_and_content_in_request(
         == "fb65c1441d6743274738fe3b3042a73167ba1fb2d34679d8dd16433473758f97"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, "
-        "Signature=ff98a199b570988a5d2891939a1a4a5e98e4171329a53c7306fc7a19ef6cad23"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=e02c4733589cf6e80361f6905564da6d0c23a0829bb3c3899b328e43b2f7b581"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
     assert headers["x-amz-security-token"] == "security_token"
@@ -462,10 +454,8 @@ def test_aws_auth_override_x_amz_date_header(httpx_mock: HTTPXMock, mock_aws_dat
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=b26b1ba261652e67fee5174c7fa1de1ef8f74e9d8e427528e197ce5e64d52d74"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ce708380ee69b1a9558b9b0dddd4d15f35a2a5e5ea3534b541247f1a746626db"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -486,10 +476,8 @@ def test_aws_auth_root_path(httpx_mock: HTTPXMock, mock_aws_datetime):
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=ce708380ee69b1a9558b9b0dddd4d15f35a2a5e5ea3534b541247f1a746626db"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ce708380ee69b1a9558b9b0dddd4d15f35a2a5e5ea3534b541247f1a746626db"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -510,10 +498,8 @@ def test_aws_auth_query_parameters(httpx_mock: HTTPXMock, mock_aws_datetime):
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=959173877981331c60d6b4cf45795a922f6639ec9714837ebb5ff009ae129fde"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=f2b8a73e388dc04586b5bcc208c6e50d92f04a1296e561229cd88811ad2494e9"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -534,10 +520,8 @@ def test_aws_auth_path_normalize(httpx_mock: HTTPXMock, mock_aws_datetime):
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=e49fb885d30c9e74901071748b783fabe8ba7a979aa20420ac76af1dda1edd03"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=e49fb885d30c9e74901071748b783fabe8ba7a979aa20420ac76af1dda1edd03"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -560,10 +544,8 @@ def test_aws_auth_path_quoting(httpx_mock: HTTPXMock, mock_aws_datetime):
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=98dd3cdd2a603907495164f08fe7197fb405bf8c556ddf7b88d7e15341a9588a"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=98dd3cdd2a603907495164f08fe7197fb405bf8c556ddf7b88d7e15341a9588a"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -586,10 +568,8 @@ def test_aws_auth_path_percent_encode_non_s3(httpx_mock: HTTPXMock, mock_aws_dat
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/iam/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=1da6c689b7a20044144a9f265ddecc38b1b884902846fbe4dc8049595f25565f"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=1da6c689b7a20044144a9f265ddecc38b1b884902846fbe4dc8049595f25565f"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"
 
@@ -612,9 +592,29 @@ def test_aws_auth_path_percent_encode_s3(httpx_mock: HTTPXMock, mock_aws_datetim
         == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
     )
     assert (
-        headers["Authorization"] == "AWS4-HMAC-SHA256 "
-        "Credential=access_id/20181011/us-east-1/s3/aws4_request, "
-        "SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
-        "Signature=2fc7c2f27151e18348862bab0bbe90c4a9f29d7863a33e725d7b1ec96709fdd6"
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2fc7c2f27151e18348862bab0bbe90c4a9f29d7863a33e725d7b1ec96709fdd6"
+    )
+    assert headers["x-amz-date"] == "20181011T150505Z"
+
+
+def test_aws_auth_without_path(httpx_mock: HTTPXMock, mock_aws_datetime):
+    auth = httpx_auth.AWS4Auth(
+        access_id="access_id",
+        secret_key="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        region="us-east-1",
+        service="iam",
+    )
+    httpx_mock.add_response(url="http://authorized_only")
+
+    httpx.get("http://authorized_only", auth=auth)
+    headers = httpx_mock.get_request().headers
+    assert (
+        headers["x-amz-content-sha256"]
+        == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+    )
+    assert (
+        headers["Authorization"]
+        == "AWS4-HMAC-SHA256 Credential=access_id/20181011/us-east-1/iam/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=e3411118ac098a820690144b8b273aa64a3366d899fa68fd64a1ab950c982b4b"
     )
     assert headers["x-amz-date"] == "20181011T150505Z"