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