Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions common/djangoapps/third_party_auth/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,11 @@ def ready(self):
# Import signal handlers to register them
from .signals import handlers # noqa: F401 pylint: disable=unused-import

# To override the settings before loading social_django.
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
self._enable_third_party_auth()

def _enable_third_party_auth(self):
"""
Enable the use of third_party_auth, which allows users to sign in to edX
using other identity providers. For configuration details, see
common/djangoapps/third_party_auth/settings.py.
"""
from common.djangoapps.third_party_auth import settings as auth_settings
auth_settings.apply_settings(settings)
# Note: Third-party auth settings are now defined statically in lms/envs/common.py
# However, the enterprise pipeline step must be inserted dynamically because
# it requires checking if enterprise is enabled, which can't be done at
# settings load time.
# Only insert enterprise elements if SOCIAL_AUTH_PIPELINE exists (LMS only, not CMS).
if hasattr(settings, 'SOCIAL_AUTH_PIPELINE'):
from openedx.features.enterprise_support.api import insert_enterprise_pipeline_elements
insert_enterprise_pipeline_elements(settings.SOCIAL_AUTH_PIPELINE)
107 changes: 0 additions & 107 deletions common/djangoapps/third_party_auth/settings.py

This file was deleted.

163 changes: 100 additions & 63 deletions common/djangoapps/third_party_auth/tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,104 @@
"""Unit tests for settings.py."""
"""Unit tests for third-party auth settings in lms/envs/common.py."""

from unittest.mock import patch
from common.djangoapps.third_party_auth import provider, settings
from common.djangoapps.third_party_auth.tests import testutil
from django.conf import settings
from django.test import TestCase, override_settings

from common.djangoapps.third_party_auth import provider
from common.djangoapps.third_party_auth.tests.utils import skip_unless_thirdpartyauth
_ORIGINAL_AUTHENTICATION_BACKENDS = ['first_authentication_backend']
_ORIGINAL_INSTALLED_APPS = ['first_installed_app']
_ORIGINAL_MIDDLEWARE_CLASSES = ['first_middleware_class']
_ORIGINAL_TEMPLATE_CONTEXT_PROCESSORS = ['first_template_context_preprocessor']
_SETTINGS_MAP = {
'AUTHENTICATION_BACKENDS': _ORIGINAL_AUTHENTICATION_BACKENDS,
'INSTALLED_APPS': _ORIGINAL_INSTALLED_APPS,
'MIDDLEWARE': _ORIGINAL_MIDDLEWARE_CLASSES,
'TEMPLATES': [{
'OPTIONS': {
'context_processors': _ORIGINAL_TEMPLATE_CONTEXT_PROCESSORS
}
}],
'FEATURES': {},
}
_SETTINGS_MAP['DEFAULT_TEMPLATE_ENGINE'] = _SETTINGS_MAP['TEMPLATES'][0]


class SettingsUnitTest(testutil.TestCase):
"""Unit tests for settings management code."""

# Suppress spurious no-member warning on fakes.
# pylint: disable=no-member

def setUp(self):
super().setUp()
self.settings = testutil.FakeDjangoSettings(_SETTINGS_MAP)

def test_apply_settings_adds_exception_middleware(self):
settings.apply_settings(self.settings)
assert 'common.djangoapps.third_party_auth.middleware.ExceptionMiddleware' in self.settings.MIDDLEWARE

def test_apply_settings_adds_fields_stored_in_session(self):
settings.apply_settings(self.settings)
assert ['auth_entry', 'next'] == self.settings.FIELDS_STORED_IN_SESSION
from openedx.core.djangolib.testing.utils import skip_unless_lms


@skip_unless_lms
class SettingsUnitTest(TestCase):
"""Unit tests for third-party auth settings defined in lms/envs/common.py."""

def test_exception_middleware_in_middleware_list(self):
"""Verify ExceptionMiddleware is included in MIDDLEWARE."""
assert 'common.djangoapps.third_party_auth.middleware.ExceptionMiddleware' in settings.MIDDLEWARE

def test_fields_stored_in_session_defined(self):
"""Verify FIELDS_STORED_IN_SESSION is defined with expected values."""
assert settings.FIELDS_STORED_IN_SESSION == ['auth_entry', 'next']

@skip_unless_thirdpartyauth()
def test_apply_settings_enables_no_providers_by_default(self):
# Providers are only enabled via ConfigurationModels in the database
settings.apply_settings(self.settings)
assert [] == provider.Registry.enabled()

def test_apply_settings_turns_off_raising_social_exceptions(self):
# Guard against submitting a conf change that's convenient in dev but
# bad in prod.
settings.apply_settings(self.settings)
assert not self.settings.SOCIAL_AUTH_RAISE_EXCEPTIONS

