diff --git a/eox_core/social_tpa_backends.py b/eox_core/social_tpa_backends.py index 62e058e6..c3319cb5 100644 --- a/eox_core/social_tpa_backends.py +++ b/eox_core/social_tpa_backends.py @@ -1,12 +1,14 @@ """ Extensions to the regular defined third party auth backends """ +import base64 +import hashlib import logging 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 @@ -32,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) @@ -192,3 +194,87 @@ 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): + """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 + ) + code_verifier = self.strategy.random_string(code_verifier_len) + self.strategy.session_set(name, code_verifier) + 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() + 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): + """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): + 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): + """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): + 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'