Repository: edgedb/edgedb

Issue:
Aliasing the PKCE Parameter "challenge" for RFC Alignment
Currently the PKCE flow for the Auth Module expects the following parameter, `challenge`.

The [PKCE RFC's](https://datatracker.ietf.org/doc/html/rfc7636#section-6.1) Parameter for this is `code_challenge`.

Given the amount of code that likely targets `challenge`, I'd assume it would be best to let both `challenge` and `code_challenge` work for a period of time, with `challenge` taking precedence for the foreseeable future.

This would allow for easier integrations with Spring Security for example, as it currently supports EdgeDB Auth until you hit login.

Documentation on if EdgeDB supports the `code_challenge_method` would be good as well.


Discussion:
@Crain-32 

Thanks for the feedback! Yeah, I think we can add an alias for `code_challenge` and `code_verifier` to conform to the RFC. We implement PKCE outside of OAuth, so didn't feel beholden to the original RFC since EdgeDB Auth is not itself an OAuth authorization flow, but if it makes it easier to integrate existing tooling, I'm 100% down.

> Documentation on if EdgeDB supports the `code_challenge_method` would be good as well.

We don't currently support it since we _require_ SHA256 and we're breaking with the RFC here by not assuming `plain` by default here. I'd be happy to add the parameter, with the caveat that we would reject if you ask for `plain`, so it would in essence be superfluous (missing would default to `S256` and present would only allow `S256`), which is not compliant with the RFC. Any thoughts on that?
Hey @scotttrinh!

Integration with `code_challenge_method` isn't really something I'm partial towards, but it should be nice to state that SHA256 is a hard requirement. Currently the [documentation](https://www.edgedb.com/docs/guides/auth/built_in_ui#start-the-pkce-flow) only says
> Take this verifier string, hash it with SHA256, and then base64url encode the resulting string. This new string is called the challenge.

I think just a minor adjustment to the [declaration](https://www.edgedb.com/docs/guides/auth/built_in_ui#example-implementation) of supporting PKCE would be a simple way to tell that.

> We secure authentication tokens and other sensitive data by using PKCE (Proof Key of Code Exchange), and only support the usage of SHA256.
@Crain-32 Great, thanks for the feedback and direction there. Should be a pretty quick fix.

Resolving patch:
diff --git a/edb/server/protocol/auth_ext/http.py b/edb/server/protocol/auth_ext/http.py
--- a/edb/server/protocol/auth_ext/http.py
+++ b/edb/server/protocol/auth_ext/http.py
@@ -253,7 +253,9 @@ async def handle_authorize(self, request: Any, response: Any):
             raise errors.InvalidData(
                 "Redirect URL does not match any allowed URLs.",
             )
-        challenge = _get_search_param(query, "challenge")
+        challenge = _get_search_param(
+            query, "challenge", fallback_keys=["code_challenge"]
+        )
         oauth_client = oauth.Client(
             db=self.db, provider_name=provider_name, base_url=self.test_url
         )
@@ -386,7 +388,9 @@ async def handle_token(self, request: Any, response: Any):
             request.url.query.decode("ascii") if request.url.query else ""
         )
         code = _get_search_param(query, "code")
-        verifier = _get_search_param(query, "verifier")
+        verifier = _get_search_param(
+            query, "verifier", fallback_keys=["code_verifier"]
+        )
 
         verifier_size = len(verifier)
 
@@ -1338,7 +1342,9 @@ async def handle_ui_forgot_password(self, request: Any, response: Any):
             query = urllib.parse.parse_qs(
                 request.url.query.decode("ascii") if request.url.query else ""
             )
-            challenge = _get_search_param(query, "challenge")
+            challenge = _get_search_param(
+                query, "challenge", fallback_keys=["code_challenge"]
+            )
             app_details_config = self._get_app_details_config()
 
             response.status = http.HTTPStatus.OK
@@ -1660,9 +1666,7 @@ def _make_session_token(self, identity_id: str) -> str:
             claims=claims,
         )
         session_token.make_signed_token(signing_key)
-        metrics.auth_successful_logins.inc(
-            1.0, self.tenant.get_instance_name()
-        )
+        metrics.auth_successful_logins.inc(1.0, self.tenant.get_instance_name())
         return session_token.serialize()
 
     def _get_from_claims(self, state: str, key: str) -> str:
@@ -1987,8 +1991,18 @@ def _maybe_get_search_param(
     return params[0] if params else None
 
 
-def _get_search_param(query_dict: dict[str, list[str]], key: str) -> str:
+def _get_search_param(
+    query_dict: dict[str, list[str]],
+    key: str,
+    *,
+    fallback_keys: Optional[list[str]] = None,
+) -> str:
     val = _maybe_get_search_param(query_dict, key)
+    if val is None and fallback_keys is not None:
+        for fallback_key in fallback_keys:
+            val = _maybe_get_search_param(query_dict, fallback_key)
+            if val is not None:
+                break
     if val is None:
         raise errors.InvalidData(f"Missing query parameter: {key}")
     return val
@@ -2010,7 +2024,9 @@ def _get_pkce_challenge(
     query_dict: dict[str, list[str]],
 ) -> str | None:
     cookie_name = 'edgedb-pkce-challenge'
-    challenge: str | None = _maybe_get_search_param(query_dict, 'challenge')
+    challenge: str | None = _maybe_get_search_param(
+        query_dict, 'challenge'
+    ) or _maybe_get_search_param(query_dict, "code_challenge")
     if challenge is not None:
         _set_cookie(response, cookie_name, challenge)
     else:


Test patch:
diff --git a/tests/test_http_ext_auth.py b/tests/test_http_ext_auth.py
--- a/tests/test_http_ext_auth.py
+++ b/tests/test_http_ext_auth.py
@@ -1071,7 +1071,7 @@ async def test_http_auth_ext_discord_authorize_01(self):
             query = {
                 "provider": provider_name,
                 "redirect_to": redirect_to,
-                "challenge": challenge,
+                "code_challenge": challenge,
             }
 
             _, headers, status = self.http_con_request(
@@ -3042,7 +3042,7 @@ async def test_http_auth_ext_token_01(self):
                 http_con,
                 {
                     "code": pkce.id,
-                    "verifier": base64.urlsafe_b64encode(os.urandom(43))
+                    "code_verifier": base64.urlsafe_b64encode(os.urandom(43))
                     .rstrip(b"=")
                     .decode(),
                 },