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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,4 @@ William Desloge <william.desloge@ionis-group.com>
Marco Re <mrc.re@tiscali.it>
Jonas Jelten <jelten@in.tum.de>
Christine Lytwynec <clytwynec@edx.org>
John Cox <johncox@google.com>
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.

Common: Add extensible third-party auth module.

Blades: Handle situation if no response were sent from XQueue to LMS in Matlab
problem after Run Code button press. BLD-994.

Expand Down
146 changes: 118 additions & 28 deletions common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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
Expand Down Expand Up @@ -85,6 +86,8 @@
validate_password_dictionary
)

from third_party_auth import pipeline, provider

log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")

Expand Down Expand Up @@ -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)


Expand All @@ -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)


Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -690,31 +724,70 @@ 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)


# 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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions common/djangoapps/third_party_auth/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Middleware classes for third_party_auth."""

from social.apps.django_app.middleware import SocialAuthExceptionMiddleware

from . import pipeline


class ExceptionMiddleware(SocialAuthExceptionMiddleware):
"""Custom middleware that handles conditional redirection."""

def get_redirect_uri(self, request, exception):
# Safe because it's already been validated by
# pipeline.parse_query_params. If that pipeline step ever moves later
# in the pipeline stack, we'd need to validate this value because it
# would be an injection point for attacker data.
auth_entry = request.session.get(pipeline.AUTH_ENTRY_KEY)
# Fall back to django settings's SOCIAL_AUTH_LOGIN_ERROR_URL.
return '/' + auth_entry if auth_entry else super(ExceptionMiddleware, self).get_redirect_uri(request, exception)
Loading