Fix: CORS issues in third-party auth disconnect by adding JSON endpoint (#37100)
Add a json auth endpoint where previously there was only an HTML redirect version. This will make it easier to work with MFEs. --------- Co-authored-by: Feanil Patel <feanil@axim.org>
This commit is contained in:
committed by
GitHub
parent
c35d3267b6
commit
254dd2f689
@@ -419,7 +419,7 @@ class TestThirdPartyAuthUserStatusView(ThirdPartyAuthTestMixin, APITestCase):
|
||||
assert (response.data ==
|
||||
[{
|
||||
'accepts_logins': True, 'name': 'Google',
|
||||
'disconnect_url': '/auth/disconnect/google-oauth2/?',
|
||||
'disconnect_url': '/auth/disconnect_json/google-oauth2/?',
|
||||
'connect_url': f'/auth/login/google-oauth2/?auth_entry=account_settings&next={next_url}',
|
||||
'connected': False, 'id': 'oa2-google-oauth2'
|
||||
}])
|
||||
|
||||
@@ -379,10 +379,13 @@ def get_disconnect_url(provider_id, association_id):
|
||||
ValueError: if no provider is enabled with the given ID.
|
||||
"""
|
||||
backend_name = _get_enabled_provider(provider_id).backend_name
|
||||
# Use custom JSON disconnect endpoint to avoid CORS issues
|
||||
if association_id:
|
||||
return _get_url('social:disconnect_individual', backend_name, url_params={'association_id': association_id})
|
||||
return _get_url(
|
||||
'custom_disconnect_json_individual', backend_name, url_params={'association_id': association_id}
|
||||
)
|
||||
else:
|
||||
return _get_url('social:disconnect', backend_name)
|
||||
return _get_url('custom_disconnect_json', backend_name)
|
||||
|
||||
|
||||
def get_login_url(provider_id, auth_entry, redirect_url=None):
|
||||
|
||||
@@ -176,7 +176,7 @@ class UrlFormationTestCase(TestCase):
|
||||
def test_disconnect_url_returns_expected_format(self):
|
||||
disconnect_url = pipeline.get_disconnect_url(self.enabled_provider.provider_id, 1000)
|
||||
disconnect_url = disconnect_url.rstrip('?')
|
||||
assert disconnect_url == '/auth/disconnect/{backend}/{association_id}/'\
|
||||
assert disconnect_url == '/auth/disconnect_json/{backend}/{association_id}/'\
|
||||
.format(backend=self.enabled_provider.backend_name, association_id=1000)
|
||||
|
||||
def test_login_url_raises_value_error_if_provider_not_enabled(self):
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.urls import path, re_path
|
||||
|
||||
from .views import (
|
||||
IdPRedirectView,
|
||||
disconnect_json_view,
|
||||
inactive_user_view,
|
||||
lti_login_and_complete_view,
|
||||
post_to_custom_auth_form,
|
||||
@@ -17,6 +18,13 @@ urlpatterns = [
|
||||
re_path(r'^auth/saml/metadata.xml', saml_metadata_view),
|
||||
re_path(r'^auth/login/(?P<backend>lti)/$', lti_login_and_complete_view),
|
||||
path('auth/idp_redirect/<slug:provider_slug>', IdPRedirectView.as_view(), name="idp_redirect"),
|
||||
# Custom JSON disconnect endpoint to avoid CORS issues
|
||||
re_path(r'^auth/disconnect_json/(?P<backend>[^/]+)/$', disconnect_json_view, name='custom_disconnect_json'),
|
||||
re_path(
|
||||
r'^auth/disconnect_json/(?P<backend>[^/]+)/(?P<association_id>\d+)/$',
|
||||
disconnect_json_view,
|
||||
name='custom_disconnect_json_individual'
|
||||
),
|
||||
path('auth/', include('social_django.urls', namespace='social')),
|
||||
path('auth/saml/v0/', include('common.djangoapps.third_party_auth.samlproviderconfig.urls')),
|
||||
path('auth/saml/v0/', include('common.djangoapps.third_party_auth.samlproviderdata.urls')),
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
Extra views required for SSO
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseServerError
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import ValidationError, PermissionDenied
|
||||
from django.db import DatabaseError
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseServerError, JsonResponse
|
||||
)
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic.base import View
|
||||
from edx_django_utils.monitoring import record_exception
|
||||
from social_core.utils import setting_name
|
||||
from social_django.models import UserSocialAuth
|
||||
from social_django.utils import load_backend, load_strategy, psa
|
||||
from social_django.views import complete
|
||||
|
||||
@@ -23,6 +32,8 @@ from .models import SAMLConfiguration, SAMLProviderConfig
|
||||
|
||||
URL_NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'social'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def inactive_user_view(request):
|
||||
"""
|
||||
@@ -160,3 +171,100 @@ class IdPRedirectView(View):
|
||||
return redirect(url)
|
||||
except ValueError:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def disconnect_json_view(request, backend, association_id=None):
|
||||
"""
|
||||
Custom disconnect view that returns JSON response instead of redirecting.
|
||||
See https://github.com/python-social-auth/social-app-django/issues/774 for why this is needed.
|
||||
"""
|
||||
user = request.user
|
||||
# Check URL parameter first, then POST parameter
|
||||
if not association_id:
|
||||
association_id = request.POST.get('association_id')
|
||||
try:
|
||||
# Load the backend strategy and backend instance
|
||||
strategy = load_strategy(request)
|
||||
backend_instance = load_backend(strategy, backend, redirect_uri=request.build_absolute_uri())
|
||||
# Use backend.disconnect method - simplified approach without partial pipeline
|
||||
response = backend_instance.disconnect(user=user, association_id=association_id)
|
||||
# Always return JSON response regardless of what backend.disconnect returns
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'Account successfully disconnected',
|
||||
'backend': backend,
|
||||
'association_id': association_id
|
||||
})
|
||||
except UserSocialAuth.DoesNotExist:
|
||||
log.warning(
|
||||
'Social auth association not found during disconnect: backend=%s, association_id=%s, user_id=%s',
|
||||
backend, association_id, user.id
|
||||
)
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Account not found or already disconnected',
|
||||
'backend': backend,
|
||||
'association_id': association_id
|
||||
}, status=404)
|
||||
except (ValueError, TypeError) as e:
|
||||
log.error(
|
||||
'Invalid parameter during social auth disconnect: backend=%s, association_id=%s, user_id=%s, error=%s',
|
||||
backend, association_id, user.id, str(e)
|
||||
)
|
||||
record_exception()
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Invalid request parameters',
|
||||
'backend': backend,
|
||||
'association_id': association_id
|
||||
}, status=400)
|
||||
except DatabaseError as e:
|
||||
log.error(
|
||||
'Database error during social auth disconnect: backend=%s, association_id=%s, user_id=%s, error=%s',
|
||||
backend, association_id, user.id, str(e)
|
||||
)
|
||||
record_exception()
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Service temporarily unavailable',
|
||||
'backend': backend,
|
||||
'association_id': association_id
|
||||
}, status=500)
|
||||
except ValidationError as e:
|
||||
log.error(
|
||||
'Validation error during social auth disconnect: backend=%s, association_id=%s, user_id=%s, error=%s',
|
||||
backend, association_id, user.id, str(e)
|
||||
)
|
||||
record_exception()
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Invalid request data',
|
||||
'backend': backend,
|
||||
'association_id': association_id
|
||||
}, status=400)
|
||||
except PermissionDenied as e:
|
||||
log.warning(
|
||||
'Permission denied during social auth disconnect: backend=%s, association_id=%s, user_id=%s, error=%s',
|
||||
backend, association_id, user.id, str(e)
|
||||
)
|
||||
record_exception()
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'You do not have permission to perform this action',
|
||||
'backend': backend,
|
||||
'association_id': association_id
|
||||
}, status=403)
|
||||
except (ImportError, AttributeError, RuntimeError) as e:
|
||||
log.error(
|
||||
'System error during social auth disconnect: backend=%s, association_id=%s, user_id=%s, error=%s',
|
||||
backend, association_id, user.id, str(e)
|
||||
)
|
||||
record_exception()
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Service temporarily unavailable',
|
||||
'backend': backend,
|
||||
'association_id': association_id
|
||||
}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user