Repository: dwavesystems/dwave-cloud-client

Issue:
PKCE `code_verifier` absent from fetch token request
Although `code_challenge` is present in the initial authorization request ([rfc7636, section 4.3](https://datatracker.ietf.org/doc/html/rfc7636#section-4.3)), and `code_verifier` is generated client-side, it's missing in the code exchange request (see [section 4.5](https://datatracker.ietf.org/doc/html/rfc7636#section-4.5)).

This represents a security vulnerability for public clients.

Discussion:

Patch:
diff --git a/dwave/cloud/auth/flows.py b/dwave/cloud/auth/flows.py
index 9776baa..679d752 100644
--- a/dwave/cloud/auth/flows.py
+++ b/dwave/cloud/auth/flows.py
@@ -189,6 +189,7 @@ class AuthFlow:
             url=self.token_endpoint,
             grant_type='authorization_code',
             code=code,
+            code_verifier=self.code_verifier,
             **kwargs)
 
         logger.debug(f"{type(self).__name__}.fetch_token() = {token!r}")
diff --git a/releasenotes/notes/fix-pkce-missing-code-verifier-in-fetch-token-b5cc871cc9d6dfac.yaml b/releasenotes/notes/fix-pkce-missing-code-verifier-in-fetch-token-b5cc871cc9d6dfac.yaml
new file mode 100644
index 0000000..4b6df21
--- /dev/null
+++ b/releasenotes/notes/fix-pkce-missing-code-verifier-in-fetch-token-b5cc871cc9d6dfac.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+  - |
+    Fix PKCE support in ``dwave.cloud.auth.flow.AuthFlow`` by properly including
+    ``code_verifier`` in fetch token (code exchange) requests.
+    See `#605 <https://github.com/dwavesystems/dwave-cloud-client/issues/605>`_.


Test patch:
diff --git a/tests/auth/test_flows.py b/tests/auth/test_flows.py
index 1506b19..8c897a4 100644
--- a/tests/auth/test_flows.py
+++ b/tests/auth/test_flows.py
@@ -91,14 +91,18 @@ class TestAuthFlow(unittest.TestCase):
 
         m.get(requests_mock.ANY, status_code=404)
         m.post(requests_mock.ANY, status_code=404)
+        m.post(self.token_endpoint, json=dict(error="error", error_description="bad request"))
         m.post(self.token_endpoint, additional_matcher=post_body_matcher, json=self.token)
 
         # reset creds
         self.creds.clear()
 
-        # verify token fetch flow
+        # make auth request to generate all request params (like PKCE's verifier)
         flow = AuthFlow(**self.test_args)
+        _ = flow.get_authorization_url()
+        expected_params.update(code_verifier=flow.code_verifier)
 
+        # verify token fetch flow
         response = flow.fetch_token(code=code)
         self.assertEqual(response, self.token)