Add third party auth context api (#25497)

This commit is contained in:
Zainab Amir
2020-11-05 19:26:29 +05:00
committed by GitHub
parent 8522579608
commit 97e9fee92e
13 changed files with 326 additions and 80 deletions

View File

@@ -2273,6 +2273,7 @@ DISABLE_DEPRECATED_SIGNUP_URL = False
##### LOGISTRATION RATE LIMIT SETTINGS #####
LOGISTRATION_RATELIMIT_RATE = '100/5m'
LOGISTRATION_API_RATELIMIT = '20/m'
##### REGISTRATION RATE LIMIT SETTINGS #####
REGISTRATION_VALIDATION_RATELIMIT = '30/7d'

View File

@@ -322,5 +322,6 @@ PROCTORING_SETTINGS = {}
##### LOGISTRATION RATE LIMIT SETTINGS #####
LOGISTRATION_RATELIMIT_RATE = '5/5m'
LOGISTRATION_API_RATELIMIT = '5/m'
REGISTRATION_VALIDATION_RATELIMIT = '5/minute'

View File

@@ -3930,6 +3930,7 @@ RATELIMIT_RATE = '120/m'
##### LOGISTRATION RATE LIMIT SETTINGS #####
LOGISTRATION_RATELIMIT_RATE = '100/5m'
LOGISTRATION_API_RATELIMIT = '20/m'
##### PASSWORD RESET RATE LIMIT SETTINGS #####
PASSWORD_RESET_IP_RATE = '1/m'

View File

@@ -605,6 +605,7 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get(
##### LOGISTRATION RATE LIMIT SETTINGS #####
LOGISTRATION_RATELIMIT_RATE = ENV_TOKENS.get('LOGISTRATION_RATELIMIT_RATE', LOGISTRATION_RATELIMIT_RATE)
LOGISTRATION_API_RATELIMIT = ENV_TOKENS.get('LOGISTRATION_API_RATELIMIT', LOGISTRATION_API_RATELIMIT)
##### REGISTRATION RATE LIMIT SETTINGS #####
REGISTRATION_VALIDATION_RATELIMIT = ENV_TOKENS.get(

View File

@@ -587,5 +587,6 @@ RATELIMIT_RATE = '2/m'
##### LOGISTRATION RATE LIMIT SETTINGS #####
LOGISTRATION_RATELIMIT_RATE = '5/5m'
LOGISTRATION_API_RATELIMIT = '5/m'
REGISTRATION_VALIDATION_RATELIMIT = '5/minute'

View File

@@ -0,0 +1,166 @@
"""
Logistration API View Tests
"""
import ddt
from django.conf import settings
from django.urls import reverse
from mock import patch
from rest_framework.test import APITestCase
from six.moves.urllib.parse import urlencode
from openedx.core.djangolib.testing.utils import skip_unless_lms
from third_party_auth import pipeline
from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin, simulate_running_pipeline
@skip_unless_lms
@ddt.ddt
class TPAContextViewTest(ThirdPartyAuthTestMixin, APITestCase):
"""
Third party auth context tests
"""
def setUp(self): # pylint: disable=arguments-differ
"""
Test Setup
"""
super(TPAContextViewTest, self).setUp()
self.url = reverse('third_party_auth_context')
self.query_params = {'redirect_to': '/dashboard'}
# Several third party auth providers are created for these tests:
self.configure_google_provider(enabled=True, visible=True)
self.configure_facebook_provider(enabled=True, visible=True)
self.hidden_enabled_provider = self.configure_linkedin_provider(
visible=False,
enabled=True,
)
def _third_party_login_url(self, backend_name, auth_entry, params):
"""
Construct the login URL to start third party authentication
"""
return u'{url}?auth_entry={auth_entry}&{param_str}'.format(
url=reverse('social:begin', kwargs={'backend': backend_name}),
auth_entry=auth_entry,
param_str=urlencode(params)
)
def get_provider_data(self, params):
"""
Returns the expected provider data based on providers enabled in test setup
"""
return [
{
'id': 'oa2-facebook',
'name': 'Facebook',
'iconClass': 'fa-facebook',
'iconImage': None,
'loginUrl': self._third_party_login_url('facebook', 'login', params),
'registerUrl': self._third_party_login_url('facebook', 'register', params)
},
{
'id': 'oa2-google-oauth2',
'name': 'Google',
'iconClass': 'fa-google-plus',
'iconImage': None,
'loginUrl': self._third_party_login_url('google-oauth2', 'login', params),
'registerUrl': self._third_party_login_url('google-oauth2', 'register', params)
},
]
def get_context(self, params=None, current_provider=None, backend_name=None, add_user_details=False):
"""
Returns the third party auth context
"""
return {
'currentProvider': current_provider,
'providers': self.get_provider_data(params) if params else [],
'secondaryProviders': [],
'finishAuthUrl': pipeline.get_complete_url(backend_name) if backend_name else None,
'errorMessage': None,
'registerFormSubmitButtonText': 'Create Account',
'syncLearnerProfileData': False,
'pipeline_user_details': {'email': 'test@test.com'} if add_user_details else {}
}
def test_missing_arguments(self):
"""
Test that if required arguments are missing, proper status code and message
is returned.
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data, {'message': 'Request missing required parameter: redirect_to'})
@patch.dict(settings.FEATURES, {'ENABLE_THIRD_PARTY_AUTH': False})
def test_no_third_party_auth_providers(self):
"""
Test that if third party auth is enabled, context returned by API contains
the provider information
"""
response = self.client.get(self.url, self.query_params)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, self.get_context())
def test_third_party_auth_providers(self):
"""
Test that api returns details of currently enabled third party auth providers
"""
response = self.client.get(self.url, self.query_params)
params = {
'next': self.query_params['redirect_to']
}
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, self.get_context(params))
@ddt.data(
('google-oauth2', 'Google', False),
('facebook', 'Facebook', False),
('google-oauth2', 'Google', True)
)
@ddt.unpack
def test_running_pipeline(self, current_backend, current_provider, add_user_details):
"""
Test that when third party pipeline is running, the api returns details
of current provider
"""
email = 'test@test.com' if add_user_details else None
params = {
'next': self.query_params['redirect_to']
}
# Simulate a running pipeline
pipeline_target = 'openedx.core.djangoapps.user_authn.views.login_form.third_party_auth.pipeline'
with simulate_running_pipeline(pipeline_target, current_backend, email=email):
response = self.client.get(self.url, self.query_params)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, self.get_context(params, current_provider, current_backend, add_user_details))
def test_tpa_hint(self):
"""
Test that if tpa_hint is provided, the context returns the third party auth provider
even if it is not visible on the login page
"""
params = {
'next': self.query_params['redirect_to']
}
tpa_hint = self.hidden_enabled_provider.provider_id
self.query_params.update({'tpa_hint': tpa_hint})
provider_data = self.get_provider_data(params)
provider_data.append({
'id': self.hidden_enabled_provider.provider_id,
'name': 'LinkedIn',
'iconClass': 'fa-linkedin',
'iconImage': None,
'loginUrl': self._third_party_login_url('linkedin-oauth2', 'login', params),
'registerUrl': self._third_party_login_url('linkedin-oauth2', 'register', params)
})
response = self.client.get(self.url, self.query_params)
self.assertEqual(response.data['providers'], provider_data)

View File

@@ -0,0 +1,13 @@
"""
Logistration API urls
"""
from django.conf.urls import url
from openedx.core.djangoapps.user_authn.api.views import TPAContextView
urlpatterns = [
url(
r'^third_party_auth_context$', TPAContextView.as_view(), name='third_party_auth_context'
),
]

View File

@@ -0,0 +1,56 @@
"""
Logistration API Views
"""
from django.conf import settings
from rest_framework import status
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle
from rest_framework.views import APIView
from openedx.core.djangoapps.user_authn.utils import third_party_auth_context
REDIRECT_KEY = 'redirect_to'
class ThirdPartyAuthContextThrottle(AnonRateThrottle):
"""
Setting rate limit for ThirdPartyAuthContext API
"""
rate = settings.LOGISTRATION_API_RATELIMIT
class TPAContextView(APIView):
"""
API to get third party auth providers and the currently running pipeline.
"""
throttle_classes = [ThirdPartyAuthContextThrottle]
def get(self, request, **kwargs):
"""
Returns the context for third party auth providers and the currently running pipeline.
Arguments:
request (HttpRequest): The request, used to determine if a pipeline
is currently running.
redirect_to: The URL to send the user to following successful
authentication.
tpa_hint (string): An override flag that will return a matching provider
as long as its configuration has been enabled
"""
request_params = request.GET
if REDIRECT_KEY not in request_params:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={'message': 'Request missing required parameter: {}'.format(REDIRECT_KEY)}
)
redirect_to = request_params.get(REDIRECT_KEY)
third_party_auth_hint = request_params.get('tpa_hint')
context = third_party_auth_context(request, redirect_to, third_party_auth_hint)
return Response(
status=status.HTTP_200_OK,
data=context
)

View File

@@ -1,15 +1,13 @@
""" URLs for User Authentication """
from django.conf import settings
from django.conf.urls import include, url
from .views import login, login_form
urlpatterns = [
# TODO move contents of urls_common here once CMS no longer has its own login
url(r'', include('openedx.core.djangoapps.user_authn.urls_common')),
url(r'^api/', include('openedx.core.djangoapps.user_authn.api.urls')),
url(r'^account/finish_auth$', login.finish_auth, name='finish_auth'),
]

View File

@@ -2,16 +2,93 @@
Utility functions used during user authentication.
"""
import random
import string
import six
from django.conf import settings
from django.contrib import messages
from django.utils import http
from django.utils.translation import ugettext as _
from oauth2_provider.models import Application
from six.moves.urllib.parse import urlparse # pylint: disable=import-error
import third_party_auth
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from third_party_auth import pipeline
def third_party_auth_context(request, redirect_to, tpa_hint=None):
"""
Context for third party auth providers and the currently running pipeline.
Arguments:
request (HttpRequest): The request, used to determine if a pipeline
is currently running.
redirect_to: The URL to send the user to following successful
authentication.
tpa_hint (string): An override flag that will return a matching provider
as long as its configuration has been enabled
Returns:
dict
"""
context = {
"currentProvider": None,
"providers": [],
"secondaryProviders": [],
"finishAuthUrl": None,
"errorMessage": None,
"registerFormSubmitButtonText": _("Create Account"),
"syncLearnerProfileData": False,
"pipeline_user_details": {}
}
if third_party_auth.is_enabled():
for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint):
info = {
"id": enabled.provider_id,
"name": enabled.name,
"iconClass": enabled.icon_class or None,
"iconImage": enabled.icon_image.url if enabled.icon_image else None,
"loginUrl": pipeline.get_login_url(
enabled.provider_id,
pipeline.AUTH_ENTRY_LOGIN,
redirect_url=redirect_to,
),
"registerUrl": pipeline.get_login_url(
enabled.provider_id,
pipeline.AUTH_ENTRY_REGISTER,
redirect_url=redirect_to,
),
}
context["providers" if not enabled.secondary else "secondaryProviders"].append(info)
running_pipeline = pipeline.get(request)
if running_pipeline is not None:
current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline)
user_details = running_pipeline['kwargs']['details']
if user_details:
context['pipeline_user_details'] = user_details
if current_provider is not None:
context["currentProvider"] = current_provider.name
context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name)
context["syncLearnerProfileData"] = current_provider.sync_learner_profile_data
if current_provider.skip_registration_form:
# As a reliable way of "skipping" the registration form, we just submit it automatically
context["autoSubmitRegForm"] = True
# Check for any error messages we may want to display:
for msg in messages.get_messages(request):
if msg.extra_tags.split()[0] == "social-auth":
# msg may or may not be translated. Try translating [again] in case we are able to:
context["errorMessage"] = _(six.text_type(msg)) # pylint: disable=E7610
break
return context
def is_safe_login_or_logout_redirect(redirect_to, request_host, dot_client_id, require_https):

