Skip to content
Merged
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
27 changes: 13 additions & 14 deletions openedx/core/djangoapps/user_authn/views/login_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,16 @@ def login_and_registration_form(request, initial_mode="login"):
# Retrieve the form descriptions from the user API
form_descriptions = _get_form_descriptions(request)

# Detect a running TPA pipeline early so it can guard against redirect loops below.
saml_provider = False
running_pipeline = None
if third_party_auth.is_enabled():
running_pipeline = pipeline.get(request)
if running_pipeline:
saml_provider, __ = third_party_auth.utils.is_saml_provider(
running_pipeline.get('backend'), running_pipeline.get('kwargs')
)

# Our ?next= URL may itself contain a parameter 'tpa_hint=x' that we need to check.
# If present, we display a login page focused on third-party auth with that provider.
third_party_auth_hint = None
Expand All @@ -172,9 +182,10 @@ def login_and_registration_form(request, initial_mode="login"):
provider_id = next_args['tpa_hint'][0]
tpa_hint_provider = third_party_auth.provider.Registry.get(provider_id=provider_id)
if tpa_hint_provider:
if tpa_hint_provider.skip_hinted_login_dialog:
if tpa_hint_provider.skip_hinted_login_dialog and not running_pipeline:
# Forward the user directly to the provider's login URL when the provider is configured
# to skip the dialog.
# to skip the dialog. Do not redirect if a TPA pipeline is already running, as that
# would cause an infinite loop (e.g. new SAML users dispatched back to /login).
if initial_mode == "register":
auth_entry = pipeline.AUTH_ENTRY_REGISTER
else:
Expand All @@ -194,18 +205,6 @@ def login_and_registration_form(request, initial_mode="login"):
# tpa_hint_provider is not available
# AND
# user is not coming from a SAML IDP.
saml_provider = False
running_pipeline = pipeline.get(request)
if running_pipeline:
backend_name = running_pipeline.get('backend')
if backend_name == 'tpa-saml':
# Directly detect SAML backend to avoid registry lookup failures
# (e.g. when pipeline kwargs lack response['idp_name'] at this point).
saml_provider = True
else:
saml_provider, __ = third_party_auth.utils.is_saml_provider(
backend_name, running_pipeline.get('kwargs')
)

enterprise_customer = enterprise_customer_for_request(request)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,28 @@ def test_hinted_login_dialog_disabled(self, url_name, auth_entry):
target_status_code=302
)

@ddt.data(
('signin_user', 'login'),
('register_user', 'register'),
)
@ddt.unpack
def test_hinted_login_dialog_disabled_with_running_pipeline(self, url_name, auth_entry): # pylint: disable=unused-argument
"""
Test that skip_hinted_login_dialog does NOT redirect to the provider when a TPA
pipeline is already running. Without this guard, new SAML users dispatched back
to /login or /register by ensure_user_information would be sent straight back to
the IdP, creating an infinite redirect loop that eventually results in a 429 from
the IdP (e.g. Auth0).
"""
self.google_provider.skip_hinted_login_dialog = True
self.google_provider.save()
params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")]
pipeline_target = "openedx.core.djangoapps.user_authn.views.login_form.third_party_auth.pipeline"
with simulate_running_pipeline(pipeline_target, "tpa-saml"):
response = self.client.get(reverse(url_name), params, HTTP_ACCEPT="text/html")
# The form should be rendered (200), not a redirect to the provider's auth URL.
self.assertEqual(response.status_code, 200)

@override_settings(FEATURES=dict(settings.FEATURES, THIRD_PARTY_AUTH_HINT='oa2-google-oauth2'))
@ddt.data(
'signin_user',
Expand Down
Loading