def test_apply_settings_turns_off_redirect_sanitization(self):
settings.apply_settings(self.settings)
assert not self.settings.SOCIAL_AUTH_SANITIZE_REDIRECTS

def test_apply_settings_avoids_default_username_check(self):
# Avoid the default username check where non-ascii characters are not
# allowed when unicode username is enabled
settings.apply_settings(self.settings)
assert self.settings.SOCIAL_AUTH_CLEAN_USERNAMES
# verify default behavior
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_UNICODE_USERNAME': True}):
settings.apply_settings(self.settings)
assert not self.settings.SOCIAL_AUTH_CLEAN_USERNAMES
def test_no_providers_enabled_by_default(self):
"""Providers are only enabled via ConfigurationModels in the database."""
assert provider.Registry.enabled() == []

def test_social_auth_raise_exceptions_is_false(self):
"""Guard against submitting a conf change that's convenient in dev but bad in prod."""
assert settings.SOCIAL_AUTH_RAISE_EXCEPTIONS is False

def test_social_auth_sanitize_redirects_is_false(self):
"""Verify redirect sanitization is disabled (platform does its own)."""
assert settings.SOCIAL_AUTH_SANITIZE_REDIRECTS is False

def test_social_auth_login_error_url(self):
"""Verify SOCIAL_AUTH_LOGIN_ERROR_URL is set."""
assert settings.SOCIAL_AUTH_LOGIN_ERROR_URL == '/'

def test_social_auth_login_redirect_url(self):
"""Verify SOCIAL_AUTH_LOGIN_REDIRECT_URL is set."""
assert settings.SOCIAL_AUTH_LOGIN_REDIRECT_URL == '/dashboard'

def test_social_auth_strategy(self):
"""Verify SOCIAL_AUTH_STRATEGY is set to use ConfigurationModelStrategy."""
assert settings.SOCIAL_AUTH_STRATEGY == 'common.djangoapps.third_party_auth.strategy.ConfigurationModelStrategy'

def test_social_auth_pipeline_defined(self):
"""Verify SOCIAL_AUTH_PIPELINE is defined and includes expected steps."""
pipeline = settings.SOCIAL_AUTH_PIPELINE
assert isinstance(pipeline, list)
assert len(pipeline) > 0
# Verify some key pipeline steps are present
assert 'common.djangoapps.third_party_auth.pipeline.parse_query_params' in pipeline
assert 'social_core.pipeline.user.create_user' in pipeline
assert 'common.djangoapps.third_party_auth.pipeline.ensure_redirect_url_is_safe' in pipeline

def test_social_auth_context_processors(self):
"""Verify social_django context processors are included."""
# CONTEXT_PROCESSORS is used to build TEMPLATES, so check there
context_processors = settings.TEMPLATES[0]['OPTIONS']['context_processors']
assert 'social_django.context_processors.backends' in context_processors
assert 'social_django.context_processors.login_redirect' in context_processors

@override_settings(FEATURES={'ENABLE_UNICODE_USERNAME': False})
def test_social_auth_clean_usernames_default(self):
"""Verify SOCIAL_AUTH_CLEAN_USERNAMES is True when unicode usernames disabled."""
# Note: SOCIAL_AUTH_CLEAN_USERNAMES is a Derived setting, computed at settings load time.
# This test verifies the default behavior (unicode usernames disabled).
assert settings.SOCIAL_AUTH_CLEAN_USERNAMES is True

def test_social_auth_clean_usernames_computation(self):
"""
Verify the SOCIAL_AUTH_CLEAN_USERNAMES computation logic.

SOCIAL_AUTH_CLEAN_USERNAMES is a Derived setting that is computed at settings load time,
so we can't use @override_settings to test both cases. Instead, we test the computation
logic directly to ensure it correctly inverts the ENABLE_UNICODE_USERNAME feature flag.
"""
# The logic in lms/envs/common.py is:
# SOCIAL_AUTH_CLEAN_USERNAMES = Derived(
# lambda settings: not settings.FEATURES.get('ENABLE_UNICODE_USERNAME', False)
# )
# We replicate and test that logic here.

class FakeSettings:
"""Fake settings object for testing the Derived computation."""
def __init__(self, features):
self.FEATURES = features

# When ENABLE_UNICODE_USERNAME is False (default), SOCIAL_AUTH_CLEAN_USERNAMES should be True
fake_settings = FakeSettings({'ENABLE_UNICODE_USERNAME': False})
result = not fake_settings.FEATURES.get('ENABLE_UNICODE_USERNAME', False)
assert result is True

# When ENABLE_UNICODE_USERNAME is True, SOCIAL_AUTH_CLEAN_USERNAMES should be False
fake_settings = FakeSettings({'ENABLE_UNICODE_USERNAME': True})
result = not fake_settings.FEATURES.get('ENABLE_UNICODE_USERNAME', False)
assert result is False