View File

@@ -24,14 +24,17 @@ from openedx.core.djangoapps.user_api.accounts.utils import (
)
from openedx.core.djangoapps.user_api.helpers import FormDescription
from openedx.core.djangoapps.user_authn.cookies import are_logged_in_cookies_set
from openedx.core.djangoapps.user_authn.utils import should_redirect_to_logistration_mircrofrontend
from openedx.core.djangoapps.user_authn.utils import (
should_redirect_to_logistration_mircrofrontend,
third_party_auth_context
)
from openedx.core.djangoapps.user_authn.views.password_reset import get_password_reset_form
from openedx.core.djangoapps.user_authn.views.registration_form import RegistrationFormFactory
from openedx.features.enterprise_support.api import enterprise_customer_for_request
from openedx.features.enterprise_support.utils import (
handle_enterprise_cookies_for_logistration,
get_enterprise_slug_login_url,
update_logistration_context_for_enterprise,
handle_enterprise_cookies_for_logistration,
update_logistration_context_for_enterprise
)
from student.helpers import get_next_url_for_login_page
from third_party_auth import pipeline
@@ -209,7 +212,7 @@ def login_and_registration_form(request, initial_mode="login"):
'data': {
'login_redirect_url': redirect_to,
'initial_mode': initial_mode,
'third_party_auth': _third_party_auth_context(request, redirect_to, third_party_auth_hint),
'third_party_auth': third_party_auth_context(request, redirect_to, third_party_auth_hint),
'third_party_auth_hint': third_party_auth_hint or '',
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
'support_link': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK),
@@ -268,75 +271,3 @@ def _get_form_descriptions(request):
'login': get_login_session_form(request).to_json(),
'registration': RegistrationFormFactory().get_registration_form(request).to_json()
}
def _third_party_auth_context(request, redirect_to, tpa_hint=None):
"""Context for third party auth providers and the currently running pipeline.
Arguments:
request (HttpRequest): The request, used to determine if a pipeline
is currently running.
redirect_to: The URL to send the user to following successful
authentication.
tpa_hint (string): An override flag that will return a matching provider
as long as its configuration has been enabled
Returns:
dict
"""
context = {
"currentProvider": None,
"providers": [],
"secondaryProviders": [],
"finishAuthUrl": None,
"errorMessage": None,
"registerFormSubmitButtonText": _("Create Account"),
"syncLearnerProfileData": False,
"pipeline_user_details": {}
}
if third_party_auth.is_enabled():
for enabled in third_party_auth.provider.Registry.displayed_for_login(tpa_hint=tpa_hint):
info = {
"id": enabled.provider_id,
"name": enabled.name,
"iconClass": enabled.icon_class or None,
"iconImage": enabled.icon_image.url if enabled.icon_image else None,
"loginUrl": pipeline.get_login_url(
enabled.provider_id,
pipeline.AUTH_ENTRY_LOGIN,
redirect_url=redirect_to,
),
"registerUrl": pipeline.get_login_url(
enabled.provider_id,
pipeline.AUTH_ENTRY_REGISTER,
redirect_url=redirect_to,
),
}
context["providers" if not enabled.secondary else "secondaryProviders"].append(info)
running_pipeline = pipeline.get(request)
if running_pipeline is not None:
current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline)
user_details = running_pipeline['kwargs']['details']
if user_details:
context['pipeline_user_details'] = user_details
if current_provider is not None:
context["currentProvider"] = current_provider.name
context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name)
context["syncLearnerProfileData"] = current_provider.sync_learner_profile_data
if current_provider.skip_registration_form:
# As a reliable way of "skipping" the registration form, we just submit it automatically
context["autoSubmitRegForm"] = True
# Check for any error messages we may want to display:
for msg in messages.get_messages(request):
if msg.extra_tags.split()[0] == "social-auth":
# msg may or may not be translated. Try translating [again] in case we are able to:
context["errorMessage"] = _(six.text_type(msg)) # pylint: disable=E7610
break
return context