Add third-party auth implementation and tests
This commit is contained in:
@@ -16,6 +16,7 @@ from django.contrib.auth import logout, authenticate, login
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.views import password_reset_confirm
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.mail import send_mail
|
||||
@@ -85,6 +86,8 @@ from util.password_policy_validators import (
|
||||
validate_password_dictionary
|
||||
)
|
||||
|
||||
from third_party_auth import pipeline, provider
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
|
||||
@@ -363,11 +366,16 @@ def signin_user(request):
|
||||
context = {
|
||||
'course_id': request.GET.get('course_id'),
|
||||
'enrollment_action': request.GET.get('enrollment_action'),
|
||||
# Bool injected into JS to submit form if we're inside a running third-
|
||||
# party auth pipeline; distinct from the actual instance of the running
|
||||
# pipeline, if any.
|
||||
'pipeline_running': 'true' if pipeline.running(request) else 'false',
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
),
|
||||
}
|
||||
|
||||
return render_to_response('login.html', context)
|
||||
|
||||
|
||||
@@ -385,17 +393,34 @@ def register_user(request, extra_context=None):
|
||||
|
||||
context = {
|
||||
'course_id': request.GET.get('course_id'),
|
||||
'email': '',
|
||||
'enrollment_action': request.GET.get('enrollment_action'),
|
||||
'name': '',
|
||||
'running_pipeline': None,
|
||||
'platform_name': microsite.get_value(
|
||||
'platform_name',
|
||||
settings.PLATFORM_NAME
|
||||
),
|
||||
'selected_provider': '',
|
||||
'username': '',
|
||||
}
|
||||
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
|
||||
if context.get("extauth_domain", '').startswith(external_auth.views.SHIBBOLETH_DOMAIN_PREFIX):
|
||||
return render_to_response('register-shib.html', context)
|
||||
|
||||
# If third-party auth is enabled, prepopulate the form with data from the
|
||||
# selected provider.
|
||||
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
current_provider = provider.Registry.get_by_backend_name(running_pipeline.get('backend'))
|
||||
overrides = current_provider.get_register_form_data(running_pipeline.get('kwargs'))
|
||||
overrides['running_pipeline'] = running_pipeline
|
||||
overrides['selected_provider'] = current_provider.NAME
|
||||
context.update(overrides)
|
||||
|
||||
return render_to_response('register.html', context)
|
||||
|
||||
|
||||
@@ -532,8 +557,17 @@ def dashboard(request):
|
||||
'language_options': language_options,
|
||||
'current_language': current_language,
|
||||
'current_language_code': cur_lang_code,
|
||||
'user': user,
|
||||
'duplicate_provider': None,
|
||||
'logout_url': reverse(logout_user),
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
'provider_states': [],
|
||||
}
|
||||
|
||||
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
|
||||
context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request))
|
||||
context['provider_user_states'] = pipeline.get_provider_user_states(user)
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
@@ -690,6 +724,7 @@ def accounts_login(request):
|
||||
return external_auth.views.course_specific_login(request, course_id)
|
||||
|
||||
context = {
|
||||
'pipeline_running': 'false',
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
}
|
||||
return render_to_response('login.html', context)
|
||||
@@ -697,24 +732,62 @@ def accounts_login(request):
|
||||
|
||||
# Need different levels of logging
|
||||
@ensure_csrf_cookie
|
||||
def login_user(request, error=""):
|
||||
def login_user(request, error=""): # pylint: disable-msg=too-many-statements,unused-argument
|
||||
"""AJAX request to log in the user."""
|
||||
if 'email' not in request.POST or 'password' not in request.POST:
|
||||
return JsonResponse({
|
||||
"success": False,
|
||||
"value": _('There was an error receiving your login information. Please email us.'), # TODO: User error message
|
||||
}) # TODO: this should be status code 400 # pylint: disable=fixme
|
||||
|
||||
email = request.POST['email']
|
||||
password = request.POST['password']
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
|
||||
AUDIT_LOG.warning(u"Login failed - Unknown user email")
|
||||
else:
|
||||
AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email))
|
||||
user = None
|
||||
backend_name = None
|
||||
email = None
|
||||
password = None
|
||||
redirect_url = None
|
||||
response = None
|
||||
running_pipeline = None
|
||||
third_party_auth_requested = settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request)
|
||||
third_party_auth_successful = False
|
||||
trumped_by_first_party_auth = bool(request.POST.get('email')) or bool(request.POST.get('password'))
|
||||
user = None
|
||||
|
||||
if third_party_auth_requested and not trumped_by_first_party_auth:
|
||||
# The user has already authenticated via third-party auth and has not
|
||||
# asked to do first party auth by supplying a username or password. We
|
||||
# now want to put them through the same logging and cookie calculation
|
||||
# logic as with first-party auth.
|
||||
running_pipeline = pipeline.get(request)
|
||||
username = running_pipeline['kwargs'].get('username')
|
||||
backend_name = running_pipeline['backend']
|
||||
requested_provider = provider.Registry.get_by_backend_name(backend_name)
|
||||
|
||||
try:
|
||||
user = pipeline.get_authenticated_user(username, backend_name)
|
||||
third_party_auth_successful = True
|
||||
except User.DoesNotExist:
|
||||
AUDIT_LOG.warning(
|
||||
u'Login failed - user with username {username} has no social auth with backend_name {backend_name}'.format(
|
||||
username=username, backend_name=backend_name))
|
||||
return JsonResponse({
|
||||
"success": False,
|
||||
# Translators: provider_name is the name of an external, third-party user authentication service (like
|
||||
# Google or LinkedIn).
|
||||
"value": _('There is no {platform_name} account associated with your {provider_name} account. Please use your {platform_name} credentials or pick another provider.').format(
|
||||
platform_name=settings.PLATFORM_NAME, provider_name=requested_provider.NAME)
|
||||
}) # TODO: this should be a status code 401 # pylint: disable=fixme
|
||||
|
||||
else:
|
||||
|
||||
if 'email' not in request.POST or 'password' not in request.POST:
|
||||
return JsonResponse({
|
||||
"success": False,
|
||||
"value": _('There was an error receiving your login information. Please email us.'), # TODO: User error message
|
||||
}) # TODO: this should be status code 400 # pylint: disable=fixme
|
||||
|
||||
email = request.POST['email']
|
||||
password = request.POST['password']
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
if settings.FEATURES['SQUELCH_PII_IN_LOGS']:
|
||||
AUDIT_LOG.warning(u"Login failed - Unknown user email")
|
||||
else:
|
||||
AUDIT_LOG.warning(u"Login failed - Unknown user email: {0}".format(email))
|
||||
|
||||
# check if the user has a linked shibboleth account, if so, redirect the user to shib-login
|
||||
# This behavior is pretty much like what gmail does for shibboleth. Try entering some @stanford.edu
|
||||
@@ -753,14 +826,17 @@ def login_user(request, error=""):
|
||||
# username so that authentication is guaranteed to fail and we can take
|
||||
# advantage of the ratelimited backend
|
||||
username = user.username if user else ""
|
||||
try:
|
||||
user = authenticate(username=username, password=password, request=request)
|
||||
# this occurs when there are too many attempts from the same IP address
|
||||
except RateLimitException:
|
||||
return JsonResponse({
|
||||
"success": False,
|
||||
"value": _('Too many failed login attempts. Try again later.'),
|
||||
}) # TODO: this should be status code 429 # pylint: disable=fixme
|
||||
|
||||
if not third_party_auth_successful:
|
||||
try:
|
||||
user = authenticate(username=username, password=password, request=request)
|
||||
# this occurs when there are too many attempts from the same IP address
|
||||
except RateLimitException:
|
||||
return JsonResponse({
|
||||
"success": False,
|
||||
"value": _('Too many failed login attempts. Try again later.'),
|
||||
}) # TODO: this should be status code 429 # pylint: disable=fixme
|
||||
|
||||
if user is None:
|
||||
# tick the failed login counters if the user exists in the database
|
||||
if user_found_by_email_lookup and LoginFailures.is_feature_enabled():
|
||||
@@ -801,7 +877,9 @@ def login_user(request, error=""):
|
||||
|
||||
redirect_url = try_change_enrollment(request)
|
||||
|
||||
dog_stats_api.increment("common.student.successful_login")
|
||||
if third_party_auth_successful:
|
||||
redirect_url = pipeline.get_complete_url(backend_name)
|
||||
|
||||
response = JsonResponse({
|
||||
"success": True,
|
||||
"redirect_url": redirect_url,
|
||||
@@ -1027,16 +1105,20 @@ def _do_create_account(post_vars):
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def create_account(request, post_override=None):
|
||||
def create_account(request, post_override=None): # pylint: disable-msg=too-many-statements
|
||||
"""
|
||||
JSON call to create new edX account.
|
||||
Used by form in signup_modal.html, which is included into navigation.html
|
||||
"""
|
||||
js = {'success': False}
|
||||
js = {'success': False} # pylint: disable-msg=invalid-name
|
||||
|
||||
post_vars = post_override if post_override else request.POST
|
||||
extra_fields = getattr(settings, 'REGISTRATION_EXTRA_FIELDS', {})
|
||||
|
||||
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request):
|
||||
post_vars = dict(post_vars.items())
|
||||
post_vars.update({'password': pipeline.make_random_password()})
|
||||
|
||||
# if doing signup for an external authorization, then get email, password, name from the eamap
|
||||
# don't use the ones from the form, since the user could have hacked those
|
||||
# unless originally we didn't get a valid email or name from the external auth
|
||||
@@ -1234,9 +1316,17 @@ def create_account(request, post_override=None):
|
||||
login_user.save()
|
||||
AUDIT_LOG.info(u"Login activated on extauth account - {0} ({1})".format(login_user.username, login_user.email))
|
||||
|
||||
dog_stats_api.increment("common.student.account_created")
|
||||
redirect_url = try_change_enrollment(request)
|
||||
|
||||
# Resume the third-party-auth pipeline if necessary.
|
||||
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and pipeline.running(request):
|
||||
running_pipeline = pipeline.get(request)
|
||||
redirect_url = pipeline.get_complete_url(running_pipeline['backend'])
|
||||
|
||||
response = JsonResponse({
|
||||
'success': True,
|
||||
'redirect_url': try_change_enrollment(request),
|
||||
'redirect_url': redirect_url,
|
||||
})
|
||||
|
||||
# set the login cookie for the edx marketing site
|
||||
|
||||
Reference in New Issue
Block a user