From c2a86534e6685a62e769f924012d5df362fc2af6 Mon Sep 17 00:00:00 2001 From: Ahtisham Shahid Date: Mon, 4 Aug 2025 02:27:12 +0500 Subject: [PATCH] fix: updated captcha api to use enterprise assessment (#37079) * fix: updated captcha api to use enterprise assessment --- lms/djangoapps/discussion/config/settings.py | 2 +- lms/djangoapps/discussion/rest_api/api.py | 4 +- .../discussion/rest_api/tests/test_api.py | 2 +- .../discussion/rest_api/tests/test_views.py | 2 +- lms/djangoapps/discussion/rest_api/utils.py | 73 ++++++++++++++----- lms/envs/common.py | 32 ++++++-- 6 files changed, 82 insertions(+), 33 deletions(-) diff --git a/lms/djangoapps/discussion/config/settings.py b/lms/djangoapps/discussion/config/settings.py index 06040ad82e..e9dbfebeea 100644 --- a/lms/djangoapps/discussion/config/settings.py +++ b/lms/djangoapps/discussion/config/settings.py @@ -25,7 +25,7 @@ def is_forum_daily_digest_enabled(): # .. toggle_name: discussion.enable_captcha # .. toggle_implementation: CourseWaffleFlag # .. toggle_default: False -# .. toggle_description: Waffle flag to enable account level preferences for notifications +# .. toggle_description: When the flag is ON, users will be able to see captcha for discussion # .. toggle_use_cases: temporary, open_edx # .. toggle_creation_date: 2025-07-12 # .. toggle_target_removal_date: 2025-07-29 diff --git a/lms/djangoapps/discussion/rest_api/api.py b/lms/djangoapps/discussion/rest_api/api.py index 3b093b4117..d3eaab4118 100644 --- a/lms/djangoapps/discussion/rest_api/api.py +++ b/lms/djangoapps/discussion/rest_api/api.py @@ -130,7 +130,7 @@ from .utils import ( get_usernames_from_search_string, set_attribute, is_posting_allowed, - can_user_notify_all_learners, is_captcha_enabled + can_user_notify_all_learners, is_captcha_enabled, get_captcha_site_key_by_platform ) User = get_user_model() @@ -383,7 +383,7 @@ def get_course(request, course_key, check_tab=True): ), 'captcha_settings': { 'enabled': is_captcha_enabled(course_key), - 'site_key': settings.RECAPTCHA_SITE_KEY, + 'site_key': get_captcha_site_key_by_platform('web'), }, "is_email_verified": request.user.is_active, "only_verified_users_can_post": ONLY_VERIFIED_USERS_CAN_POST.is_enabled(course_key), diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api.py b/lms/djangoapps/discussion/rest_api/tests/test_api.py index 6b6fdb6ced..4542df116e 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api.py @@ -218,7 +218,7 @@ class GetCourseTest(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase) 'is_notify_all_learners_enabled': False, 'captcha_settings': { 'enabled': False, - 'site_key': '', + 'site_key': None, }, "is_email_verified": True, "only_verified_users_can_post": False, diff --git a/lms/djangoapps/discussion/rest_api/tests/test_views.py b/lms/djangoapps/discussion/rest_api/tests/test_views.py index 96ee730f9b..4bb5088136 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_views.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_views.py @@ -570,7 +570,7 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): 'is_notify_all_learners_enabled': False, 'captcha_settings': { 'enabled': False, - 'site_key': '', + 'site_key': None, }, "is_email_verified": True, "only_verified_users_can_post": False, diff --git a/lms/djangoapps/discussion/rest_api/utils.py b/lms/djangoapps/discussion/rest_api/utils.py index 22e028b489..0f02a0dcdc 100644 --- a/lms/djangoapps/discussion/rest_api/utils.py +++ b/lms/djangoapps/discussion/rest_api/utils.py @@ -6,6 +6,7 @@ from datetime import datetime from typing import Dict, List import requests +from crum import get_current_request from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core.paginator import Paginator @@ -413,26 +414,6 @@ def can_user_notify_all_learners(user_roles, is_course_staff, is_course_admin): return is_staff_or_instructor -def verify_recaptcha_token(token): - """ - Helper function to verify reCAPTCHA token - """ - verify_url = settings.RECAPTCHA_VERIFY_URL - verify_data = { - 'secret': settings.RECAPTCHA_PRIVATE_KEY, - 'response': token, - } - - try: - response = requests.post(verify_url, data=verify_data, timeout=10) - result = response.json() - log.info("reCAPTCHA verification result: %s", result) - return result.get('success', False) - except Exception as e: # pylint: disable=broad-except - log.error("Error verifying reCAPTCHA token: %s", e) - return False - - def is_captcha_enabled(course_id) -> bool: """ Check if reCAPTCHA is enabled for discussion posts in the given course. @@ -463,3 +444,55 @@ def is_only_student(course_key, user) -> bool: is_user_admin = user.is_staff user_roles = get_user_role_names(user, course_key) return user_roles == {FORUM_ROLE_STUDENT} and not (is_course_staff_or_admin or is_user_admin) + + +def verify_recaptcha_token(token: str) -> bool: + """ + Assess the reCAPTCHA token using Google reCAPTCHA Enterprise API. + Logs success or error and returns True if an error occurs, along with logging the error. + """ + try: + site_key = get_captcha_site_key_by_platform(get_platform_from_request()) + url = (f"https://recaptchaenterprise.googleapis.com/v1/projects/{settings.RECAPTCHA_PROJECT_ID}/assessments" + f"?key={settings.RECAPTCHA_PRIVATE_KEY}") + data = { + "event": { + "token": token, + "siteKey": site_key, + } + } + + response = requests.post(url, json=data, timeout=10).json() + + if response.get('tokenProperties', {}).get('valid'): + logging.info("reCAPTCHA token assessment successful. Token is valid.") + return True + elif response.get('error'): + logging.error(f"reCAPTCHA token assessment failed: {response['error']}.") + return True + else: + logging.error(f"reCAPTCHA token assessment failed: Invalid token.{response}.") + return False + except requests.exceptions.RequestException as e: + logging.error(f"Network or API error during reCAPTCHA assessment: {e}") + return True + except KeyError as e: + logging.error(f"Unexpected response format from reCAPTCHA API. Missing key: {e}. Full response: {response}") + return True + except Exception as e: # lint-amnesty, pylint: disable=broad-except + logging.error(f"An unexpected error occurred during reCAPTCHA assessment: {e}", exc_info=True) + return True + + +def get_platform_from_request(): + """ + get Mobile-Platform-Identifier header value from request + """ + return get_current_request().headers.get('Mobile-Platform-Identifier', 'web') + + +def get_captcha_site_key_by_platform(platform: str) -> str | None: + """ + Get reCAPTCHA site key based on the platform. + """ + return settings.RECAPTCHA_SITE_KEYS.get(platform, None) diff --git a/lms/envs/common.py b/lms/envs/common.py index d635ac23e4..491f04acdf 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5001,12 +5001,28 @@ LMS_COMM_DEFAULT_FROM_EMAIL = "no-reply@example.com" # .. setting_description: Add recaptcha private key to use captcha feature in discussion app. RECAPTCHA_PRIVATE_KEY = "" -# .. setting_name: RECAPTCHA_VERIFY_URL -# .. setting_default: empty string -# .. setting_description: Add recaptcha verification api url to verify capthca tokens. -RECAPTCHA_VERIFY_URL = "" +# .. setting_name: RECAPTCHA_SITE_KEYS +# .. setting_default: empty dictionary +# .. setting_description: Add recaptcha site keys to use captcha feature in discussion app. +# .. setting_warning: This setting is used to configure the reCAPTCHA keys for web, +# iOS, and Android platforms. +# The keys are expected to be in the format: +# { +# 'web': 'your-web-site-key', +# 'ios': 'your-ios-site-key', +# 'android': 'your-android-site-key', +# } +RECAPTCHA_SITE_KEYS = { + 'web': None, + 'ios': None, + 'android': None, +} -# .. setting_name: RECAPTCHA_SITE_KEY -# .. setting_default: empty string -# .. setting_description: Add recaptcha site key to use captcha feature in discussion MFE. -RECAPTCHA_SITE_KEY = "" +# .. setting_name: RECAPTCHA_PROJECT_ID +# .. setting_default: None +# .. setting_description: Add recaptcha project id to use captcha feature in discussion app. +# .. setting_warning: This setting is used to configure the reCAPTCHA project ID for the discussion app. +# The project ID is used to identify the reCAPTCHA project in the Google Cloud Console +# and is required for the reCAPTCHA service to function correctly. +# The project ID should be obtained from the Google Cloud Console when creating a reCAPTCHA +RECAPTCHA_PROJECT_ID = None