Commit a97a9500 by Bertrand Marron

Add IONISx auth platform

parent a2a4e794
......@@ -11,6 +11,7 @@ This is the default template for our main set of AWS servers.
# and throws spurious errors. Therefore, we disable invalid-name checking.
# pylint: disable=invalid-name
import datetime
import json
from .common import *
......@@ -318,6 +319,27 @@ 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 ################################################
if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
AUTHENTICATION_BACKENDS = (
ENV_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [
'social.backends.google.GoogleOAuth2',
'social.backends.linkedin.LinkedinOAuth2',
'social.backends.facebook.FacebookOAuth2',
'third_party_auth.saml.SAMLAuthBackend'
]) + list(AUTHENTICATION_BACKENDS)
)
MIDDLEWARE_CLASSES += ('third_party_auth.middleware.PortalSynchronizerMiddleware',)
# The reduced session expiry time during the third party login pipeline. (Value in seconds)
SOCIAL_AUTH_PIPELINE_TIMEOUT = ENV_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600)
# third_party_auth config moved to ConfigurationModels. This is for data migration only:
THIRD_PARTY_AUTH_OLD_CONFIG = AUTH_TOKENS.get('THIRD_PARTY_AUTH', None)
IONISX_AUTH = AUTH_TOKENS.get('IONISX_AUTH')
##### ADVANCED_SECURITY_CONFIG #####
ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
......
......@@ -110,6 +110,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,
......@@ -891,6 +895,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 = {}
......
......@@ -24,6 +24,9 @@ def run():
if settings.FEATURES.get('USE_CUSTOM_THEME', False):
enable_theme()
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH', False):
enable_third_party_auth()
def add_mimetypes():
"""
......@@ -68,3 +71,14 @@ def enable_theme():
settings.STATICFILES_DIRS.append(
(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)
......@@ -58,6 +58,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'),
......@@ -70,8 +73,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(COURSELIKE_KEY_PATTERN), 'course_team_handler'),
......@@ -183,6 +184,12 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'):
'contentstore.views.certificates.certificates_list_handler')
)
# 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
......
......@@ -56,7 +56,18 @@ def login(request):
# is not handling the request.
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
# branding and allow that to process the login if it
# is enabled and the header is in the request.
......@@ -85,7 +96,18 @@ def register(request):
"""
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
# and registration is disabled.
response = external_auth.views.redirect_with_get('root', request.GET)
......
......@@ -1254,12 +1254,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'))
delete_logged_in_cookies(response)
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
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
......@@ -73,6 +74,9 @@ from social.apps.django_app.default import models
from social.exceptions import AuthException
from social.pipeline import partial
from social.pipeline.social_auth import associate_by_email
from student.models import (
Registration, UserProfile, create_comments_service_user
)
import student
......@@ -83,6 +87,7 @@ from . import provider
# 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
log = logging.getLogger(__file__)
# These are the query string params you can pass
# to the URL that starts the authentication process.
......@@ -441,7 +446,7 @@ def parse_query_params(strategy, response, *args, **kwargs):
"""Reads whitelisted query params, transforms them into pipeline args."""
auth_entry = strategy.session.get(AUTH_ENTRY_KEY)
if not (auth_entry and auth_entry in _AUTH_ENTRY_CHOICES):
raise AuthEntryError(strategy.request.backend, 'auth_entry missing or invalid')
auth_entry = AUTH_ENTRY_LOGIN
return {'auth_entry': auth_entry}
......@@ -476,6 +481,20 @@ def set_pipeline_timeout(strategy, user, *args, **kwargs):
# choice of the user.
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
def ensure_user_information(strategy, auth_entry, backend=None, user=None, social=None,
allow_inactive_user=False, *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'))
......@@ -52,8 +52,12 @@ def apply_settings(django_settings):
'third_party_auth.pipeline.associate_by_email_if_login_api',
'social.pipeline.user.get_username',
'third_party_auth.pipeline.set_pipeline_timeout',
'third_party_auth.pipeline.ensure_user_information',
# IONISx: Disable as we always have enough information. :)
# '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',
......
......@@ -548,6 +548,8 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
]) + list(AUTHENTICATION_BACKENDS)
)
MIDDLEWARE_CLASSES += ('third_party_auth.middleware.PortalSynchronizerMiddleware',)
# The reduced session expiry time during the third party login pipeline. (Value in seconds)
SOCIAL_AUTH_PIPELINE_TIMEOUT = ENV_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600)
......@@ -560,6 +562,8 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
'schedule': datetime.timedelta(hours=ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24)),
}
IONISX_AUTH = AUTH_TOKENS.get('IONISX_AUTH')
##### OAUTH2 Provider ##############
if FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
OAUTH_OIDC_ISSUER = ENV_TOKENS['OAUTH_OIDC_ISSUER']
......
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