Commit 9f6edd1e by William Desloge Committed by Bertrand Marron

Add IONISx auth platform

parent ccb2e42b
......@@ -291,6 +291,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,
......@@ -668,6 +672,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 = {}
......
......@@ -25,6 +25,9 @@ def run():
add_mimetypes()
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
enable_third_party_auth()
def add_mimetypes():
"""
......@@ -38,3 +41,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
......
......@@ -9,6 +9,7 @@ import time
import json
from collections import defaultdict
from pytz import UTC
from requests import request, ConnectionError
from django.conf import settings
from django.contrib.auth import logout, authenticate, login
......@@ -26,7 +27,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.http import require_POST, require_GET
......@@ -90,6 +91,7 @@ from util.password_policy_validators import (
)
from third_party_auth import pipeline, provider
from social.apps.django_app.default import models
from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import CourseRegistrationCode
......@@ -335,6 +337,7 @@ def signin_user(request):
"""
This view will display the non-modal login form
"""
if not settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
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
......@@ -361,13 +364,15 @@ def signin_user(request):
}
return render_to_response('login.html', context)
else:
return redirect("/auth/login/portal-oauth2/?auth_entry=login")
@ensure_csrf_cookie
def register_user(request, extra_context=None):
"""
This view will display the non-modal registration form
"""
if not settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
......@@ -410,6 +415,12 @@ def register_user(request, extra_context=None):
context.update(overrides)
return render_to_response('register.html', context)
else:
if request.user.is_authenticated():
if 'course_id' in request.GET:
return redirect("/courses/{0}/about".format(request.GET.get('course_id')))
else:
return redirect("/auth/login/portal-oauth2/?auth_entry=login&next={0}".format(urlquote(request.get_full_path())))
def complete_course_mode_info(course_id, enrollment):
......@@ -826,6 +837,12 @@ def _get_course_enrollment_domain(course_id):
@never_cache
@ensure_csrf_cookie
def auth_with_no_login(request):
return redirect("/auth/login/portal-oauth2/?auth_entry=login&next={0}".format(request.REQUEST.get('next', '')))
@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
......@@ -1075,6 +1092,17 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
}) # TODO: this should be status code 400 # pylint: disable=fixme
def logout_portal(user_mail):
try:
user = models.DjangoStorage.user.objects.get(uid=user_mail)
response = request('POST', settings.IONISX_AUTH['SYNC_LOGOUT_URL'],
params={ 'access_token': user.extra_data['access_token'] })
except ConnectionError as err:
log.warning(err)
except Exception as err:
log.warning(err)
@ensure_csrf_cookie
def logout_user(request):
"""
......@@ -1084,6 +1112,11 @@ def logout_user(request):
"""
# We do not log here, because we have a handler registered
# to perform logging on successful logouts.
if isinstance(request.user, AnonymousUser):
user_mail = None
else:
user_mail = request.user.email
logout(request)
if settings.FEATURES.get('AUTH_USE_CAS'):
target = reverse('cas-logout')
......@@ -1094,6 +1127,8 @@ def logout_user(request):
settings.EDXMKTG_COOKIE_NAME,
path='/', domain=settings.SESSION_COOKIE_DOMAIN,
)
if user_mail:
logout_portal(user_mail)
return response
......
"""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
log = logging.getLogger('third_party_auth.middleware')
class ExceptionMiddleware(SocialAuthExceptionMiddleware):
"""Custom middleware that handles conditional redirection."""
......@@ -16,3 +27,37 @@ class ExceptionMiddleware(SocialAuthExceptionMiddleware):
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)
class PortalSynchronizerMiddleware(object):
"""Custom middleware to synchronize user status of LMS with Portal provider."""
def process_request(self, request):
if not isinstance(request.user, AnonymousUser):
try:
email = request.user.email
user = models.DjangoStorage.user.objects.get(uid=email)
response = requests.request('POST', settings.IONISX_AUTH['SYNC_USER_URL'],
params={ 'access_token': user.extra_data['access_token'] })
response = response.json()
if response is None:
logout(request)
response = redirect(request.get_full_path())
response.delete_cookie(
settings.EDXMKTG_COOKIE_NAME,
path='/', domain=settings.SESSION_COOKIE_DOMAIN,
)
return response
if response['updated'] is True:
log.warning('need update !')
user = request.user
user.email = response['emails'][0]['email']
user.username = response['username']
user.save()
profile = UserProfile.objects.get(user=request.user)
profile.name = response['name']
profile.save()
except requests.ConnectionError as err:
log.warning(err)
except Exception as err:
log.warning(err)
......@@ -60,6 +60,7 @@ See http://psa.matiasaguirre.net/docs/pipeline.html for more docs.
import random
import string # pylint: disable-msg=deprecated-module
import analytics
import logging
from eventtracking import tracker
from django.contrib.auth.models import User
......@@ -68,9 +69,14 @@ 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
)
from . import provider
log = logging.getLogger(__file__)
AUTH_ENTRY_KEY = 'auth_entry'
AUTH_ENTRY_DASHBOARD = 'dashboard'
......@@ -338,6 +344,25 @@ def parse_query_params(strategy, response, *args, **kwargs):
}
def create_user_from_oauth(strategy, details, response, uid, is_dashboard=None, is_login=None, is_register=None, user=None, *args, **kwargs):
if 'is_new' in kwargs and kwargs['is_new'] is True:
user = User.objects.get(username=details['username'])
registration = Registration()
registration.register(user)
profile = UserProfile(user=user)
profile.name = details['fullname']
try:
profile.save()
except Exception:
log.exception("UserProfile creation failed for user {id}.".format(id=user.id))
raise
registration.activate()
registration.save()
create_comments_service_user(user)
@partial.partial
def redirect_to_supplementary_form(strategy, details, response, uid, is_dashboard=None, is_login=None, is_register=None, user=None, *args, **kwargs):
"""Dispatches user to views outside the pipeline if necessary."""
......@@ -367,6 +392,7 @@ def redirect_to_supplementary_form(strategy, details, response, uid, is_dashboar
if is_register and user_unset:
return redirect('/register', name='register_user')
@partial.partial
def login_analytics(*args, **kwargs):
event_name = None
......
from requests import HTTPError
from django.conf import settings
from social.backends.oauth import BaseOAuth2
from social.exceptions import AuthCanceled
class PortalOAuth2(BaseOAuth2):
"""Portal OAuth2 authentication backend"""
name = 'portal-oauth2'
auth_settings = settings.IONISX_AUTH
AUTHORIZATION_URL = auth_settings['AUTHORIZATION_URL']
ACCESS_TOKEN_URL = auth_settings['ACCESS_TOKEN_URL']
ACCESS_TOKEN_METHOD = 'POST'
REDIRECT_STATE = False
USER_DATA_URL = auth_settings['USER_DATA_URL']
def get_user_id(self, details, response):
"""Use portal email as unique id"""
if self.setting('USE_UNIQUE_USER_ID', False):
return response['id']
else:
return details['email']
def get_user_details(self, response):
"""Return user details from Portal account"""
return {'username': response.get('username', ''),
'email': response.get('emails', '')[0]['email'],
'fullname': response.get('name')}
def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service"""
params = self.setting('PROFILE_EXTRA_PARAMS', {})
params['access_token'] = access_token
return self.get_json(self.USER_DATA_URL, params=params)
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']
_MIDDLEWARE_CLASSES = (
'third_party_auth.middleware.ExceptionMiddleware',
)
_SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/dashboard'
_SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/'
def _merge_auth_info(django_settings, auth_info):
......@@ -104,12 +104,12 @@ def _set_global_settings(django_settings):
'social.pipeline.social_auth.auth_allowed',
'social.pipeline.social_auth.social_user',
'social.pipeline.user.get_username',
'third_party_auth.pipeline.redirect_to_supplementary_form',
'social.pipeline.user.create_user',
'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details',
'third_party_auth.pipeline.login_analytics',
'third_party_auth.pipeline.create_user_from_oauth',
)
# We let the user specify their email address during signup.
......
......@@ -446,6 +446,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'):
......
......@@ -199,96 +199,6 @@
</section>
% endif
<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 microsite.get_value('ENABLE_THIRD_PARTY_AUTH', settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH')):
<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.track_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