From 158442675f757fc13237d3566a8d1c4e131d8539 Mon Sep 17 00:00:00 2001 From: Johan Castiblanco Date: Fri, 17 Nov 2023 15:32:02 -0500 Subject: [PATCH 1/6] feat: add pkce-openid-backend This add a 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 this has it code for `code_challenge` and `code_challenge_method`implementation. PR: https://github.com/python-social-auth/social-core/pull/856 --- eox_core/social_tpa_backends.py | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 5936140ef..451324071 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 @@ -199,3 +201,72 @@ def get_user_id(self, *args, **kwargs): LOG.info("Updating uid: %s to %s", uid, slug_uid) return slug_uid + + +class ConfigurableOpenIdConnectAuthPKCE(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 this has it code 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' + PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" + PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 + USE_PKCE = True + + def create_code_verifier(self): + name = 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 = 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 + elif method == "plain": + return code_verifier + else: + raise AuthException("Unsupported code challenge method.") + + def auth_params(self, state=None): + params = super().auth_params(state=state) + + if self.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.USE_PKCE: + code_verifier = self.get_code_verifier() + params["code_verifier"] = code_verifier + + return params + From 53dee2faabd039e132b5e997e5089c64eb4de04f Mon Sep 17 00:00:00 2001 From: Johan Castiblanco <51926076+johanv26@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:47:45 -0500 Subject: [PATCH 2/6] refactor: apply suggestions from code review Co-authored-by: Omar Al-Ithawi @ NELC <134635705+omar-nelc@users.noreply.github.com> chore: suggestions from code review Co-authored-by: Omar Al-Ithawi @ NELC <134635705+omar-nelc@users.noreply.github.com> --- eox_core/social_tpa_backends.py | 65 ++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 451324071..2ccc5cf8a 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -201,9 +201,72 @@ def get_user_id(self, *args, **kwargs): LOG.info("Updating uid: %s to %s", uid, slug_uid) return slug_uid +# TODO: 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 +class BaseOAuth2PKCEMixin: + """ + 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 + USE_PKCE = True + + def create_code_verifier(self): + name = 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 = 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 + elif method == "plain": + return code_verifier + else: + raise AuthException("Unsupported code challenge method.") + + def auth_params(self, state=None): + params = super().auth_params(state=state) + + if self.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.USE_PKCE: + code_verifier = self.get_code_verifier() + params["code_verifier"] = code_verifier + + return params -class ConfigurableOpenIdConnectAuthPKCE(ConfigurableOpenIdConnectAuth): +class ConfigurableOpenIdConnectAuthPKCE(BaseOAuth2PKCEMixin, ConfigurableOpenIdConnectAuth): """ Generic backend based in ConfigurableOpenIdConnectAuth but with PKCE. From 6664e76380b9385572eddd2222b7d45163013a42 Mon Sep 17 00:00:00 2001 From: Johan Castiblanco Date: Tue, 21 Nov 2023 09:56:14 -0500 Subject: [PATCH 3/6] feat: reorder some logic for the mixin --- eox_core/social_tpa_backends.py | 63 ++++----------------------------- 1 file changed, 7 insertions(+), 56 deletions(-) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 2ccc5cf8a..53e7bf3e4 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -201,9 +201,11 @@ def get_user_id(self, *args, **kwargs): LOG.info("Updating uid: %s to %s", uid, slug_uid) return slug_uid -# TODO: 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 + + 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 @@ -266,13 +268,14 @@ def auth_complete_params(self, state=None): return params + class ConfigurableOpenIdConnectAuthPKCE(BaseOAuth2PKCEMixin, ConfigurableOpenIdConnectAuth): """ - Generic backend based in ConfigurableOpenIdConnectAuth but + 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 this has it code for `code_challenge` and `code_challenge_method`implementation. + 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 @@ -281,55 +284,3 @@ class ConfigurableOpenIdConnectAuthPKCE(BaseOAuth2PKCEMixin, ConfigurableOpenIdC PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 USE_PKCE = True - - def create_code_verifier(self): - name = 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 = 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 - elif method == "plain": - return code_verifier - else: - raise AuthException("Unsupported code challenge method.") - - def auth_params(self, state=None): - params = super().auth_params(state=state) - - if self.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.USE_PKCE: - code_verifier = self.get_code_verifier() - params["code_verifier"] = code_verifier - - return params - From 816e40f48cd61cab45f288ccf4bf8caf149762b4 Mon Sep 17 00:00:00 2001 From: Johan Castiblanco Date: Tue, 21 Nov 2023 10:35:35 -0500 Subject: [PATCH 4/6] refactor: avoid repeated pkce conf defintion --- eox_core/social_tpa_backends.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 53e7bf3e4..89016a5a6 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -281,6 +281,3 @@ class ConfigurableOpenIdConnectAuthPKCE(BaseOAuth2PKCEMixin, ConfigurableOpenIdC """ name = 'config-based-openidconnect-PKCE' - PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" - PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 - USE_PKCE = True From bab0bd5755b9fb233acd9a41aa228b3a9a54affa Mon Sep 17 00:00:00 2001 From: Johan Castiblanco Date: Tue, 21 Nov 2023 11:55:00 -0500 Subject: [PATCH 5/6] feat: update with small changes social_core --- eox_core/social_tpa_backends.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 89016a5a6..84e17121a 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -215,7 +215,7 @@ class BaseOAuth2PKCEMixin: PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 - USE_PKCE = True + DEFAULT_USE_PKCE = True def create_code_verifier(self): name = self.name + "_code_verifier" @@ -246,7 +246,7 @@ def generate_code_challenge(self, code_verifier, challenge_method): def auth_params(self, state=None): params = super().auth_params(state=state) - if self.USE_PKCE: + 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, @@ -262,7 +262,7 @@ def auth_params(self, state=None): def auth_complete_params(self, state=None): params = super().auth_complete_params(state=state) - if self.USE_PKCE: + if self.setting("USE_PKCE", default=self.DEFAULT_USE_PKCE): code_verifier = self.get_code_verifier() params["code_verifier"] = code_verifier From 5a4fc333dad64cc03cf87da9931204210b5785de Mon Sep 17 00:00:00 2001 From: Johan Castiblanco <51926076+johanv26@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:22:55 -0500 Subject: [PATCH 6/6] refactor: suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrey CaƱon --- eox_core/social_tpa_backends.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 84e17121a..babf5364b 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -218,7 +218,7 @@ class BaseOAuth2PKCEMixin: DEFAULT_USE_PKCE = True def create_code_verifier(self): - name = self.name + "_code_verifier" + name = f"{self.name}_code_verifier" code_verifier_len = self.setting( "PKCE_CODE_VERIFIER_LENGTH", default=self.PKCE_DEFAULT_CODE_VERIFIER_LENGTH ) @@ -227,7 +227,7 @@ def create_code_verifier(self): return code_verifier def get_code_verifier(self): - name = self.name + "_code_verifier" + name = f"{self.name}_code_verifier" code_verifier = self.strategy.session_get(name) return code_verifier @@ -238,10 +238,9 @@ def generate_code_challenge(self, code_verifier, challenge_method): encoded = base64.urlsafe_b64encode(hashed) code_challenge = encoded.decode().replace("=", "") # remove padding return code_challenge - elif method == "plain": + if method == "plain": return code_verifier - else: - raise AuthException("Unsupported code challenge method.") + raise AuthException("Unsupported code challenge method.") def auth_params(self, state=None): params = super().auth_params(state=state)