views.py 8.59 KB
Newer Older
ichuang committed
1 2 3
import json
import logging
import random
4
import re
ichuang committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
import string

from external_auth.models import ExternalAuthMap

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 django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.shortcuts import redirect
from django.template import RequestContext
from mitxmako.shortcuts import render_to_response, render_to_string
try:
    from django.views.decorators.csrf import csrf_exempt
except ImportError:
    from django.contrib.csrf.middleware import csrf_exempt
from django_future.csrf import ensure_csrf_cookie
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)
29
import django_openid_auth.views as openid_views
ichuang committed
30 31 32 33 34 35 36 37 38

import student.views as student_views

log = logging.getLogger("mitx.external_auth")

@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
39
    log.debug(message)
ichuang committed
40 41 42
    data = render_to_string( template_name, dict(message=message, exception=exception))
    return HttpResponse(data, status=status)

43 44 45
#-----------------------------------------------------------------------------
# Openid

46 47
def edXauth_generate_password(length=12, chars=string.letters + string.digits):
    """Generate internal password for externally authenticated user"""
48 49
    return ''.join([random.choice(chars) for i in range(length)])

ichuang committed
50 51 52 53 54 55 56 57 58
@csrf_exempt
def edXauth_openid_login_complete(request,  redirect_field_name=REDIRECT_FIELD_NAME, render_failure=None):
    """Complete the openid login process"""

    redirect_to = request.REQUEST.get(redirect_field_name, '')
    render_failure = render_failure or \
                     getattr(settings, 'OPENID_RENDER_FAILURE', None) or \
                     default_render_failure
                                                   
59
    openid_response = openid_views.parse_openid_response(request)
ichuang committed
60 61 62 63 64 65
    if not openid_response:
        return render_failure(request, 'This is an OpenID relying party endpoint.')

    if openid_response.status == SUCCESS:
        external_id = openid_response.identity_url
        oid_backend =  openid_auth.OpenIDBackend()
ichuang committed
66
        details = oid_backend._extract_user_details(openid_response)
ichuang committed
67 68 69

        log.debug('openid success, details=%s' % details)

70 71 72 73 74 75 76 77
        return edXauth_external_login_or_signup(request,
                                                external_id, 
                                                "openid:%s" % settings.OPENID_SSO_SERVER_URL,
                                                details,
                                                details.get('email',''),
                                                '%s %s' % (details.get('first_name',''),details.get('last_name',''))
                                                )
                                 
ichuang committed
78
    return render_failure(request, 'Openid failure')
79 80 81 82

#-----------------------------------------------------------------------------
# generic external auth login or signup

83 84
def edXauth_external_login_or_signup(request, external_id, external_domain, credentials, email, fullname,
                                     retfun=None):
85 86
    # see if we have a map from this external_id to an edX username
    try:
87 88 89
        eamap = ExternalAuthMap.objects.get(external_id = external_id,
                                            external_domain = external_domain,
                                            )
90 91 92 93 94 95 96 97 98
        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),
                                )
        eamap.external_email = email
        eamap.external_name = fullname
99
        eamap.internal_password = edXauth_generate_password()
100 101 102 103 104 105 106 107
        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)
        return edXauth_signup(request, eamap)
ichuang committed
108
    
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
    uname = internal_user.username
    user = authenticate(username=uname, password=eamap.internal_password)
    if user is None:
        log.warning("External Auth Login failed for %s / %s" % (uname,eamap.internal_password))
        return edXauth_signup(request, eamap)

    if not user.is_active:
        log.warning("External Auth: user %s is not active" % (uname))
        # TODO: improve error page
        return render_failure(request, 'Account not yet activated: please look for link in your email')
     
    login(request, user)
    request.session.set_expiry(0)
    student_views.try_change_enrollment(request)
    log.info("Login success - {0} ({1})".format(user.username, user.email))
124 125 126 127
    if retfun is None:
        return redirect('/')
    return retfun()
        
128 129 130 131
    
#-----------------------------------------------------------------------------
# generic external auth signup

ichuang committed
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
@ensure_csrf_cookie
@cache_if_anonymous
def edXauth_signup(request, eamap=None):
    """
    Present form to complete for signup via external authentication.
    Even though the user has external credentials, he/she still needs
    to create an account on the edX system, and fill in the user
    registration form.

    eamap is an ExteralAuthMap object, specifying the external user
    for which to complete the signup.
    """
    
    if eamap is None:
        pass

    request.session['ExternalAuthMap'] = eamap	# save this for use by student.views.create_account
    
    context = {'has_extauth_info': True,
               'show_signup_immediately' : True,
               'extauth_email': eamap.external_email,
153
               'extauth_username' : eamap.external_name.replace(' ',''), # default - conjoin name, no spaces
ichuang committed
154 155 156 157 158 159
               'extauth_name': eamap.external_name,
               }
    
    log.debug('ExtAuth: doing signup for %s' % eamap.external_email)

    return student_views.main_index(extra_context=context)
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202

#-----------------------------------------------------------------------------
# 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)
        email = "%s@%s" % (user, ss.group(2))
    else:
        return None
    ss = re.search('/CN=([^/]+)/', dn)
    if ss:
        fullname = ss.group(1)
    else:
        return None
    return (user, email, fullname)

@csrf_exempt
def edXauth_ssl_login(request):
    """
    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 
    to require specific client certificates.

    If the incoming protocol is HTTPS (SSL) then authenticate via client certificate.  
    The certificate provides user email and fullname; this populates the ExternalAuthMap.
    The user is nevertheless still asked to complete the edX signup.  

    Else continues on with student.views.main_index, and no authentication.
    """
    certkey = "SSL_CLIENT_S_DN"	 # specify the request.META field to use
    
    cert = request.META.get(certkey,'')
    if not cert:
        cert = request.META.get('HTTP_'+certkey,'')
    if not cert:
203 204 205 206
        try:
            cert = request._req.subprocess_env.get(certkey,'')	 # try the direct apache2 SSL key
        except Exception as err:
            pass
207 208 209 210 211 212 213 214 215 216 217
    if not cert:
        # no certificate information - go onward to main index
        return student_views.main_index()

    (user, email, fullname) = ssl_dn_extract_info(cert)
    
    return edXauth_external_login_or_signup(request, 
                                            external_id=email, 
                                            external_domain="ssl:MIT", 
                                            credentials=cert, 
                                            email=email,
218 219
                                            fullname=fullname,
                                            retfun = student_views.main_index)