Commit cb67af59 by Carlos Andrés Rocha

[34078525] OpenID provider cleanup and minor fixes

parent 4126f3a2
...@@ -9,17 +9,12 @@ from external_auth.models import ExternalAuthMap ...@@ -9,17 +9,12 @@ from external_auth.models import ExternalAuthMap
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
from django.contrib.auth.models import Group
from django.contrib.auth.models import User from django.contrib.auth.models import User
from student.models import UserProfile from student.models import UserProfile
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.utils.http import urlquote from django.utils.http import urlquote
from django.shortcuts import render_to_response
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import RequestContext
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
try: try:
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
...@@ -27,84 +22,103 @@ except ImportError: ...@@ -27,84 +22,103 @@ except ImportError:
from django.contrib.csrf.middleware import csrf_exempt from django.contrib.csrf.middleware import csrf_exempt
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from util.cache import cache_if_anonymous from util.cache import cache_if_anonymous
from django_openid_auth import auth as openid_auth
from openid.consumer.consumer import (Consumer, SUCCESS, CANCEL, FAILURE)
import django_openid_auth.views as openid_views import django_openid_auth.views as openid_views
from django_openid_auth import auth as openid_auth
from openid.consumer.consumer import SUCCESS
from openid.server.server import Server, ProtocolError, CheckIDRequest, EncodingError from openid.server.server import Server
from openid.server.trustroot import TrustRoot from openid.server.trustroot import TrustRoot
from openid.store.filestore import FileOpenIDStore from openid.store.filestore import FileOpenIDStore
from openid.yadis.discover import DiscoveryFailure
from openid.consumer.discover import OPENID_IDP_2_0_TYPE
from openid.extensions import ax, sreg from openid.extensions import ax, sreg
from openid.fetchers import HTTPFetchingError
import student.views as student_views import student.views as student_views
log = logging.getLogger("mitx.external_auth") log = logging.getLogger("mitx.external_auth")
@csrf_exempt @csrf_exempt
def default_render_failure(request, message, status=403, template_name='extauth_failure.html', exception=None): def default_render_failure(request,
message,
status=403,
template_name='extauth_failure.html',
exception=None):
"""Render an Openid error page to the user.""" """Render an Openid error page to the user."""
message = "In openid_failure " + message message = "In openid_failure " + message
log.debug(message) log.debug(message)
data = render_to_string( template_name, dict(message=message, exception=exception)) data = render_to_string(template_name,
dict(message=message, exception=exception))
return HttpResponse(data, status=status) return HttpResponse(data, status=status)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Openid # Openid
def edXauth_generate_password(length=12, chars=string.letters + string.digits): def edXauth_generate_password(length=12, chars=string.letters + string.digits):
"""Generate internal password for externally authenticated user""" """Generate internal password for externally authenticated user"""
return ''.join([random.choice(chars) for i in range(length)]) return ''.join([random.choice(chars) for i in range(length)])
@csrf_exempt @csrf_exempt
def edXauth_openid_login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME, render_failure=None): def edXauth_openid_login_complete(request,
redirect_field_name=REDIRECT_FIELD_NAME,
render_failure=None):
"""Complete the openid login process""" """Complete the openid login process"""
redirect_to = request.REQUEST.get(redirect_field_name, '') 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 \ render_failure = (render_failure or
default_render_failure 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:
return render_failure(request, 'This is an OpenID relying party endpoint.') return render_failure(request,
'This is an OpenID relying party endpoint.')
if openid_response.status == SUCCESS: if openid_response.status == SUCCESS:
external_id = openid_response.identity_url external_id = openid_response.identity_url
oid_backend = openid_auth.OpenIDBackend() oid_backend = openid_auth.OpenIDBackend()
details = oid_backend._extract_user_details(openid_response) details = oid_backend._extract_user_details(openid_response)
log.debug('openid success, details=%s' % details) log.debug('openid success, details=%s' % details)
external_domain = "openid:%s" % settings.OPENID_SSO_SERVER_URL
fullname = '%s %s' % (details.get('first_name', ''),
details.get('last_name', ''))
return edXauth_external_login_or_signup(request, return edXauth_external_login_or_signup(request,
external_id, external_id,
"openid:%s" % settings.OPENID_SSO_SERVER_URL, external_domain,
details, details,
details.get('email',''), details.get('email', ''),
'%s %s' % (details.get('first_name',''),details.get('last_name','')) fullname,
) )
return render_failure(request, 'Openid failure') return render_failure(request, 'Openid failure')
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# generic external auth login or signup # generic external auth login or signup
def edXauth_external_login_or_signup(request, external_id, external_domain, credentials, email, fullname, def edXauth_external_login_or_signup(request,
external_id,
external_domain,
credentials,
email,
fullname,
retfun=None): retfun=None):
# 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
...@@ -115,20 +129,23 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred ...@@ -115,20 +129,23 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred
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' % eamap.external_email) log.debug('ExtAuth: no user for %s yet, doing signup' %
eamap.external_email)
return edXauth_signup(request, eamap) return edXauth_signup(request, eamap)
uname = internal_user.username uname = internal_user.username
user = authenticate(username=uname, password=eamap.internal_password) user = authenticate(username=uname, password=eamap.internal_password)
if user is None: if user is None:
log.warning("External Auth Login failed for %s / %s" % (uname,eamap.internal_password)) log.warning("External Auth Login failed for %s / %s" %
(uname, eamap.internal_password))
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("External Auth: user %s is not active" % (uname))
# TODO: improve error page # TODO: improve error page
return render_failure(request, '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?
login(request, user) login(request, user)
request.session.set_expiry(0) request.session.set_expiry(0)
student_views.try_change_enrollment(request) student_views.try_change_enrollment(request)
...@@ -136,8 +153,8 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred ...@@ -136,8 +153,8 @@ def edXauth_external_login_or_signup(request, external_id, external_domain, cred
if retfun is None: if retfun is None:
return redirect('/') return redirect('/')
return retfun() return retfun()
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# generic external auth signup # generic external auth signup
...@@ -153,31 +170,36 @@ def edXauth_signup(request, eamap=None): ...@@ -153,31 +170,36 @@ def edXauth_signup(request, eamap=None):
eamap is an ExteralAuthMap object, specifying the external user eamap is an ExteralAuthMap object, specifying the external user
for which to complete the signup. for which to complete the signup.
""" """
if eamap is None: if eamap is None:
pass pass
request.session['ExternalAuthMap'] = eamap # save this for use by student.views.create_account # save this for use by student.views.create_account
request.session['ExternalAuthMap'] = eamap
# default conjoin name, no spaces
username = eamap.external_name.replace(' ', '')
context = {'has_extauth_info': True, context = {'has_extauth_info': True,
'show_signup_immediately' : True, 'show_signup_immediately': True,
'extauth_email': eamap.external_email, 'extauth_email': eamap.external_email,
'extauth_username' : eamap.external_name.replace(' ',''), # default - conjoin name, no spaces 'extauth_username': username,
'extauth_name': eamap.external_name, 'extauth_name': eamap.external_name,
} }
log.debug('ExtAuth: doing signup for %s' % eamap.external_email) log.debug('ExtAuth: 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 full name Extract username, email address (may be anyuser@anydomain.com) and
from the SSL DN string. Return (user,email,fullname) if successful, and None full name from the SSL DN string. Return (user,email,fullname) if
otherwise. successful, and None otherwise.
''' '''
ss = re.search('/emailAddress=(.*)@([^/]+)', dn) ss = re.search('/emailAddress=(.*)@([^/]+)', dn)
if ss: if ss:
...@@ -192,43 +214,50 @@ def ssl_dn_extract_info(dn): ...@@ -192,43 +214,50 @@ def ssl_dn_extract_info(dn):
return None return None
return (user, email, fullname) return (user, email, fullname)
@csrf_exempt @csrf_exempt
def edXauth_ssl_login(request): def edXauth_ssl_login(request):
""" """
This is called by student.views.index when MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True This is called by student.views.index when
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
Used for MIT user authentication. This presumes the web server (nginx) has been configured Used for MIT user authentication. This presumes the web server
to require specific client certificates. (nginx) has been configured to require specific client
certificates.
If the incoming protocol is HTTPS (SSL) then authenticate via client certificate. If the incoming protocol is HTTPS (SSL) then authenticate via
The certificate provides user email and fullname; this populates the ExternalAuthMap. client certificate. The certificate provides user email and
The user is nevertheless still asked to complete the edX signup. fullname; this populates the ExternalAuthMap. The user is
nevertheless still asked to complete the edX signup.
Else continues on with student.views.index, and no authentication. Else continues on with student.views.index, and no authentication.
""" """
certkey = "SSL_CLIENT_S_DN" # specify the request.META field to use certkey = "SSL_CLIENT_S_DN" # specify the request.META field to use
cert = request.META.get(certkey,'') cert = request.META.get(certkey, '')
if not cert: if not cert:
cert = request.META.get('HTTP_'+certkey,'') cert = request.META.get('HTTP_' + certkey, '')
if not cert: if not cert:
try: try:
cert = request._req.subprocess_env.get(certkey,'') # try the direct apache2 SSL key # try the direct apache2 SSL key
except Exception as err: cert = request._req.subprocess_env.get(certkey, '')
except Exception:
pass pass
if not cert: if not cert:
# no certificate information - go onward to main index # no certificate information - go onward to main index
return student_views.index(request) return student_views.index(request)
(user, email, fullname) = ssl_dn_extract_info(cert) (user, email, fullname) = ssl_dn_extract_info(cert)
return edXauth_external_login_or_signup(request, retfun = functools.partial(student_views.index, request)
external_id=email, return edXauth_external_login_or_signup(request,
external_domain="ssl:MIT", external_id=email,
credentials=cert, external_domain="ssl:MIT",
credentials=cert,
email=email, email=email,
fullname=fullname, fullname=fullname,
retfun = functools.partial(student_views.index, request)) retfun=retfun)
def get_dict_for_openid(data): def get_dict_for_openid(data):
""" """
...@@ -237,6 +266,7 @@ def get_dict_for_openid(data): ...@@ -237,6 +266,7 @@ def get_dict_for_openid(data):
return dict((k, v) for k, v in data.iteritems()) return dict((k, v) for k, v in data.iteritems())
def get_xrds_url(resource, request): def get_xrds_url(resource, request):
""" """
Return the XRDS url for a resource Return the XRDS url for a resource
...@@ -250,12 +280,13 @@ def get_xrds_url(resource, request): ...@@ -250,12 +280,13 @@ def get_xrds_url(resource, request):
return url return url
def provider_respond(server, request, response, data): def provider_respond(server, request, response, data):
""" """
Respond to an OpenID request Respond to an OpenID request
""" """
# get simple registration 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()
...@@ -269,7 +300,8 @@ def provider_respond(server, request, response, data): ...@@ -269,7 +300,8 @@ def provider_respond(server, request, response, data):
sreg_data['fullname'] = data['fullname'] sreg_data['fullname'] = data['fullname']
# construct sreg response # construct sreg response
sreg_response = sreg.SRegResponse.extractResponse(sreg_request, sreg_data) sreg_response = sreg.SRegResponse.extractResponse(sreg_request,
sreg_data)
sreg_response.toMessage(response.fields) sreg_response.toMessage(response.fields)
# get attribute exchange request # get attribute exchange request
...@@ -287,9 +319,8 @@ def provider_respond(server, request, response, data): ...@@ -287,9 +319,8 @@ def provider_respond(server, request, response, data):
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: if type_uri == 'http://axschema.org/contact/email' and 'email' in data:
ax_response.addValue('http://axschema.org/contact/email', data['email']) ax_response.addValue('http://axschema.org/contact/email', data['email'])
elif type_uri == 'http://axschema.org/namePerson' and 'fullname' in data: elif type_uri == 'http://axschema.org/namePerson' and 'fullname' in data:
ax_response.addValue('http://axschema.org/namePerson', data['fullname']); ax_response.addValue('http://axschema.org/namePerson', data['fullname'])
# construct ax response # construct ax response
ax_response.toMessage(response.fields) ax_response.toMessage(response.fields)
...@@ -313,24 +344,24 @@ def validate_trust_root(openid_request): ...@@ -313,24 +344,24 @@ def validate_trust_root(openid_request):
# verify the trust root/return to # verify the trust root/return to
trust_root = openid_request.trust_root trust_root = openid_request.trust_root
return_to = openid_request.return_to return_to = openid_request.return_to # TODO: [rocha] never used?
# don't allow empty trust roots # don't allow empty trust roots
if openid_request.trust_root is None: if openid_request.trust_root is None:
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:
return false return False
# don't allow empty return tos # don't allow empty return tos
if openid_request.return_to is None: if openid_request.return_to is None:
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):
return false return False
# only allow *.cs50.net for now # only allow *.cs50.net for now
return trust_root.host.endswith('cs50.net') return trust_root.host.endswith('cs50.net')
...@@ -356,11 +387,12 @@ def provider_login(request): ...@@ -356,11 +387,12 @@ def provider_login(request):
# don't allow invalid and non-*.cs50.net trust roots # don't allow invalid and non-*.cs50.net 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")
# checkid_immediate not supported, require user interaction # checkid_immediate not supported, require user interaction
if openid_request.mode == 'checkid_immediate': if openid_request.mode == 'checkid_immediate':
return provider_respond(server, openid_request, openid_request.answer(false), {}) return provider_respond(server, openid_request,
openid_request.answer(False), {})
# checkid_setup, so display login page # checkid_setup, so display login page
elif openid_request.mode == 'checkid_setup': elif openid_request.mode == 'checkid_setup':
...@@ -378,7 +410,8 @@ def provider_login(request): ...@@ -378,7 +410,8 @@ def provider_login(request):
# OpenID response # OpenID response
else: else:
return provider_respond(server, openid_request, server.handleRequest(openid_request), {}) return provider_respond(server, openid_request,
server.handleRequest(openid_request), {})
# handle login # handle login
if request.method == 'POST' and 'openid_request' in request.session: if request.method == 'POST' and 'openid_request' in request.session:
...@@ -388,7 +421,7 @@ def provider_login(request): ...@@ -388,7 +421,7 @@ def provider_login(request):
# don't allow invalid and non-*.cs50.net trust roots # don't allow invalid and non-*.cs50.net 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")
# check if user with given email exists # check if user with given email exists
email = request.POST['email'] email = request.POST['email']
...@@ -397,7 +430,8 @@ def provider_login(request): ...@@ -397,7 +430,8 @@ def provider_login(request):
user = User.objects.get(email=email) user = User.objects.get(email=email)
except User.DoesNotExist: except User.DoesNotExist:
request.session['openid_error'] = True request.session['openid_error'] = True
log.warning("OpenID login failed - Unknown user email: {0}".format(email)) msg = "OpenID login failed - Unknown user email: {0}".format(email)
log.warning(msg)
return HttpResponseRedirect(openid_request['url']) return HttpResponseRedirect(openid_request['url'])
# attempt to authenticate user # attempt to authenticate user
...@@ -405,7 +439,8 @@ def provider_login(request): ...@@ -405,7 +439,8 @@ def provider_login(request):
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
log.warning("OpenID login failed - password for {0} is invalid".format(email)) msg = "OpenID login failed - password for {0} is invalid".format(email)
log.warning(msg)
return HttpResponseRedirect(openid_request['url']) return HttpResponseRedirect(openid_request['url'])
# authentication succeeded, so log user in # authentication succeeded, so log user in
...@@ -416,14 +451,19 @@ def provider_login(request): ...@@ -416,14 +451,19 @@ def provider_login(request):
# fullname field comes from user profile # fullname field comes from user profile
profile = UserProfile.objects.get(user=user) profile = UserProfile.objects.get(user=user)
log.info("OpenID login success - {0} ({1})".format(user.username, user.email)) log.info("OpenID login success - {0} ({1})".format(user.username,
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)) response = openid_request['request'].answer(True, None, endpoint + urlquote(user.username))
return provider_respond(server, openid_request['request'], response, {
'fullname': profile.name, return provider_respond(server,
'email': user.email openid_request['request'],
}) response,
{
'fullname': profile.name,
'email': user.email
})
request.session['openid_error'] = True request.session['openid_error'] = True
log.warning("Login failed - Account not active for user {0}".format(username)) log.warning("Login failed - Account not active for user {0}".format(username))
...@@ -445,6 +485,7 @@ def provider_login(request): ...@@ -445,6 +485,7 @@ def provider_login(request):
response['X-XRDS-Location'] = get_xrds_url('xrds', request) response['X-XRDS-Location'] = get_xrds_url('xrds', request)
return response return response
def provider_identity(request): def provider_identity(request):
""" """
XRDS for identity discovery XRDS for identity discovery
...@@ -458,6 +499,7 @@ def provider_identity(request): ...@@ -458,6 +499,7 @@ def provider_identity(request):
response['X-XRDS-Location'] = get_xrds_url('identity', request) response['X-XRDS-Location'] = get_xrds_url('identity', request)
return response return response
def provider_xrds(request): def provider_xrds(request):
""" """
XRDS for endpoint discovery XRDS for endpoint discovery
...@@ -470,4 +512,3 @@ def provider_xrds(request): ...@@ -470,4 +512,3 @@ def provider_xrds(request):
# 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)
return response return response
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