Commit 3375cd3d by Bertrand Marron

Add IONISx auth platform

parent 6b89fafc
...@@ -304,6 +304,12 @@ SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIME ...@@ -304,6 +304,12 @@ SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIME
##### X-Frame-Options response header settings ##### ##### X-Frame-Options response header settings #####
X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS) 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 #####
ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {}) ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
......
...@@ -101,6 +101,10 @@ FEATURES = { ...@@ -101,6 +101,10 @@ FEATURES = {
# Turn on/off Microsites feature # Turn on/off Microsites feature
'USE_MICROSITES': False, '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 creating courses with non-ascii characters in the course id
'ALLOW_UNICODE_COURSE_ID': False, 'ALLOW_UNICODE_COURSE_ID': False,
...@@ -830,6 +834,10 @@ for app_name in OPTIONAL_APPS: ...@@ -830,6 +834,10 @@ for app_name in OPTIONAL_APPS:
continue continue
INSTALLED_APPS += (app_name,) 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 ### ADVANCED_SECURITY_CONFIG
# Empty by default # Empty by default
ADVANCED_SECURITY_CONFIG = {} ADVANCED_SECURITY_CONFIG = {}
......
...@@ -24,6 +24,9 @@ def run(): ...@@ -24,6 +24,9 @@ def run():
if settings.FEATURES.get('USE_CUSTOM_THEME', False): if settings.FEATURES.get('USE_CUSTOM_THEME', False):
enable_theme() enable_theme()
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
enable_third_party_auth()
def add_mimetypes(): def add_mimetypes():
""" """
...@@ -68,3 +71,14 @@ def enable_theme(): ...@@ -68,3 +71,14 @@ def enable_theme():
settings.STATICFILES_DIRS.append( settings.STATICFILES_DIRS.append(
(u'themes/{}'.format(settings.THEME_NAME), theme_root / 'static') (u'themes/{}'.format(settings.THEME_NAME), theme_root / 'static')
) )
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)
...@@ -59,6 +59,9 @@ urlpatterns += patterns( ...@@ -59,6 +59,9 @@ urlpatterns += patterns(
url(r'^create_account$', 'student.views.create_account', name='create_account'), url(r'^create_account$', 'student.views.create_account', name='create_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name='activate'), 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 # ajax view that actually does the work
url(r'^login_post$', 'student.views.login_user', name='login_post'), url(r'^login_post$', 'student.views.login_user', name='login_post'),
...@@ -71,8 +74,6 @@ urlpatterns += patterns( ...@@ -71,8 +74,6 @@ urlpatterns += patterns(
url(r'^$', 'howitworks', name='homepage'), url(r'^$', 'howitworks', name='homepage'),
url(r'^howitworks$', 'howitworks'), 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'^request_course_creator$', 'request_course_creator'),
url(r'^course_team/{}(?:/(?P<email>.+))?$'.format(COURSELIKE_KEY_PATTERN), 'course_team_handler'), url(r'^course_team/{}(?:/(?P<email>.+))?$'.format(COURSELIKE_KEY_PATTERN), 'course_team_handler'),
...@@ -172,6 +173,12 @@ if settings.FEATURES.get('ENTRANCE_EXAMS'): ...@@ -172,6 +173,12 @@ if settings.FEATURES.get('ENTRANCE_EXAMS'):
url(r'^course/{}/entrance_exam/?$'.format(settings.COURSE_KEY_PATTERN), 'contentstore.views.entrance_exam'), url(r'^course/{}/entrance_exam/?$'.format(settings.COURSE_KEY_PATTERN), 'contentstore.views.entrance_exam'),
) )
# Third-party auth.
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
urlpatterns += (
url(r'', include('third_party_auth.urls')),
)
if settings.DEBUG: if settings.DEBUG:
try: try:
from .urls_dev import urlpatterns as dev_urlpatterns from .urls_dev import urlpatterns as dev_urlpatterns
......
...@@ -56,7 +56,18 @@ def login(request): ...@@ -56,7 +56,18 @@ def login(request):
# is not handling the request. # is not handling the request.
response = None response = None
if settings.FEATURES['AUTH_USE_CERTIFICATES'] and external_auth.views.ssl_get_cert_from_request(request): 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',))
response = external_auth.views.redirect_with_get(redirect_uri, get, do_reverse=False)
elif settings.FEATURES['AUTH_USE_CERTIFICATES'] and external_auth.views.ssl_get_cert_from_request(request):
# SSL login doesn't require a view, so redirect # SSL login doesn't require a view, so redirect
# branding and allow that to process the login if it # branding and allow that to process the login if it
# is enabled and the header is in the request. # is enabled and the header is in the request.
...@@ -85,7 +96,18 @@ def register(request): ...@@ -85,7 +96,18 @@ def register(request):
""" """
response = None response = None
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'): 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',))
response = external_auth.views.redirect_with_get(redirect_uri, get, do_reverse=False)
elif settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to branding to process their certificate if SSL is enabled # Redirect to branding to process their certificate if SSL is enabled
# and registration is disabled. # and registration is disabled.
response = external_auth.views.redirect_with_get('root', request.GET) response = external_auth.views.redirect_with_get('root', request.GET)
......
...@@ -1205,11 +1205,7 @@ def logout_user(request): ...@@ -1205,11 +1205,7 @@ def logout_user(request):
# We do not log here, because we have a handler registered # We do not log here, because we have a handler registered
# to perform logging on successful logouts. # to perform logging on successful logouts.
logout(request) logout(request)
if settings.FEATURES.get('AUTH_USE_CAS'): response = redirect(settings.IONISX_AUTH.get('LOGOUT_URL'))
target = reverse('cas-logout')
else:
target = '/'
response = redirect(target)
response.delete_cookie( response.delete_cookie(
settings.EDXMKTG_COOKIE_NAME, settings.EDXMKTG_COOKIE_NAME,
path='/', domain=settings.SESSION_COOKIE_DOMAIN, path='/', domain=settings.SESSION_COOKIE_DOMAIN,
......
"""Middleware classes for third_party_auth.""" """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 social.apps.django_app.middleware import SocialAuthExceptionMiddleware
from . import pipeline from . import pipeline
from . import portal
log = logging.getLogger(__file__)
class ExceptionMiddleware(SocialAuthExceptionMiddleware): class ExceptionMiddleware(SocialAuthExceptionMiddleware):
"""Custom middleware that handles conditional redirection.""" """Custom middleware that handles conditional redirection."""
...@@ -23,3 +35,49 @@ class ExceptionMiddleware(SocialAuthExceptionMiddleware): ...@@ -23,3 +35,49 @@ class ExceptionMiddleware(SocialAuthExceptionMiddleware):
redirect_uri = pipeline.AUTH_DISPATCH_URLS[auth_entry] redirect_uri = pipeline.AUTH_DISPATCH_URLS[auth_entry]
return redirect_uri 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()
...@@ -63,6 +63,7 @@ from collections import OrderedDict ...@@ -63,6 +63,7 @@ from collections import OrderedDict
import urllib import urllib
from ipware.ip import get_ip from ipware.ip import get_ip
import analytics import analytics
import logging
from eventtracking import tracker from eventtracking import tracker
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -73,6 +74,9 @@ from social.apps.django_app.default import models ...@@ -73,6 +74,9 @@ from social.apps.django_app.default import models
from social.exceptions import AuthException from social.exceptions import AuthException
from social.pipeline import partial from social.pipeline import partial
from social.pipeline.social_auth import associate_by_email from social.pipeline.social_auth import associate_by_email
from student.models import (
Registration, UserProfile, create_comments_service_user
)
import student import student
from embargo import api as embargo_api from embargo import api as embargo_api
...@@ -93,6 +97,7 @@ from . import provider ...@@ -93,6 +97,7 @@ from . import provider
# Note that this lives in openedx, so this dependency should be refactored. # Note that this lives in openedx, so this dependency should be refactored.
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
log = logging.getLogger(__file__)
# These are the query string params you can pass # These are the query string params you can pass
# to the URL that starts the authentication process. # to the URL that starts the authentication process.
...@@ -445,11 +450,26 @@ def parse_query_params(strategy, response, *args, **kwargs): ...@@ -445,11 +450,26 @@ def parse_query_params(strategy, response, *args, **kwargs):
"""Reads whitelisted query params, transforms them into pipeline args.""" """Reads whitelisted query params, transforms them into pipeline args."""
auth_entry = strategy.session.get(AUTH_ENTRY_KEY) auth_entry = strategy.session.get(AUTH_ENTRY_KEY)
if not (auth_entry and auth_entry in _AUTH_ENTRY_CHOICES): if not (auth_entry and auth_entry in _AUTH_ENTRY_CHOICES):
raise AuthEntryError(strategy.backend, 'auth_entry missing or invalid') # raise AuthEntryError(strategy.backend, 'auth_entry missing or invalid')
auth_entry = AUTH_ENTRY_LOGIN
return {'auth_entry': auth_entry} return {'auth_entry': auth_entry}
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)
@partial.partial @partial.partial
def ensure_user_information(strategy, auth_entry, user=None, *args, **kwargs): def ensure_user_information(strategy, auth_entry, user=None, *args, **kwargs):
""" """
......
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. ...@@ -5,6 +5,7 @@ invoke the Django armature.
""" """
from social.backends import google, linkedin, facebook from social.backends import google, linkedin, facebook
from . import portal
_DEFAULT_ICON_CLASS = 'fa-signin' _DEFAULT_ICON_CLASS = 'fa-signin'
...@@ -170,6 +171,26 @@ class FacebookOauth2(BaseProvider): ...@@ -170,6 +171,26 @@ class FacebookOauth2(BaseProvider):
return provider_details.get('fullname') 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): class Registry(object):
"""Singleton registry of third-party auth providers. """Singleton registry of third-party auth providers.
......
...@@ -105,8 +105,8 @@ def _set_global_settings(django_settings): ...@@ -105,8 +105,8 @@ def _set_global_settings(django_settings):
'social.pipeline.social_auth.social_user', 'social.pipeline.social_auth.social_user',
'third_party_auth.pipeline.associate_by_email_if_login_api', 'third_party_auth.pipeline.associate_by_email_if_login_api',
'social.pipeline.user.get_username', 'social.pipeline.user.get_username',
'third_party_auth.pipeline.ensure_user_information',
'social.pipeline.user.create_user', 'social.pipeline.user.create_user',
'third_party_auth.pipeline.create_user_from_oauth',
'social.pipeline.social_auth.associate_user', 'social.pipeline.social_auth.associate_user',
'social.pipeline.social_auth.load_extra_data', 'social.pipeline.social_auth.load_extra_data',
'social.pipeline.user.user_details', 'social.pipeline.user.user_details',
......
...@@ -497,6 +497,10 @@ X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS) ...@@ -497,6 +497,10 @@ X_FRAME_OPTIONS = ENV_TOKENS.get('X_FRAME_OPTIONS', X_FRAME_OPTIONS)
##### Third-party auth options ################################################ ##### Third-party auth options ################################################
THIRD_PARTY_AUTH = AUTH_TOKENS.get('THIRD_PARTY_AUTH', THIRD_PARTY_AUTH) 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 ############## ##### OAUTH2 Provider ##############
if FEATURES.get('ENABLE_OAUTH2_PROVIDER'): if FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
......
...@@ -74,106 +74,6 @@ ...@@ -74,106 +74,6 @@
</div> </div>
<section class="container dashboard" id="dashboard-main"> <section class="container dashboard" id="dashboard-main">
<section class="profile-sidebar">
<header class="profile">
<h2><span class="sr">${_("Username")}: </span><span class="user-name">${ user.username }</span></h2>
</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 fa fa-link"></i> <span class="copy">${_("Linked")}</span>
% else:
<i class="icon fa fa-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, redirect_url='/')}">
## 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 len(order_history_list):
<li class="order-history">
<span class="title">${_("Order History")}</span>
% for order_history_item in order_history_list:
<span><a href="${order_history_item['receipt_url']}" target="_blank" class="edit-name">${order_history_item['order_date']}</a></span>
% endfor
</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 id="my-courses" class="my-courses" role="main" aria-label="Content"> <section id="my-courses" class="my-courses" role="main" aria-label="Content">
<header> <header>
<h2>${_("Current Courses")}</h2> <h2>${_("Current Courses")}</h2>
......
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