Repository: Colin-b/httpx_auth

Issue:
JSONDecodeError due to Improper Handling of Nested JSON Strings in JWT Payloads
## Description

There is an issue in the `httpx-auth` library where the decoding of base64-encoded JSON within JWT tokens corrupts JSON strings that contain nested JSON. This happens because the double quotes inside the nested JSON string are not correctly handled during the decoding process, leading to a failure when attempting to load the string back into a JSON object.

## Steps to Reproduce

The issue can be reproduced with the following test case:

```python
import jwt
import json
from httpx_auth._oauth2.tokens import decode_base64

def test_decode_base64_with_nested_json_string():
    # Encode a JSON inside the JWT
    dummy_token = jwt.encode({"data": json.dumps({"something": ["else"]})}, key="")
    header, body, signature = dummy_token.split(".")
    
    # Decode the body
    decoded_bytes = decode_base64(body)
    
    # Attempt to load JSON
    result = json.loads(decoded_bytes)
    assert result == {"data": '{"something": ["else"]}'}
```

Running this test results in a json.decoder.JSONDecodeError due to incorrect handling of the nested JSON string.

## Expected Behavior
The decoded JSON string should be handled correctly, allowing for proper loading into a Python dictionary without JSON parsing errors.

## Actual Behavior
The test raises the following error due to malformed JSON:
```shell
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 1 column 12 (char 11)
```

This error is caused by the way double quotes inside the nested JSON are handled, which corrupts the JSON string during the base64 decoding step.

## Environment
Python Version: 3.10.11
httpx-auth version: 0.22.0 (2024-03-02)

## Additional Context
This issue impacts scenarios where JWT tokens contain nested JSON strings as part of their payload. A fix would likely involve adjusting the base64 decoding function to correctly handle nested JSON strings without corrupting them.


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3541ca5..9c64ded 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Fixed
+- Fix `JSONDecodeError` due to Improper Handling of Nested JSON Strings in JWT Payloads
+
 ### Changed
 - Requires [`httpx`](https://www.python-httpx.org)==0.28.\*
 
diff --git a/httpx_auth/_oauth2/tokens.py b/httpx_auth/_oauth2/tokens.py
index 0e8085a..ca65c08 100644
--- a/httpx_auth/_oauth2/tokens.py
+++ b/httpx_auth/_oauth2/tokens.py
@@ -28,7 +28,7 @@ def decode_base64(base64_encoded_string: str) -> str:
     missing_padding = len(base64_encoded_string) % 4
     if missing_padding != 0:
         base64_encoded_string += "=" * (4 - missing_padding)
-    return base64.b64decode(base64_encoded_string).decode("unicode_escape")
+    return base64.urlsafe_b64decode(base64_encoded_string).decode("utf-8")
 
 
 def is_expired(expiry: float, early_expiry: float) -> bool:


Test patch:
diff --git a/tests/oauth2/tokens/__init__.py b/tests/oauth2/tokens/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/oauth2/tokens/test_tokens.py b/tests/oauth2/tokens/test_tokens.py
new file mode 100644
index 0000000..a345ec8
--- /dev/null
+++ b/tests/oauth2/tokens/test_tokens.py
@@ -0,0 +1,30 @@
+import json
+import jwt
+
+from httpx_auth._oauth2.tokens import decode_base64
+
+
+def test_decode_base64():
+    # Encode a JSON inside the JWT
+    dummy_token = jwt.encode({"name": "John"}, key="")
+    header, body, signature = dummy_token.split(".")
+
+    # Decode the body
+    decoded_bytes = decode_base64(body)
+
+    # Attempt to load JSON
+    result = json.loads(decoded_bytes)
+    assert result == {"name": "John"}
+
+
+def test_decode_base64_with_nested_json_string():
+    # Encode a JSON inside the JWT
+    dummy_token = jwt.encode({"data": json.dumps({"something": ["else"]})}, key="")
+    header, body, signature = dummy_token.split(".")
+
+    # Decode the body
+    decoded_bytes = decode_base64(body)
+
+    # Attempt to load JSON
+    result = json.loads(decoded_bytes)
+    assert result == {"data": '{"something": ["else"]}'}