Commit f89a9d37 by Bertrand Marron

Add IONISx auth platform

parent d9dcd4db
......@@ -286,6 +286,12 @@ SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIME
##### X-Frame-Options response header settings #####
X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS)
##### Third-party auth options ################################################
THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH)
IONISX_AUTH = AUTH_TOKENS.get('IONISX_AUTH')
if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
MIDDLEWARE_CLASSES += ('third_party_auth.middleware.PortalSynchronizerMiddleware',)
##### ADVANCED_SECURITY_CONFIG #####
ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
......
......@@ -96,6 +96,10 @@ FEATURES = {
# Turn on/off Microsites feature
'USE_MICROSITES': False,
# Turn on third-party auth. Disabled for now because full implementations are not yet available. Remember to syncdb
# if you enable this; we don't create tables by default.
'ENABLE_THIRD_PARTY_AUTH': False,
# Allow creating courses with non-ascii characters in the course id
'ALLOW_UNICODE_COURSE_ID': False,
......@@ -716,6 +720,10 @@ for app_name in OPTIONAL_APPS:
continue
INSTALLED_APPS += (app_name,)
# Stub for third_party_auth options.
# See common/djangoapps/third_party_auth/settings.py for configuration details.
THIRD_PARTY_AUTH = {}
### ADVANCED_SECURITY_CONFIG
# Empty by default
ADVANCED_SECURITY_CONFIG = {}
......
......@@ -21,6 +21,9 @@ def run():
add_mimetypes()
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
enable_third_party_auth()
def add_mimetypes():
"""
......@@ -34,3 +37,14 @@ def add_mimetypes():
mimetypes.add_type('application/x-font-opentype', '.otf')
mimetypes.add_type('application/x-font-ttf', '.ttf')
mimetypes.add_type('application/font-woff', '.woff')
def enable_third_party_auth():
"""
Enable the use of third_party_auth, which allows users to sign in to edX
using other identity providers. For configuration details, see
common/djangoapps/third_party_auth/settings.py.
"""
from third_party_auth import settings as auth_settings
auth_settings.apply_settings(settings.THIRD_PARTY_AUTH, settings)
......@@ -49,6 +49,9 @@ urlpatterns += patterns(
url(r'^create_account$', 'student.views.create_account', name='create_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name='activate'),
url(r'^signin$', 'student.views.signin_user', name="login"),
url(r'^signup$', 'student.views.signin_user', name='signup'),
# ajax view that actually does the work
url(r'^login_post$', 'student.views.login_user', name='login_post'),
......@@ -62,8 +65,6 @@ urlpatterns += patterns(
url(r'^$', 'howitworks', name='homepage'),
url(r'^howitworks$', 'howitworks'),
url(r'^signup$', 'signup', name='signup'),
url(r'^signin$', 'login_page', name='login'),
url(r'^request_course_creator$', 'request_course_creator'),
url(r'^course_team/{}/(?P<email>.+)?$'.format(settings.COURSE_KEY_PATTERN), 'course_team_handler'),
......@@ -133,6 +134,12 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
url(r'^auto_auth$', 'student.views.auto_auth'),
)
# Third-party auth.
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
urlpatterns += (
url(r'', include('third_party_auth.urls')),
)
if settings.DEBUG:
try:
from .urls_dev import urlpatterns as dev_urlpatterns
......
......@@ -26,7 +26,7 @@ from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbi
from django.shortcuts import redirect
from django.utils.translation import ungettext
from django_future.csrf import ensure_csrf_cookie
from django.utils.http import cookie_date, base36_to_int
from django.utils.http import cookie_date, base36_to_int, urlquote
from django.utils.translation import ugettext as _, get_language
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
......@@ -100,6 +100,7 @@ from util.password_policy_validators import (
import third_party_auth
from third_party_auth import pipeline, provider
from student.helpers import auth_pipeline_urls, set_logged_in_cookie
from social.apps.django_app.default import models
from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import CourseRegistrationCode
......@@ -347,6 +348,20 @@ def signin_user(request):
"""
This view will display the non-modal login form
"""
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
# Redirect to IONISx, we don't want the registration form.
get = request.GET.copy()
if 'course_id' in request.GET:
request.session['enroll_course_id'] = request.GET.get('course_id')
get.update({ 'next': reverse('about_course', kwargs={ 'course_id': unicode(request.GET.get('course_id')) }) })
redirect_uri = reverse('social:begin', args=('portal-oauth2',))
return external_auth.views.redirect_with_get(redirect_uri, get, do_reverse=False)
if (settings.FEATURES['AUTH_USE_CERTIFICATES'] and
external_auth.views.ssl_get_cert_from_request(request)):
# SSL login doesn't require a view, so redirect
......@@ -356,8 +371,6 @@ def signin_user(request):
if settings.FEATURES.get('AUTH_USE_CAS'):
# If CAS is enabled, redirect auth handling to there
return redirect(reverse('cas-login'))
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
course_id = request.GET.get('course_id')
context = {
......@@ -376,7 +389,6 @@ def signin_user(request):
return render_to_response('login.html', context)
@ensure_csrf_cookie
def register_user(request, extra_context=None):
"""
......@@ -384,6 +396,18 @@ def register_user(request, extra_context=None):
"""
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
# Redirect to IONISx, we don't want the registration form.
get = request.GET.copy()
if 'course_id' in request.GET:
request.session['enroll_course_id'] = request.GET.get('course_id')
get.update({ 'next': reverse('about_course', kwargs={ 'course_id': unicode(request.GET.get('course_id')) }) })
redirect_uri = reverse('social:begin', args=('portal-oauth2',))
return external_auth.views.redirect_with_get(redirect_uri, get, do_reverse=False)
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to branding to process their certificate if SSL is enabled
# and registration is disabled.
......@@ -884,6 +908,13 @@ def _get_course_enrollment_domain(course_id):
@never_cache
@ensure_csrf_cookie
def auth_with_no_login(request):
redirect_uri = reverse('social:begin', args=('portal-oauth2',))
return external_auth.views.redirect_with_get(redirect_uri, request.GET, do_reverse=False)
@never_cache
@ensure_csrf_cookie
def accounts_login(request):
"""
This view is mainly used as the redirect from the @login_required decorator. I don't believe that
......@@ -1157,11 +1188,7 @@ def logout_user(request):
# We do not log here, because we have a handler registered
# to perform logging on successful logouts.
logout(request)
if settings.FEATURES.get('AUTH_USE_CAS'):
target = reverse('cas-logout')
else:
target = '/'
response = redirect(target)
response = redirect(settings.IONISX_AUTH.get('LOGOUT_URL'))
response.delete_cookie(
settings.EDXMKTG_COOKIE_NAME,
path='/', domain=settings.SESSION_COOKIE_DOMAIN,
......
"""Middleware classes for third_party_auth."""
import logging
import requests
from django.conf import settings
from django.contrib.auth import logout
from django.shortcuts import redirect
from django.contrib.auth.models import AnonymousUser
from student.models import UserProfile
from social.apps.django_app.default import models
from social.apps.django_app.middleware import SocialAuthExceptionMiddleware
from . import pipeline
from . import portal
log = logging.getLogger(__file__)
class ExceptionMiddleware(SocialAuthExceptionMiddleware):
"""Custom middleware that handles conditional redirection."""
......@@ -23,3 +35,49 @@ class ExceptionMiddleware(SocialAuthExceptionMiddleware):
redirect_uri = pipeline.AUTH_DISPATCH_URLS[auth_entry]
return redirect_uri
class PortalSynchronizerMiddleware(object):
"""Custom middleware to synchronize user status of LMS with Portal provider."""
def process_request(self, request):
if request.user.is_authenticated():
user = request.user
social_auth = models.DjangoStorage.user.get_social_auth_for_user(user)
if len(social_auth) == 1:
social_data = social_auth[0]
try:
r = requests.get(
settings.IONISX_AUTH.get('USER_DATA_URL'),
headers={'Authorization': 'Bearer {0}'.format(social_data.extra_data['access_token'])}
)
except requests.ConnectionError as err:
log.warning(err)
return
body = r.json()
if r.status_code != 200:
if body and u'error' in body and u'redirectTo' in body[u'error']:
return redirect(body[u'error'][u'redirectTo'])
else:
return logout(request)
if body:
_id = body['_id']
email = portal.get_primary_email(body['emails'])
username = body['username']
name = body['name']
if (user.email != email or user.username != body['username']):
log.info('User {} needs to be updated'.format(_id))
user.email = email
user.username = username
user.save()
if user.profile.name != body['name']:
log.info('User profile for {} needs to be updated'.format(_id))
user.profile.name = name
user.profile.save()
......@@ -62,6 +62,7 @@ import string # pylint: disable-msg=deprecated-module
from collections import OrderedDict
import urllib
import analytics
import logging
from eventtracking import tracker
from django.contrib.auth.models import User
......@@ -71,6 +72,10 @@ from django.shortcuts import redirect
from social.apps.django_app.default import models
from social.exceptions import AuthException
from social.pipeline import partial
from django.db import IntegrityError, transaction
from student.models import (
Registration, UserProfile, create_comments_service_user
)
import student
from shoppingcart.models import Order, PaidCourseRegistration # pylint: disable=F0401
......@@ -87,6 +92,7 @@ from logging import getLogger
from . import provider
log = logging.getLogger(__file__)
# These are the query string params you can pass
# to the URL that starts the authentication process.
......@@ -450,8 +456,23 @@ def parse_query_params(strategy, response, *args, **kwargs):
# pylint: disable=fixme
'is_login_2': auth_entry == AUTH_ENTRY_LOGIN_2,
'is_register_2': auth_entry == AUTH_ENTRY_REGISTER_2,
}
def create_user_from_oauth(strategy, details, user, is_new, *args, **kwargs):
if is_new:
profile = UserProfile(user=user)
profile.name = details.get('fullname')
try:
profile.save()
except Exception:
log.error("UserProfile creation failed for user {id}.".format(id=user.id))
raise
create_comments_service_user(user)
# TODO (ECOM-369): Once the A/B test of the combined login/registration
# form completes, we will be able to remove the extra login/registration
# end-points. HOWEVER, users who used the new forms during the A/B
......@@ -527,6 +548,7 @@ def ensure_user_information(
return redirect(AUTH_DISPATCH_URLS[AUTH_ENTRY_REGISTER_2])
@partial.partial
def set_logged_in_cookie(backend=None, user=None, request=None, is_api=None, *args, **kwargs):
"""This pipeline step sets the "logged in" cookie for authenticated users.
......
from requests import HTTPError
from django.conf import settings
from social.backends.oauth import BaseOAuth2
from social.exceptions import AuthCanceled
def get_primary_email(emails):
for email in emails:
if email['primary'] is True:
return email['email']
return None
class PortalOAuth2(BaseOAuth2):
"""Portal OAuth2 authentication backend"""
auth_settings = settings.IONISX_AUTH
name = 'portal-oauth2'
ID_KEY = '_id'
AUTHORIZATION_URL = auth_settings.get('AUTHORIZATION_URL')
ACCESS_TOKEN_URL = auth_settings.get('ACCESS_TOKEN_URL')
ACCESS_TOKEN_METHOD = 'POST'
REDIRECT_STATE = False
USER_DATA_URL = auth_settings.get('USER_DATA_URL')
def get_user_details(self, response):
"""Return user details from IONISx account"""
return {
'username': response['username'],
'email': get_primary_email(response['emails']),
'fullname': response['name']
}
def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service"""
return self.get_json(
self.USER_DATA_URL,
headers={'Authorization': 'Bearer {0}'.format(access_token)}
)
def process_error(self, data):
super(PortalOAuth2, self).process_error(data)
if data.get('error_code'):
raise AuthCanceled(self, data.get('error_message') or
data.get('error_code'))
......@@ -5,6 +5,7 @@ invoke the Django armature.
"""
from social.backends import google, linkedin, facebook
from . import portal
_DEFAULT_ICON_CLASS = 'icon-signin'
......@@ -170,6 +171,26 @@ class FacebookOauth2(BaseProvider):
return provider_details.get('fullname')
class PortalOauth2(BaseProvider):
"""Provider for Portal's Oauth2 auth system."""
BACKEND_CLASS = portal.PortalOAuth2
ICON_CLASS = 'icon-ionisx'
NAME = 'Portal'
SETTINGS = {
'SOCIAL_AUTH_PORTAL_OAUTH2_KEY': None,
'SOCIAL_AUTH_PORTAL_OAUTH2_SECRET': None,
}
@classmethod
def get_email(cls, provider_details):
return provider_details.get('email')
@classmethod
def get_name(cls, provider_details):
return provider_details.get('fullname')
class Registry(object):
"""Singleton registry of third-party auth providers.
......
......@@ -50,7 +50,7 @@ _FIELDS_STORED_IN_SESSION = ['auth_entry', 'next', 'enroll_course_id']
_MIDDLEWARE_CLASSES = (
'third_party_auth.middleware.ExceptionMiddleware',
)
_SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/dashboard'
_SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/'
_SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = '/profile'
_SOCIAL_AUTH_DISCONNECT_REDIRECT_URL = '/profile'
......@@ -105,14 +105,13 @@ def _set_global_settings(django_settings):
# Inject our customized auth pipeline. All auth backends must work with
# this pipeline.
django_settings.SOCIAL_AUTH_PIPELINE = (
'third_party_auth.pipeline.parse_query_params',
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'third_party_auth.pipeline.ensure_user_information',
'social.pipeline.user.create_user',
'third_party_auth.pipeline.create_user_from_oauth',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details',
......
......@@ -440,6 +440,10 @@ X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS)
##### Third-party auth options ################################################
THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH)
IONISX_AUTH = AUTH_TOKENS.get('IONISX_AUTH')
if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
MIDDLEWARE_CLASSES += ('third_party_auth.middleware.PortalSynchronizerMiddleware',)
##### OAUTH2 Provider ##############
if FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
......
......@@ -75,96 +75,6 @@
<section class="container dashboard" id="dashboard-main">
<section class="profile-sidebar">
<header class="profile">
<h1 class="user-name">${ user.username }</h1>
</header>
<section class="user-info">
<ul>
<li class="info--username">
<span class="title">${_("Full Name")} (<a href="#apply_name_change" rel="leanModal" class="edit-name">${_("edit")}</a>)</span> <span class="data">${ user.profile.name | h }</span>
</li>
<li class="info--email">
<span class="title">${_("Email")}
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
(<a href="#change_email" rel="leanModal" class="edit-email">${_("edit")}</a>)
% endif
</span> <span class="data">${ user.email | h }</span>
</li>
%if len(language_options) > 1:
<%include file='dashboard/_dashboard_info_language.html' />
%endif
% if third_party_auth.is_enabled():
<li class="controls--account">
<span class="title">
## Translators: this section lists all the third-party authentication providers (for example, Google and LinkedIn) the user can link with or unlink from their edX account.
${_("Connected Accounts")}
</span>
<span class="data">
<span class="third-party-auth">
% for state in provider_user_states:
<div class="auth-provider">
<div class="status">
% if state.has_account:
<i class="icon icon-link"></i> <span class="copy">${_("Linked")}</span>
% else:
<i class="icon icon-unlink"></i><span class="copy">${_("Not Linked")}</span>
% endif
</div>
<span class="provider">${state.provider.NAME}</span>
<span class="control">
% if state.has_account:
<form
action="${pipeline.get_disconnect_url(state.provider.NAME)}"
method="post"
name="${state.get_unlink_form_name()}">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<a href="#" onclick="document.${state.get_unlink_form_name()}.submit()">
## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Unlink")}
</a>
% else:
<a href="${pipeline.get_login_url(state.provider.NAME, pipeline.AUTH_ENTRY_DASHBOARD)}">
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Link")}
</a>
% endif
</form>
</span>
</div>
% endfor
</span>
</li>
% endif
% if external_auth_map is None or 'shib' not in external_auth_map.external_domain:
<li class="controls--account">
<span class="title"><a href="#password_reset_complete" rel="leanModal" id="pwd_reset_button">${_("Reset Password")}</a></span>
<form id="password_reset_form" method="post" data-remote="true" action="${reverse('password_reset')}">
<input id="id_email" type="hidden" name="email" maxlength="75" value="${user.email}" />
<!-- <input type="submit" id="pwd_reset_button" value="${_("Reset Password")}" /> -->
</form>
</li>
% endif
<%include file='dashboard/_dashboard_status_verification.html' />
<%include file='dashboard/_dashboard_reverification_sidebar.html' />
</ul>
</section>
</section>
<section class="my-courses" id="my-courses">
<header>
<h2>${_("Current Courses")}</h2>
......
......@@ -31,7 +31,7 @@ urlpatterns = ('', # nopep8
url(r'^segmentio/event$', 'track.views.segmentio.segmentio_event'),
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
url(r'^accounts/login$', 'student.views.accounts_login', name="accounts_login"),
url(r'^accounts/login$', 'student.views.auth_with_no_login', name="auto_login"),
url(r'^accounts/manage_user_standing', 'student.views.manage_user_standing',
name='manage_user_standing'),
url(r'^accounts/disable_account_ajax$', 'student.views.disable_account_ajax',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment