diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f4cb08..28c36b3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,12 @@ Change Log Unreleased ---------- +Fixed +~~~~~ + +* Session cleanup in ``EdXOAuth2.start()`` no longer discards the ``next`` redirect that social-auth stores before the OAuth flow begins. Users who were already logged in were sent to ``LOGIN_REDIRECT_URL`` instead of the page they asked for. +* The same cleanup also keeps any fields listed in ``FIELDS_STORED_IN_SESSION``, which social-auth stores at the same point. + [4.6.2] - 2025-10-16 -------------------- diff --git a/src/auth_backends/backends.py b/src/auth_backends/backends.py index c2d7675..01c1430 100644 --- a/src/auth_backends/backends.py +++ b/src/auth_backends/backends.py @@ -98,7 +98,14 @@ def start(self): existing_username ) + # logout() flushes the session, which also holds what social-auth just stored for this request: + # the sanitized redirect and any FIELDS_STORED_IN_SESSION. Carry only those across. + stored_names = ['next', *self.setting('FIELDS_STORED_IN_SESSION', [])] + stored_values = {name: self.strategy.session_get(name) for name in stored_names} logout(request) + for name, value in stored_values.items(): + if value is not None: + self.strategy.session_set(name, value) return super().start() diff --git a/src/auth_backends/tests/test_backends.py b/src/auth_backends/tests/test_backends.py index e73237f..508bf36 100644 --- a/src/auth_backends/tests/test_backends.py +++ b/src/auth_backends/tests/test_backends.py @@ -159,6 +159,29 @@ def test_start_with_session_cleanup(self, user_authenticated, mock_logger, mock_ else: mock_logger.info.assert_not_called() + @pytest.mark.django_db + @patch('auth_backends.backends.set_custom_attribute') + @patch('auth_backends.backends.logger') + def test_start_with_session_cleanup_preserves_next(self, _mock_logger, _mock_set_attr): + """The post-login redirect stored by social-auth must survive the session cleanup.""" + request = RequestFactory().get('/auth/login/edx-oauth2/?next=/credentials/abc/') + request.user = User.objects.create_user(username='existing_user', email='existing@example.com') + middleware = SessionMiddleware(lambda req: None) + middleware.process_request(request) + request.session.save() + self.backend.strategy.request = request + # In production the strategy's session IS request.session; the fake test strategy keeps its own dict. + self.backend.strategy._session = request.session # pylint: disable=protected-access + self.set_social_auth_setting('FIELDS_STORED_IN_SESSION', ['auth_entry']) + self.backend.strategy.session_set('next', '/credentials/abc/') + self.backend.strategy.session_set('auth_entry', 'login') + + self.do_start() + + self.assertTrue(request.user.is_anonymous) + self.assertEqual(self.backend.strategy.session_get('next'), '/credentials/abc/') + self.assertEqual(self.backend.strategy.session_get('auth_entry'), 'login') + def test_partial_pipeline(self): self.do_partial_pipeline()