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. ...@@ -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. # and throws spurious errors. Therefore, we disable invalid-name checking.
# pylint: disable=invalid-name # pylint: disable=invalid-name
import datetime
import json import json
from .common import * from .common import *
...@@ -318,6 +319,27 @@ SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIME ...@@ -318,6 +319,27 @@ 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 ################################################
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 #####
ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {}) ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
......
...@@ -110,6 +110,10 @@ FEATURES = { ...@@ -110,6 +110,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,
...@@ -891,6 +895,10 @@ for app_name in OPTIONAL_APPS: ...@@ -891,6 +895,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)
...@@ -58,6 +58,9 @@ urlpatterns += patterns( ...@@ -58,6 +58,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'),
...@@ -70,8 +73,6 @@ urlpatterns += patterns( ...@@ -70,8 +73,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'),
...@@ -183,6 +184,12 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'): ...@@ -183,6 +184,12 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'):
'contentstore.views.certificates.certificates_list_handler') '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: 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)
......
...@@ -1254,12 +1254,7 @@ def logout_user(request): ...@@ -1254,12 +1254,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)
delete_logged_in_cookies(response) delete_logged_in_cookies(response)
return response return response
......
"""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()
...@@ -62,6 +62,7 @@ import string # pylint: disable-msg=deprecated-module ...@@ -62,6 +62,7 @@ import string # pylint: disable-msg=deprecated-module
from collections import OrderedDict from collections import OrderedDict
import urllib import urllib
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
...@@ -83,6 +87,7 @@ from . import provider ...@@ -83,6 +87,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.
...@@ -441,7 +446,7 @@ def parse_query_params(strategy, response, *args, **kwargs): ...@@ -441,7 +446,7 @@ 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.request.backend, 'auth_entry missing or invalid') auth_entry = AUTH_ENTRY_LOGIN
return {'auth_entry': auth_entry} return {'auth_entry': auth_entry}
...@@ -476,6 +481,20 @@ def set_pipeline_timeout(strategy, user, *args, **kwargs): ...@@ -476,6 +481,20 @@ def set_pipeline_timeout(strategy, user, *args, **kwargs):
# choice of the user. # 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 @partial.partial
def ensure_user_information(strategy, auth_entry, backend=None, user=None, social=None, def ensure_user_information(strategy, auth_entry, backend=None, user=None, social=None,
allow_inactive_user=False, *args, **kwargs): 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): ...@@ -52,8 +52,12 @@ def apply_settings(django_settings):
'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.set_pipeline_timeout', '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', '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',
......
...@@ -548,6 +548,8 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): ...@@ -548,6 +548,8 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
]) + list(AUTHENTICATION_BACKENDS) ]) + list(AUTHENTICATION_BACKENDS)
) )
MIDDLEWARE_CLASSES += ('third_party_auth.middleware.PortalSynchronizerMiddleware',)
# The reduced session expiry time during the third party login pipeline. (Value in seconds) # 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) SOCIAL_AUTH_PIPELINE_TIMEOUT = ENV_TOKENS.get('SOCIAL_AUTH_PIPELINE_TIMEOUT', 600)
...@@ -560,6 +562,8 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): ...@@ -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)), 'schedule': datetime.timedelta(hours=ENV_TOKENS.get('THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS', 24)),
} }
IONISX_AUTH = AUTH_TOKENS.get('IONISX_AUTH')
##### OAUTH2 Provider ############## ##### OAUTH2 Provider ##############
if FEATURES.get('ENABLE_OAUTH2_PROVIDER'): if FEATURES.get('ENABLE_OAUTH2_PROVIDER'):
OAUTH_OIDC_ISSUER = ENV_TOKENS['OAUTH_OIDC_ISSUER'] 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