From 25094fcc9d5beca9a1e0bdd1d0d05155e7fd6709 Mon Sep 17 00:00:00 2001 From: Johan Castiblanco <51926076+johanv26@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:31:17 -0500 Subject: [PATCH 1/3] feat: add pkce-openid-backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a generic backend based on ConfigurableOpenIdConnectAuth but with PKCE. This backend is inspired in the social-core way to implement PKCE. There is a current PR in work, but for the moment, that class is not merged and accessible. So after that is finished this has it code for `code_challenge` and `code_challenge_method`implementation. PR: https://github.com/python-social-auth/social-core/pull/856 Co-authored-by: Omar Al-Ithawi @ NELC <134635705+omar-nelc@users.noreply.github.com> Co-authored-by: Andrey CaƱon (cherry picked from commit 708a1cede55d14c936a4773cc9ee59c60061a226) --- eox_core/social_tpa_backends.py | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 62e058e6e..894a8891f 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -1,6 +1,8 @@ """ Extensions to the regular defined third party auth backends """ +import base64 +import hashlib import logging from django.conf import settings @@ -192,3 +194,82 @@ def get_user_id(self, *args, **kwargs): LOG.info("Updating uid: %s to %s", uid, slug_uid) return slug_uid + + +class BaseOAuth2PKCEMixin: + """ + TO-DO: Use the `from social_core.backends.oauth import BaseOAuth2PKCE` base class once the pull request is merged: https://github.com/python-social-auth/social-core/pull/856/files#diff-d44db201b48f2ec7cab2a0c981213a2991630567778cc6608d03fa0e3804e466R467 + Base class for providers using OAuth2 with Proof Key for Code Exchange (PKCE). + OAuth2 details at: + https://datatracker.ietf.org/doc/html/rfc6749 + PKCE details at: + https://datatracker.ietf.org/doc/html/rfc7636 + """ + + PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" + PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 + DEFAULT_USE_PKCE = True + + def create_code_verifier(self): + name = f"{self.name}_code_verifier" + code_verifier_len = self.setting( + "PKCE_CODE_VERIFIER_LENGTH", default=self.PKCE_DEFAULT_CODE_VERIFIER_LENGTH + ) + code_verifier = self.strategy.random_string(code_verifier_len) + self.strategy.session_set(name, code_verifier) + return code_verifier + + def get_code_verifier(self): + name = f"{self.name}_code_verifier" + code_verifier = self.strategy.session_get(name) + return code_verifier + + def generate_code_challenge(self, code_verifier, challenge_method): + method = challenge_method.lower() + if method == "s256": + hashed = hashlib.sha256(code_verifier.encode()).digest() + encoded = base64.urlsafe_b64encode(hashed) + code_challenge = encoded.decode().replace("=", "") # remove padding + return code_challenge + if method == "plain": + return code_verifier + raise AuthException("Unsupported code challenge method.") + + def auth_params(self, state=None): + params = super().auth_params(state=state) + + if self.setting("USE_PKCE", default=self.DEFAULT_USE_PKCE): + code_challenge_method = self.setting( + "PKCE_CODE_CHALLENGE_METHOD", + default=self.PKCE_DEFAULT_CODE_CHALLENGE_METHOD, + ) + code_verifier = self.create_code_verifier() + code_challenge = self.generate_code_challenge( + code_verifier, code_challenge_method + ) + params["code_challenge_method"] = code_challenge_method + params["code_challenge"] = code_challenge + return params + + def auth_complete_params(self, state=None): + params = super().auth_complete_params(state=state) + + if self.setting("USE_PKCE", default=self.DEFAULT_USE_PKCE): + code_verifier = self.get_code_verifier() + params["code_verifier"] = code_verifier + + return params + + +class ConfigurableOpenIdConnectAuthPKCE(BaseOAuth2PKCEMixin, ConfigurableOpenIdConnectAuth): + """ + Generic backend based in ConfigurableOpenIdConnectAuth but + with PKCE. + This backend is inspired in the social-core way to implement PKCE. + There is a current PR in working, but for the moment, that class is not merged and accesible. + So after that is finished we use `BaseOAuth2PKCEMixin` for `code_challenge` and `code_challenge_method`implementation. + PR: https://github.com/python-social-auth/social-core/pull/856 + Block code: https://github.com/python-social-auth/social-core/pull/856/files#diff-d44db201b48f2ec7cab2a0c981213a2991630567778cc6608d03fa0e3804e466R467-R530 + + """ + name = 'config-based-openidconnect-PKCE' From 54cfb216b474a8f20ffc6c665c5325ea5fc9b9ca Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Thu, 9 Jan 2025 11:01:16 -0500 Subject: [PATCH 2/3] feat: change logger level in order to avoid unnecessary sentry notifications (cherry picked from commit 653521f74a82d562b9e6ee8dde5798a9e5ee912b) --- eox_core/social_tpa_backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 894a8891f..8b16b961d 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -34,7 +34,7 @@ def __init__(self, *args, **kwargs): try: setattr(self, key, conf.get(key, getattr(self, key))) except Exception: # pylint: disable=broad-except - LOG.error("Tried and failed to set property %s of a config-based-openidconnect", key) + LOG.warning("Tried and failed to set property %s of a config-based-openidconnect", key) super().__init__(*args, **kwargs) From 6d5e9905f4b5bd144d80a502b7b65bffb59378da Mon Sep 17 00:00:00 2001 From: Johan Castiblanco Date: Wed, 26 Nov 2025 15:56:11 -0500 Subject: [PATCH 3/3] chore: improve pylint docstrings --- eox_core/social_tpa_backends.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 8b16b961d..c3319cb53 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -8,7 +8,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from social_core.backends.open_id_connect import OpenIdConnectAuth -from social_core.exceptions import AuthMissingParameter +from social_core.exceptions import AuthException, AuthMissingParameter from eox_core.edxapp_wrapper.configuration_helpers import get_configuration_helper @@ -211,6 +211,7 @@ class BaseOAuth2PKCEMixin: DEFAULT_USE_PKCE = True def create_code_verifier(self): + """Create a new code verifier and store it in the session.""" name = f"{self.name}_code_verifier" code_verifier_len = self.setting( "PKCE_CODE_VERIFIER_LENGTH", default=self.PKCE_DEFAULT_CODE_VERIFIER_LENGTH @@ -220,11 +221,13 @@ def create_code_verifier(self): return code_verifier def get_code_verifier(self): + """Retrieve the code verifier from the session.""" name = f"{self.name}_code_verifier" code_verifier = self.strategy.session_get(name) return code_verifier def generate_code_challenge(self, code_verifier, challenge_method): + """Generate a code challenge from the code verifier.""" method = challenge_method.lower() if method == "s256": hashed = hashlib.sha256(code_verifier.encode()).digest() @@ -236,6 +239,7 @@ def generate_code_challenge(self, code_verifier, challenge_method): raise AuthException("Unsupported code challenge method.") def auth_params(self, state=None): + """Get the authentication parameters, adding PKCE parameters if enabled.""" params = super().auth_params(state=state) if self.setting("USE_PKCE", default=self.DEFAULT_USE_PKCE): @@ -252,6 +256,7 @@ def auth_params(self, state=None): return params def auth_complete_params(self, state=None): + """Get the authentication complete parameters, adding PKCE parameters if enabled.""" params = super().auth_complete_params(state=state) if self.setting("USE_PKCE", default=self.DEFAULT_USE_PKCE):