Commit 1451d0a2 by Carlos Andrés Rocha

[34078525] Added OPENID_PROVIDER_TRUSTED_ROOTS django setting

Added OPENID_PROVIDER_TRUSTED_ROOTS django setting. It should be a
list of glob matching patterns: ['*.cs50.net', '*.other.net']

Also did some minor refactoring and cleanup.
parent d77c4ca9
...@@ -4,6 +4,7 @@ import logging ...@@ -4,6 +4,7 @@ import logging
import random import random
import re import re
import string import string
import fnmatch
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
...@@ -37,21 +38,30 @@ import student.views as student_views ...@@ -37,21 +38,30 @@ import student.views as student_views
log = logging.getLogger("mitx.external_auth") log = logging.getLogger("mitx.external_auth")
# -----------------------------------------------------------------------------
# OpenID Common
# -----------------------------------------------------------------------------
@csrf_exempt @csrf_exempt
def default_render_failure(request, def default_render_failure(request,
message, message,
status=403, status=403,
template_name='extauth_failure.html', template_name='extauth_failure.html',
exception=None): exception=None):
"""Render an Openid error page to the user.""" """Render an Openid error page to the user"""
message = "In openid_failure " + message
log.debug(message) log.debug("In openid_failure " + message)
data = render_to_string(template_name, data = render_to_string(template_name,
dict(message=message, exception=exception)) dict(message=message, exception=exception))
return HttpResponse(data, status=status) return HttpResponse(data, status=status)
#-----------------------------------------------------------------------------
# Openid # -----------------------------------------------------------------------------
# OpenID Authentication
# -----------------------------------------------------------------------------
def edXauth_generate_password(length=12, chars=string.letters + string.digits): def edXauth_generate_password(length=12, chars=string.letters + string.digits):
...@@ -65,11 +75,7 @@ def edXauth_openid_login_complete(request, ...@@ -65,11 +75,7 @@ def edXauth_openid_login_complete(request,
render_failure=None): render_failure=None):
"""Complete the openid login process""" """Complete the openid login process"""
redirect_to = request.REQUEST.get(redirect_field_name, '') # TODO: [rocha] redirect_to never used? render_failure = (render_failure or default_render_failure)
render_failure = (render_failure or
getattr(settings, 'OPENID_RENDER_FAILURE', None) or
default_render_failure)
openid_response = openid_views.parse_openid_response(request) openid_response = openid_views.parse_openid_response(request)
if not openid_response: if not openid_response:
...@@ -83,7 +89,8 @@ def edXauth_openid_login_complete(request, ...@@ -83,7 +89,8 @@ def edXauth_openid_login_complete(request,
log.debug('openid success, details=%s' % details) log.debug('openid success, details=%s' % details)
external_domain = "openid:%s" % settings.OPENID_SSO_SERVER_URL url = getattr(settings, 'OPENID_SSO_SERVER_URL', None)
external_domain = "openid:%s" % url
fullname = '%s %s' % (details.get('first_name', ''), fullname = '%s %s' % (details.get('first_name', ''),
details.get('last_name', '')) details.get('last_name', ''))
...@@ -92,15 +99,11 @@ def edXauth_openid_login_complete(request, ...@@ -92,15 +99,11 @@ def edXauth_openid_login_complete(request,
external_domain, external_domain,
details, details,
details.get('email', ''), details.get('email', ''),
fullname, fullname)
)
return render_failure(request, 'Openid failure') return render_failure(request, 'Openid failure')
#-----------------------------------------------------------------------------
# generic external auth login or signup
def edXauth_external_login_or_signup(request, def edXauth_external_login_or_signup(request,
external_id, external_id,
external_domain, external_domain,
...@@ -108,29 +111,28 @@ def edXauth_external_login_or_signup(request, ...@@ -108,29 +111,28 @@ def edXauth_external_login_or_signup(request,
email, email,
fullname, fullname,
retfun=None): retfun=None):
"""Generic external auth login or signup"""
# see if we have a map from this external_id to an edX username # see if we have a map from this external_id to an edX username
try: try:
eamap = ExternalAuthMap.objects.get(external_id=external_id, eamap = ExternalAuthMap.objects.get(external_id=external_id,
external_domain=external_domain, external_domain=external_domain)
)
log.debug('Found eamap=%s' % eamap) log.debug('Found eamap=%s' % eamap)
except ExternalAuthMap.DoesNotExist: except ExternalAuthMap.DoesNotExist:
# go render form for creating edX user # go render form for creating edX user
eamap = ExternalAuthMap(external_id=external_id, eamap = ExternalAuthMap(external_id=external_id,
external_domain=external_domain, external_domain=external_domain,
external_credentials=json.dumps(credentials), external_credentials=json.dumps(credentials))
)
eamap.external_email = email eamap.external_email = email
eamap.external_name = fullname eamap.external_name = fullname
eamap.internal_password = edXauth_generate_password() eamap.internal_password = edXauth_generate_password()
log.debug('created eamap=%s' % eamap) log.debug('Created eamap=%s' % eamap)
eamap.save() eamap.save()
internal_user = eamap.user internal_user = eamap.user
if internal_user is None: if internal_user is None:
log.debug('ExtAuth: no user for %s yet, doing signup' % log.debug('No user for %s yet, doing signup' % eamap.external_email)
eamap.external_email)
return edXauth_signup(request, eamap) return edXauth_signup(request, eamap)
uname = internal_user.username uname = internal_user.username
...@@ -141,10 +143,10 @@ def edXauth_external_login_or_signup(request, ...@@ -141,10 +143,10 @@ def edXauth_external_login_or_signup(request,
return edXauth_signup(request, eamap) return edXauth_signup(request, eamap)
if not user.is_active: if not user.is_active:
log.warning("External Auth: user %s is not active" % (uname)) log.warning("User %s is not active" % (uname))
# TODO: improve error page # TODO: improve error page
msg = 'Account not yet activated: please look for link in your email' msg = 'Account not yet activated: please look for link in your email'
return render_failure(request, msg) # TODO: [rocha] render_failure not defined? return default_render_failure(request, msg)
login(request, user) login(request, user)
request.session.set_expiry(0) request.session.set_expiry(0)
...@@ -155,9 +157,6 @@ def edXauth_external_login_or_signup(request, ...@@ -155,9 +157,6 @@ def edXauth_external_login_or_signup(request,
return retfun() return retfun()
#-----------------------------------------------------------------------------
# generic external auth signup
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous
def edXauth_signup(request, eamap=None): def edXauth_signup(request, eamap=None):
...@@ -187,20 +186,22 @@ def edXauth_signup(request, eamap=None): ...@@ -187,20 +186,22 @@ def edXauth_signup(request, eamap=None):
'extauth_name': eamap.external_name, 'extauth_name': eamap.external_name,
} }
log.debug('ExtAuth: doing signup for %s' % eamap.external_email) log.debug('Doing signup for %s' % eamap.external_email)
return student_views.index(request, extra_context=context) return student_views.index(request, extra_context=context)
#----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# MIT SSL # MIT SSL
# -----------------------------------------------------------------------------
def ssl_dn_extract_info(dn): def ssl_dn_extract_info(dn):
''' """
Extract username, email address (may be anyuser@anydomain.com) and Extract username, email address (may be anyuser@anydomain.com) and
full name from the SSL DN string. Return (user,email,fullname) if full name from the SSL DN string. Return (user,email,fullname) if
successful, and None otherwise. successful, and None otherwise.
''' """
ss = re.search('/emailAddress=(.*)@([^/]+)', dn) ss = re.search('/emailAddress=(.*)@([^/]+)', dn)
if ss: if ss:
user = ss.group(1) user = ss.group(1)
...@@ -259,6 +260,11 @@ def edXauth_ssl_login(request): ...@@ -259,6 +260,11 @@ def edXauth_ssl_login(request):
retfun=retfun) retfun=retfun)
# -----------------------------------------------------------------------------
# OpenID Provider
# -----------------------------------------------------------------------------
def get_dict_for_openid(data): def get_dict_for_openid(data):
""" """
Return a dictionary suitable for the OpenID library Return a dictionary suitable for the OpenID library
...@@ -281,12 +287,7 @@ def get_xrds_url(resource, request): ...@@ -281,12 +287,7 @@ def get_xrds_url(resource, request):
return url return url
def provider_respond(server, request, response, data): def add_openid_simple_registration(request, response, data):
"""
Respond to an OpenID request
"""
# get simple registration request
sreg_data = {} sreg_data = {}
sreg_request = sreg.SRegRequest.fromOpenIDRequest(request) sreg_request = sreg.SRegRequest.fromOpenIDRequest(request)
sreg_fields = sreg_request.allRequestedFields() sreg_fields = sreg_request.allRequestedFields()
...@@ -304,27 +305,38 @@ def provider_respond(server, request, response, data): ...@@ -304,27 +305,38 @@ def provider_respond(server, request, response, data):
sreg_data) sreg_data)
sreg_response.toMessage(response.fields) sreg_response.toMessage(response.fields)
# get attribute exchange request
def add_openid_attribute_exchange(request, response, data):
try: try:
ax_request = ax.FetchRequest.fromOpenIDRequest(request) ax_request = ax.FetchRequest.fromOpenIDRequest(request)
except ax.AXError: except ax.AXError:
# not using OpenID attribute exchange extension
pass pass
else: else:
ax_response = ax.FetchResponse() ax_response = ax.FetchResponse()
# if consumer requested attribute exchange fields, add them # if consumer requested attribute exchange fields, add them
if ax_request and ax_request.requested_attributes: if ax_request and ax_request.requested_attributes:
for type_uri in ax_request.requested_attributes.iterkeys(): for type_uri in ax_request.requested_attributes.iterkeys():
if type_uri == 'http://axschema.org/contact/email' and 'email' in data: email_schema = 'http://axschema.org/contact/email'
ax_response.addValue('http://axschema.org/contact/email', data['email']) name_schema = 'http://axschema.org/namePerson'
elif type_uri == 'http://axschema.org/namePerson' and 'fullname' in data: if type_uri == email_schema and 'email' in data:
ax_response.addValue('http://axschema.org/namePerson', data['fullname']) ax_response.addValue(email_schema, data['email'])
elif type_uri == name_schema and 'fullname' in data:
ax_response.addValue(name_schema, data['fullname'])
# construct ax response # construct ax response
ax_response.toMessage(response.fields) ax_response.toMessage(response.fields)
def provider_respond(server, request, response, data):
"""
Respond to an OpenID request
"""
# get and add extensions
add_openid_simple_registration(request, response, data)
add_openid_attribute_exchange(request, response, data)
# create http response from OpenID response # create http response from OpenID response
webresponse = server.encodeResponse(response) webresponse = server.encodeResponse(response)
http_response = HttpResponse(webresponse.body) http_response = HttpResponse(webresponse.body)
...@@ -342,25 +354,44 @@ def validate_trust_root(openid_request): ...@@ -342,25 +354,44 @@ def validate_trust_root(openid_request):
Only allow OpenID requests from valid trust roots Only allow OpenID requests from valid trust roots
""" """
trusted_roots = getattr(settings, 'OPENID_PROVIDER_TRUSTED_ROOT', None)
if trusted_roots is None:
log.debug('not using trusted roots')
# not using trusted roots
return True
log.debug('validating trusted roots')
# don't allow empty trust roots # don't allow empty trust roots
if openid_request.trust_root is None: if (not hasattr(openid_request, 'trust_root') or
openid_request.trust_root is None):
log.debug('no trust_root')
return False return False
# ensure trust root parses cleanly (one wildcard, of form *.foo.com, etc.) # ensure trust root parses cleanly (one wildcard, of form *.foo.com, etc.)
trust_root = TrustRoot.parse(openid_request.trust_root) trust_root = TrustRoot.parse(openid_request.trust_root)
if trust_root is None: if trust_root is None:
log.debug('invalid trust_root')
return False return False
# don't allow empty return tos # don't allow empty return tos
if openid_request.return_to is None: if (not hasattr(openid_request, 'return_to') or
openid_request.return_to is None):
log.debug('empty return_to')
return False return False
# ensure return to is within trust root # ensure return to is within trust root
if not trust_root.validateURL(openid_request.return_to): if not trust_root.validateURL(openid_request.return_to):
log.debug('invalid return_to')
return False
# check that the root matches the ones we trust
if not any(r for r in trusted_roots if fnmatch.fnmatch(trust_root, r)):
log.debug('non-trusted root')
return False return False
# only allow *.cs50.net for now return True
return trust_root.host.endswith('cs50.net')
@csrf_exempt @csrf_exempt
...@@ -381,7 +412,7 @@ def provider_login(request): ...@@ -381,7 +412,7 @@ def provider_login(request):
# decode request # decode request
openid_request = server.decodeRequest(query) openid_request = server.decodeRequest(query)
# don't allow invalid and non-*.cs50.net trust roots # don't allow invalid and non-trusted trust roots
if not validate_trust_root(openid_request): if not validate_trust_root(openid_request):
return default_render_failure(request, "Invalid OpenID trust root") return default_render_failure(request, "Invalid OpenID trust root")
...@@ -420,8 +451,7 @@ def provider_login(request): ...@@ -420,8 +451,7 @@ def provider_login(request):
return default_render_failure(request, "Invalid OpenID trust root") return default_render_failure(request, "Invalid OpenID trust root")
# check if user with given email exists # check if user with given email exists
email = request.POST['email'] email = request.POST.get('email', None)
password = request.POST['password']
try: try:
user = User.objects.get(email=email) user = User.objects.get(email=email)
except User.DoesNotExist: except User.DoesNotExist:
...@@ -432,10 +462,12 @@ def provider_login(request): ...@@ -432,10 +462,12 @@ def provider_login(request):
# attempt to authenticate user # attempt to authenticate user
username = user.username username = user.username
password = request.POST.get('password', None)
user = authenticate(username=username, password=password) user = authenticate(username=username, password=password)
if user is None: if user is None:
request.session['openid_error'] = True request.session['openid_error'] = True
msg = "OpenID login failed - password for {0} is invalid".format(email) msg = "OpenID login failed - password for {0} is invalid"
msg = msg.format(email)
log.warning(msg) log.warning(msg)
return HttpResponseRedirect(openid_request['url']) return HttpResponseRedirect(openid_request['url'])
...@@ -451,7 +483,8 @@ def provider_login(request): ...@@ -451,7 +483,8 @@ def provider_login(request):
user.email)) user.email))
# redirect user to return_to location # redirect user to return_to location
response = openid_request['request'].answer(True, None, endpoint + urlquote(user.username)) url = endpoint + urlquote(user.username)
response = openid_request['request'].answer(True, None, url)
return provider_respond(server, return provider_respond(server,
openid_request['request'], openid_request['request'],
...@@ -462,13 +495,15 @@ def provider_login(request): ...@@ -462,13 +495,15 @@ def provider_login(request):
}) })
request.session['openid_error'] = True request.session['openid_error'] = True
log.warning("Login failed - Account not active for user {0}".format(username)) msg = "Login failed - Account not active for user {0}".format(username)
log.warning(msg)
return HttpResponseRedirect(openid_request['url']) return HttpResponseRedirect(openid_request['url'])
# determine consumer domain if applicable # determine consumer domain if applicable
return_to = '' return_to = ''
if 'openid.return_to' in request.REQUEST: if 'openid.return_to' in request.REQUEST:
matches = re.match(r'\w+:\/\/([\w\.-]+)', request.REQUEST['openid.return_to']) return_to = request.REQUEST['openid.return_to']
matches = re.match(r'\w+:\/\/([\w\.-]+)', return_to)
return_to = matches.group(1) return_to = matches.group(1)
# display login page # display login page
...@@ -487,9 +522,9 @@ def provider_identity(request): ...@@ -487,9 +522,9 @@ def provider_identity(request):
XRDS for identity discovery XRDS for identity discovery
""" """
response = render_to_response('identity.xml', { response = render_to_response('identity.xml',
'url': get_xrds_url('login', request) {'url': get_xrds_url('login', request)},
}, mimetype='text/xml') mimetype='text/xml')
# custom XRDS header necessary for discovery process # custom XRDS header necessary for discovery process
response['X-XRDS-Location'] = get_xrds_url('identity', request) response['X-XRDS-Location'] = get_xrds_url('identity', request)
...@@ -501,9 +536,9 @@ def provider_xrds(request): ...@@ -501,9 +536,9 @@ def provider_xrds(request):
XRDS for endpoint discovery XRDS for endpoint discovery
""" """
response = render_to_response('xrds.xml', { response = render_to_response('xrds.xml',
'url': get_xrds_url('login', request) {'url': get_xrds_url('login', request)},
}, mimetype='text/xml') mimetype='text/xml')
# custom XRDS header necessary for discovery process # custom XRDS header necessary for discovery process
response['X-XRDS-Location'] = get_xrds_url('xrds', request) response['X-XRDS-Location'] = get_xrds_url('xrds', request)
......
...@@ -120,6 +120,10 @@ node_paths = [COMMON_ROOT / "static/js/vendor", ...@@ -120,6 +120,10 @@ node_paths = [COMMON_ROOT / "static/js/vendor",
] ]
NODE_PATH = ':'.join(node_paths) NODE_PATH = ':'.join(node_paths)
############################ OpenID Provider ##################################
OPENID_PROVIDER_TRUSTED_ROOTS = ['cs50.net', '*.cs50.net']
################################## MITXWEB ##################################### ################################## MITXWEB #####################################
# This is where we stick our compiled template files. Most of the app uses Mako # This is where we stick our compiled template files. Most of the app uses Mako
# templates # templates
......
...@@ -17,7 +17,6 @@ MITX_FEATURES['DISABLE_START_DATES'] = True ...@@ -17,7 +17,6 @@ MITX_FEATURES['DISABLE_START_DATES'] = True
MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains--otherwise, want all courses to show up MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains--otherwise, want all courses to show up
MITX_FEATURES['SUBDOMAIN_BRANDING'] = True MITX_FEATURES['SUBDOMAIN_BRANDING'] = True
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
WIKI_ENABLED = True WIKI_ENABLED = True
...@@ -106,6 +105,7 @@ LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1'] ...@@ -106,6 +105,7 @@ LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
################################ OpenID Auth ################################# ################################ OpenID Auth #################################
MITX_FEATURES['AUTH_USE_OPENID'] = True MITX_FEATURES['AUTH_USE_OPENID'] = True
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True
INSTALLED_APPS += ('external_auth',) INSTALLED_APPS += ('external_auth',)
...@@ -116,6 +116,8 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True ...@@ -116,6 +116,8 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True
OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id' # TODO: accept more endpoints OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id' # TODO: accept more endpoints
OPENID_USE_AS_ADMIN_LOGIN = False OPENID_USE_AS_ADMIN_LOGIN = False
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
################################ MIT Certificates SSL Auth ################################# ################################ MIT Certificates SSL Auth #################################
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
......
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