diff --git a/cms/envs/common.py b/cms/envs/common.py
index a9ea495b0841..b1b8c2cab05e 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -1082,7 +1082,7 @@
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
-DEFAULT_HASHING_ALGORITHM = 'sha1'
+DEFAULT_HASHING_ALGORITHM = 'sha256'
#################### Python sandbox ############################################
diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py
index 96832e3cb694..5489f28e0f7e 100644
--- a/lms/djangoapps/certificates/tests/test_webview_views.py
+++ b/lms/djangoapps/certificates/tests/test_webview_views.py
@@ -49,6 +49,7 @@
)
from openedx.core.djangolib.js_utils import js_escaped_string
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
+from openedx.core.lib.courses import course_image_url
from openedx.core.lib.tests.assertions.events import assert_event_matches
from openedx.features.name_affirmation_api.utils import get_name_affirmation_service
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
@@ -351,6 +352,68 @@ def test_linkedin_share_url_site(self):
js_escaped_string(self.linkedin_url.format(params=urlencode(params))),
)
+ @patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", {
+ "CERTIFICATE_FACEBOOK": True,
+ "CERTIFICATE_FACEBOOK_TEXT": "test FB text"
+ })
+ @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
+ def test_render_certificate_html_view_with_facebook_meta_tags(self):
+ """
+ Test view html certificate if share to FB is enabled.
+ If 'facebook_share_enabled=True', tags with property="og:..."
+ must be enabled to pass parameters to FB.
+ """
+ self._add_course_certificates(count=1, signatory_count=1, is_active=True)
+ self.course.cert_html_view_enabled = True
+ self.course.save()
+ self.update_course(self.course, self.user.id)
+ test_url = get_certificate_url(
+ user_id=self.user.id,
+ course_id=str(self.course.id),
+ uuid=self.cert.verify_uuid
+ )
+ platform_name = settings.PLATFORM_NAME
+ share_url = f'http://testserver{test_url}'
+ full_course_image_url = f'http://testserver{course_image_url(self.course)}'
+ document_title = f'{self.course.org} {self.course.number} Certificate | {platform_name}'
+ response = self.client.get(test_url)
+
+ assert response.status_code == 200
+ self.assertContains(response, f'')
+ self.assertContains(response, f'')
+ self.assertContains(response, '')
+ self.assertContains(response, f'')
+ self.assertContains(response, '')
+
+ @patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", {
+ "CERTIFICATE_FACEBOOK": False,
+ })
+ @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
+ def test_render_certificate_html_view_without_facebook_meta_tags(self):
+ """
+ Test view html certificate if share to FB is disabled.
+ If 'facebook_share_enabled=False', html certificate view
+ should not contain tags with parameters property="og:..."
+ """
+ self._add_course_certificates(count=1, signatory_count=1, is_active=True)
+ self.course.cert_html_view_enabled = True
+ self.course.save()
+ self.update_course(self.course, self.user.id)
+
+ test_url = get_certificate_url(
+ user_id=self.user.id,
+ course_id=str(self.course.id),
+ uuid=self.cert.verify_uuid
+ )
+ response = self.client.get(test_url)
+
+ assert response.status_code == 200
+ self.assertNotContains(response, '')
+ self.assertNotContains(response, '${_("Print or share your certificate:")}
% if facebook_share_enabled:
${_("Post on Facebook")}
diff --git a/lms/templates/certificates/accomplishment-base.html b/lms/templates/certificates/accomplishment-base.html
index 158c7698e80a..92c58dc138ff 100644
--- a/lms/templates/certificates/accomplishment-base.html
+++ b/lms/templates/certificates/accomplishment-base.html
@@ -15,7 +15,15 @@
-
+ % if facebook_share_enabled:
+ ## OG (Open Graph) url, title, type, image and description added below to give social media info to display
+ ## (https://developers.facebook.com/docs/opengraph/howtos/maximizing-distribution-media-content#tags)
+
+
+
+
+
+ %endif
${document_title}
<%static:css group='style-certificates'/>
diff --git a/openedx/core/djangoapps/cache_toolbox/middleware.py b/openedx/core/djangoapps/cache_toolbox/middleware.py
index 4ba2162fe35f..9d1ea2bf0690 100644
--- a/openedx/core/djangoapps/cache_toolbox/middleware.py
+++ b/openedx/core/djangoapps/cache_toolbox/middleware.py
@@ -95,6 +95,7 @@
from django.contrib.auth.models import AnonymousUser, User # lint-amnesty, pylint: disable=imported-auth-user
from django.utils.crypto import constant_time_compare
from django.utils.deprecation import MiddlewareMixin
+from edx_django_utils.monitoring import set_custom_attribute
from openedx.core.djangoapps.safe_sessions.middleware import SafeSessionMiddleware, _mark_cookie_for_deletion
@@ -112,6 +113,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def process_request(self, request):
+ set_custom_attribute('DEFAULT_HASHING_ALGORITHM', settings.DEFAULT_HASHING_ALGORITHM)
try:
# Try and construct a User instance from data stored in the cache
session_user_id = SafeSessionMiddleware.get_user_id_from_session(request)
@@ -141,9 +143,29 @@ def _verify_session_auth(self, request):
auto_auth_enabled = settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING', False)
if not auto_auth_enabled and hasattr(request.user, 'get_session_auth_hash'):
session_hash = request.session.get(HASH_SESSION_KEY)
- if not (session_hash and constant_time_compare(session_hash, request.user.get_session_auth_hash())):
- # The session hash has changed due to a password
- # change. Log the user out.
- request.session.flush()
- request.user = AnonymousUser()
- _mark_cookie_for_deletion(request)
+ session_hash_verified = session_hash and constant_time_compare(
+ session_hash, request.user.get_session_auth_hash())
+
+ # session hash is verified from the default algo, so skip legacy check
+ if session_hash_verified:
+ set_custom_attribute('session_hash_verified', "default")
+ return
+
+ if (
+ session_hash and
+ hasattr(request.user, '_legacy_get_session_auth_hash') and
+ constant_time_compare(
+ session_hash,
+ request.user._legacy_get_session_auth_hash() # pylint: disable=protected-access
+ )
+ ):
+ # session hash is verified from legacy hashing algorithm.
+ set_custom_attribute('session_hash_verified', "fallback")
+ return
+
+ # The session hash has changed due to a password
+ # change. Log the user out.
+ request.session.flush()
+ request.user = AnonymousUser()
+ _mark_cookie_for_deletion(request)
+ set_custom_attribute('failed_session_verification', True)
diff --git a/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py b/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py
index 21547b69ca1c..a55090b1207b 100644
--- a/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py
+++ b/openedx/core/djangoapps/cache_toolbox/tests/test_middleware.py
@@ -1,17 +1,18 @@
"""Tests for cached authentication middleware."""
-from unittest.mock import patch
+from unittest.mock import call, patch
+import django
from django.conf import settings
-from django.contrib.auth.models import User, AnonymousUser # lint-amnesty, pylint: disable=imported-auth-user
-from django.urls import reverse
-from django.test import TestCase
from django.contrib.auth import SESSION_KEY
+from django.contrib.auth.models import AnonymousUser, User # lint-amnesty, pylint: disable=imported-auth-user
from django.http import HttpResponse, SimpleCookie
+from django.test import TestCase
+from django.urls import reverse
+from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.cache_toolbox.middleware import CacheBackedAuthenticationMiddleware
from openedx.core.djangoapps.safe_sessions.middleware import SafeCookieData, SafeSessionMiddleware
-from openedx.core.djangolib.testing.utils import skip_unless_cms, skip_unless_lms, get_mock_request
-from common.djangoapps.student.tests.factories import UserFactory
+from openedx.core.djangolib.testing.utils import get_mock_request, skip_unless_cms, skip_unless_lms
class CachedAuthMiddlewareTestCase(TestCase):
@@ -36,9 +37,68 @@ def _test_change_session_hash(self, test_url, redirect_url, target_status_code=2
"""
response = self.client.get(test_url)
assert response.status_code == 200
- with patch.object(User, 'get_session_auth_hash', return_value='abc123'):
- response = self.client.get(test_url)
- self.assertRedirects(response, redirect_url, target_status_code=target_status_code)
+
+ with patch(
+ "openedx.core.djangoapps.cache_toolbox.middleware.set_custom_attribute"
+ ) as mock_set_custom_attribute:
+ with patch.object(User, 'get_session_auth_hash', return_value='abc123', autospec=True):
+ # Django 3.2 has _legacy_get_session_auth_hash, and Django 4 does not
+ # Remove once we reach Django 4
+ if hasattr(User, '_legacy_get_session_auth_hash'):
+ with patch.object(User, '_legacy_get_session_auth_hash', return_value='abc123'):
+ response = self.client.get(test_url)
+ else:
+ response = self.client.get(test_url)
+
+ self.assertRedirects(response, redirect_url, target_status_code=target_status_code)
+ mock_set_custom_attribute.assert_any_call('failed_session_verification', True)
+
+ def _test_custom_attribute_after_changing_hash(self, test_url, mock_set_custom_attribute):
+ """verify that set_custom_attribute is called with expected values"""
+ password = 'test-password'
+
+ # Test DEFAULT_HASHING_ALGORITHM of 'sha1' for both login and client get
+ with self.settings(DEFAULT_HASHING_ALGORITHM='sha1'):
+ self.client.login(username=self.user.username, password=password)
+ self.client.get(test_url)
+ # For Django 3.2, the setting 'sha1' applies and is the "default".
+ # For Django 4, the setting no longer applies, and 'sha256' will be used for both as the "default".
+ mock_set_custom_attribute.assert_has_calls([
+ call('DEFAULT_HASHING_ALGORITHM', 'sha1'),
+ call('session_hash_verified', "default"),
+ ])
+ mock_set_custom_attribute.reset_mock()
+
+ # Test DEFAULT_HASHING_ALGORITHM of 'sha1' for login and switch to 'sha256' for client get.
+ with self.settings(DEFAULT_HASHING_ALGORITHM='sha1'):
+ self.client.login(username=self.user.username, password=password)
+ with self.settings(DEFAULT_HASHING_ALGORITHM='sha256'):
+ self.client.get(test_url)
+ if django.VERSION < (4, 0):
+ # For Django 3.2, the setting 'sha1' applies to login, and uses 'she256' for client get,
+ # and should "fallback" to 'sha1".
+ mock_set_custom_attribute.assert_has_calls([
+ call('DEFAULT_HASHING_ALGORITHM', 'sha256'),
+ call('session_hash_verified', "fallback"),
+ ])
+ else:
+ # For Django 4, the setting no longer applies, and again 'sha256' will be used for both as the "default".
+ mock_set_custom_attribute.assert_has_calls([
+ call('DEFAULT_HASHING_ALGORITHM', 'sha256'),
+ call('session_hash_verified', "default"),
+ ])
+ mock_set_custom_attribute.reset_mock()
+
+ # Test DEFAULT_HASHING_ALGORITHM of 'sha256' for both login and client get
+ with self.settings(DEFAULT_HASHING_ALGORITHM='sha256'):
+ self.client.login(username=self.user.username, password=password)
+ self.client.get(test_url)
+ # For Django 3.2, the setting 'sha256' applies and is the "default".
+ # For Django 4, the setting no longer applies, and 'sha256' will be used for both as the "default".
+ mock_set_custom_attribute.assert_has_calls([
+ call('DEFAULT_HASHING_ALGORITHM', 'sha256'),
+ call('session_hash_verified', "default"),
+ ])
@skip_unless_lms
def test_session_change_lms(self):
@@ -53,6 +113,20 @@ def test_session_change_cms(self):
# Studio login redirects to LMS login
self._test_change_session_hash(home_url, settings.LOGIN_URL + '?next=' + home_url, target_status_code=302)
+ @skip_unless_lms
+ @patch("openedx.core.djangoapps.cache_toolbox.middleware.set_custom_attribute")
+ def test_custom_attribute_after_changing_hash_lms(self, mock_set_custom_attribute):
+ """Test set_custom_attribute is called with expected values in LMS"""
+ test_url = reverse('dashboard')
+ self._test_custom_attribute_after_changing_hash(test_url, mock_set_custom_attribute)
+
+ @skip_unless_cms
+ @patch("openedx.core.djangoapps.cache_toolbox.middleware.set_custom_attribute")
+ def test_custom_attribute_after_changing_hash_cms(self, mock_set_custom_attribute):
+ """Test set_custom_attribute is called with expected values in CMS"""
+ test_url = reverse('home')
+ self._test_custom_attribute_after_changing_hash(test_url, mock_set_custom_attribute)
+
def test_user_logout_on_session_hash_change(self):
"""
Verify that if a user's session auth hash and the request's hash
@@ -75,9 +149,18 @@ def test_user_logout_on_session_hash_change(self):
assert self.client.response.cookies.get(settings.SESSION_COOKIE_NAME).value == session_id
assert self.client.response.cookies.get('edx-jwt-cookie-header-payload').value == 'test-jwt-payload'
- with patch.object(User, 'get_session_auth_hash', return_value='abc123'):
- CacheBackedAuthenticationMiddleware().process_request(self.request)
- SafeSessionMiddleware().process_response(self.request, self.client.response)
+ with patch.object(User, 'get_session_auth_hash', return_value='abc123', autospec=True):
+ # Django 3.2 has _legacy_get_session_auth_hash, and Django 4 does not
+ # Remove once we reach Django 4
+ if hasattr(User, '_legacy_get_session_auth_hash'):
+ with patch.object(User, '_legacy_get_session_auth_hash', return_value='abc123'):
+ CacheBackedAuthenticationMiddleware(get_response=lambda request: None).process_request(self.request)
+
+ else:
+ CacheBackedAuthenticationMiddleware(get_response=lambda request: None).process_request(self.request)
+ SafeSessionMiddleware(get_response=lambda request: None).process_response(
+ self.request, self.client.response
+ )
# asserts that user, session, and JWT cookies do not exist
assert self.request.session.get(SESSION_KEY) is None
diff --git a/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py b/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py
index f23773b842a7..babc3160f81e 100644
--- a/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py
+++ b/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py
@@ -223,6 +223,16 @@ def test_update_cookie_data_at_step_3(self):
assert safe_cookie_data.session_id == 'some_session_id'
assert safe_cookie_data.verify(self.user.id)
+ def test_update_cookie_data_at_step_3_with_sha256(self):
+ """ first encode cookie with default algo sha1 and then check with sha256"""
+ self.assert_response(set_request_user=True, set_session_cookie=True)
+ serialized_cookie_data = self.client.response.cookies[settings.SESSION_COOKIE_NAME].value
+ safe_cookie_data = SafeCookieData.parse(serialized_cookie_data)
+ assert safe_cookie_data.version == SafeCookieData.CURRENT_VERSION
+ assert safe_cookie_data.session_id == 'some_session_id'
+ with self.settings(DEFAULT_HASHING_ALGORITHM='sha256'):
+ assert safe_cookie_data.verify(self.user.id)
+
def test_cant_update_cookie_at_step_3_error(self):
self.client.response.cookies[settings.SESSION_COOKIE_NAME] = None
with self.assert_invalid_session_id():
diff --git a/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py b/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py
index b8a23c567d24..bbad0e85851d 100644
--- a/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py
+++ b/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py
@@ -234,5 +234,5 @@ def test_pinned_values(self):
"|HvGnjXf1b3jU"
"|ImExZWZiNzVlZGFmM2FkZWZmYjM4YjI0ZmZkOWU4MzExODU0MTk4NmVlNGRiYzBlODdhYWUzOGM5MzVlNzk4NjUi"
":1m6Hve"
- ":OMhY2FL2pudJjSSXChtI-zR8QVA"
+ ":Pra4iochviPvKUoIV33gdVZFDgG-cMDlIYfl8iFIMaY"
)