From 4b6694a4ced9510d131a73b74d7d5a8f7fe271ee Mon Sep 17 00:00:00 2001 From: Tommy MacWilliam Date: Wed, 15 Aug 2012 17:24:09 -0400 Subject: [PATCH] OpenID provider implementation - endpoint supports both SReg and AX - identity taken from edX username - sreg fullname and ax http://axschema.org/namePerson taken from edX name - sreg email and ax http://axschema.org/contact/email taken from edX email --- common/djangoapps/external_auth/views.py | 205 +++++++++++++++++++++++ lms/templates/identity.xml | 10 ++ lms/templates/provider_login.html | 35 ++++ lms/templates/xrds.xml | 11 ++ lms/urls.py | 4 + 5 files changed, 265 insertions(+) create mode 100644 lms/templates/identity.xml create mode 100644 lms/templates/provider_login.html create mode 100644 lms/templates/xrds.xml diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 9e41d31c77..ece2fff00a 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -11,9 +11,12 @@ from django.conf import settings 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 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.utils.http import urlquote from django.shortcuts import render_to_response from django.shortcuts import redirect from django.template import RequestContext @@ -29,6 +32,14 @@ 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 +from openid.server.server import Server, ProtocolError, CheckIDRequest, EncodingError +from openid.server.trustroot import verifyReturnTo +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.fetchers import HTTPFetchingError + import student.views as student_views log = logging.getLogger("mitx.external_auth") @@ -218,3 +229,197 @@ def edXauth_ssl_login(request): email=email, fullname=fullname, retfun = functools.partial(student_views.index, request)) + +def get_dict_for_openid(data): + """ + Return a dictionary suitable for the OpenID library + """ + + return dict((k, v) for k, v in data.iteritems()) + +def get_xrds_url(resource, request): + """ + Return the XRDS url for a resource + """ + + location = request.META['HTTP_HOST'] + '/openid/provider/' + resource + '/' + if request.is_secure(): + url = 'https://' + location + else: + url = 'http://' + location + + return url + +def provider_respond(server, request, response, data): + """ + Respond to an OpenID request + """ + + # get simple registration request + sreg_data = {} + sreg_request = sreg.SRegRequest.fromOpenIDRequest(request) + sreg_fields = sreg_request.allRequestedFields() + + # if consumer requested simple registration fields, add them + if sreg_fields: + for field in sreg_fields: + if field == 'email' and 'email' in data: + sreg_data['email'] = data['email'] + elif field == 'fullname' and 'fullname' in data: + sreg_data['fullname'] = data['fullname'] + + # construct sreg response + sreg_response = sreg.SRegResponse.extractResponse(sreg_request, sreg_data) + sreg_response.toMessage(response.fields) + + # get attribute exchange request + try: + ax_request = ax.FetchRequest.fromOpenIDRequest(request) + + except ax.AXError: + 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']); + + # construct ax response + ax_response.toMessage(response.fields) + + # create http response from OpenID response + webresponse = server.encodeResponse(response) + http_response = HttpResponse(webresponse.body) + http_response.status_code = webresponse.code + + # add OpenID headers to response + for k, v in webresponse.headers.iteritems(): + http_response[k] = v + + return http_response + +@csrf_exempt +def provider_login(request): + """ + OpenID login endpoint + """ + + # initialize store and server + endpoint = get_xrds_url('login', request) + store = FileOpenIDStore('/tmp/openid_provider') + server = Server(store, endpoint) + + # handle OpenID request + query = get_dict_for_openid(request.GET or request.POST) + error = False + if 'openid.mode' in request.GET or 'openid.mode' in request.POST: + # decode request + openid_request = server.decodeRequest(query) + + # checkid_immediate not supported, require user interaction + if openid_request.mode == 'checkid_immediate': + return provider_respond(server, openid_request, openid_request.answer(false), {}) + + # checkid_setup, so display login page + elif openid_request.mode == 'checkid_setup': + if openid_request.idSelect(): + # remember request and original path + request.session['openid_request'] = { + 'request': openid_request, + 'url': request.get_full_path() + } + + # user failed login on previous attempt + if 'openid_error' in request.session: + error = True + del request.session['openid_error'] + + # OpenID response + else: + return provider_respond(server, openid_request, server.handleRequest(openid_request), {}) + + # handle login + if request.method == 'POST' and 'openid_request' in request.session: + # get OpenID request from session + openid_request = request.session['openid_request'] + del request.session['openid_request'] + + # check if user with given email exists + email = request.POST['email'] + password = request.POST['password'] + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + request.session['openid_error'] = True + log.warning("Login failed - Unknown user email: {0}".format(email)) + return HttpResponseRedirect(openid_request['url']) + + # attempt to authenticate user + username = user.username + user = authenticate(username=username, password=password) + if user is None: + request.session['openid_error'] = True + log.warning("Login failed - password for {0} is invalid".format(email)) + return HttpResponseRedirect(openid_request['url']) + + # authentication succeeded, so log user in + if user is not None and user.is_active: + # remove error from session since login succeeded + if 'openid_error' in request.session: + del request.session['openid_error'] + + # fullname field comes from user profile + profile = UserProfile.objects.get(user=user) + + # redirect user to return_to location + response = openid_request['request'].answer(True, None, endpoint + urlquote(user.username)) + return provider_respond(server, openid_request['request'], response, { + 'fullname': profile.name, + 'email': user.email + }) + + request.session['openid_error'] = True + log.warning("Login failed - Account not active for user {0}".format(username)) + return HttpResponseRedirect(openid_request['url']) + + # display login page + response = render_to_response('provider_login.html', { + 'error': error + }) + + # custom XRDS header necessary for discovery process + response['X-XRDS-Location'] = get_xrds_url('xrds', request) + return response + +def provider_identity(request): + """ + XRDS for identity discovery + """ + + 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) + return response + +def provider_xrds(request): + """ + XRDS for endpoint discovery + """ + + 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) + return response diff --git a/lms/templates/identity.xml b/lms/templates/identity.xml new file mode 100644 index 0000000000..a925493c03 --- /dev/null +++ b/lms/templates/identity.xml @@ -0,0 +1,10 @@ + + + + + http://specs.openid.net/auth/2.0/signon + http://openid.net/signon/1.1 + ${url} + + + diff --git a/lms/templates/provider_login.html b/lms/templates/provider_login.html new file mode 100644 index 0000000000..89061a3459 --- /dev/null +++ b/lms/templates/provider_login.html @@ -0,0 +1,35 @@ +<%inherit file="main.html" /> +<%namespace name='static' file='static_content.html'/> + + + + diff --git a/lms/templates/xrds.xml b/lms/templates/xrds.xml new file mode 100644 index 0000000000..2f7713bc8a --- /dev/null +++ b/lms/templates/xrds.xml @@ -0,0 +1,11 @@ + + + + + http://specs.openid.net/auth/2.0/server + http://openid.net/sreg/1.0 + http://openid.net/srv/ax/1.0 + ${url} + + + diff --git a/lms/urls.py b/lms/urls.py index 86d654eb40..540b713167 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -217,6 +217,10 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'), url(r'^openid/complete/$', 'external_auth.views.edXauth_openid_login_complete', name='openid-complete'), url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'), + url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'), + url(r'^openid/provider/login/(?:[\w%\. ]+)$', 'external_auth.views.provider_identity', name='openid-provider-login-identity'), + url(r'^openid/provider/identity/$', 'external_auth.views.provider_identity', name='openid-provider-identity'), + url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds') ) if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):