# When ENABLE_UNICODE_USERNAME is not set, should default to False, so result is True
fake_settings = FakeSettings({})
result = not fake_settings.FEATURES.get('ENABLE_UNICODE_USERNAME', False)
assert result is True
70 changes: 70 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,69 @@
# .. toggle_creation_date: 2014-09-15
ENABLE_THIRD_PARTY_AUTH = False

# Third-party auth settings for python-social-auth
# These are defined unconditionally; they only take effect when
# AUTHENTICATION_BACKENDS includes social auth backends.

# Where to send the user if there's an error during social authentication
SOCIAL_AUTH_LOGIN_ERROR_URL = '/'
# Where to send the user once social authentication is successful
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/dashboard'
# Disable sanitizing of redirect urls in social-auth since the platform
# already does its own sanitization via the LOGIN_REDIRECT_WHITELIST setting.
SOCIAL_AUTH_SANITIZE_REDIRECTS = False
# Adding extra key value pair in the url query string for microsoft as per request
SOCIAL_AUTH_AZUREAD_OAUTH2_AUTH_EXTRA_ARGUMENTS = {'msafed': 0}
# Required so that we can use unmodified PSA OAuth2 backends:
SOCIAL_AUTH_STRATEGY = 'common.djangoapps.third_party_auth.strategy.ConfigurationModelStrategy'
# We let the user specify their email address during signup.
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['email']
# Disable exceptions by default for prod so you get redirect behavior
# instead of a Django error page.
SOCIAL_AUTH_RAISE_EXCEPTIONS = False
# Clean username to make sure username is compatible with our system requirements
SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'common.djangoapps.third_party_auth.models.clean_username'
# Allow users to login using social auth even if their account is not verified yet
SOCIAL_AUTH_INACTIVE_USER_LOGIN = True
SOCIAL_AUTH_INACTIVE_USER_URL = '/auth/inactive'
SOCIAL_AUTH_UUID_LENGTH = 10
# Whitelisted URL query parameters retained in the pipeline session.
FIELDS_STORED_IN_SESSION = ['auth_entry', 'next']

# Computed setting: disable clean usernames check when unicode usernames are enabled
SOCIAL_AUTH_CLEAN_USERNAMES = Derived(
lambda settings: not settings.FEATURES.get('ENABLE_UNICODE_USERNAME', False)
)

# Social auth pipeline for third-party authentication.
# Operators can override SOCIAL_AUTH_PIPELINE directly in their settings
# to customize the pipeline.
# Note: The enterprise step (handle_enterprise_logistration) is inserted dynamically
# during app initialization by third_party_auth's AppConfig.ready() if enterprise
# is enabled. It cannot be included statically because it requires runtime checks.
SOCIAL_AUTH_PIPELINE = [
'common.djangoapps.third_party_auth.pipeline.parse_query_params',
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'common.djangoapps.third_party_auth.pipeline.associate_by_email_if_login_api',
'common.djangoapps.third_party_auth.pipeline.associate_by_email_if_saml',
'common.djangoapps.third_party_auth.pipeline.associate_by_email_if_oauth',
'common.djangoapps.third_party_auth.pipeline.get_username',
'common.djangoapps.third_party_auth.pipeline.set_pipeline_timeout',
'common.djangoapps.third_party_auth.pipeline.ensure_user_information',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'common.djangoapps.third_party_auth.pipeline.user_details_force_sync',
'common.djangoapps.third_party_auth.pipeline.set_id_verification_status',
'common.djangoapps.third_party_auth.pipeline.set_logged_in_cookies',
'common.djangoapps.third_party_auth.pipeline.login_analytics',
'common.djangoapps.third_party_auth.pipeline.ensure_redirect_url_is_safe',
]

# Prevent concurrent logins per user
PREVENT_CONCURRENT_LOGINS = True

Expand Down Expand Up @@ -799,6 +862,10 @@

# Context processor necessary for the survey report message appear on the admin site
'openedx.features.survey_report.context_processors.admin_extra_context',

# Third-party auth context processors for social_django
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
]

DEFAULT_TEMPLATE_ENGINE_DIRS = Derived(lambda settings: settings.TEMPLATES[0]['DIRS'][:])
Expand Down Expand Up @@ -1211,6 +1278,9 @@
# Handles automatically storing user ids in django-simple-history tables when possible.
'simple_history.middleware.HistoryRequestMiddleware',

# Third-party auth exception handling for social auth redirects
'common.djangoapps.third_party_auth.middleware.ExceptionMiddleware',

# This must be last
'openedx.core.djangoapps.site_configuration.middleware.SessionCookieDomainOverrideMiddleware',
]
Expand Down
Loading