[8650] | 1 | ''' |
---|
[8791] | 2 | BSD Licence |
---|
| 3 | Copyright (c) 2012, Science & Technology Facilities Council (STFC) |
---|
| 4 | All rights reserved. |
---|
| 5 | |
---|
| 6 | Redistribution and use in source and binary forms, with or without modification, |
---|
| 7 | are permitted provided that the following conditions are met: |
---|
| 8 | |
---|
| 9 | * Redistributions of source code must retain the above copyright notice, |
---|
| 10 | this list of conditions and the following disclaimer. |
---|
| 11 | * Redistributions in binary form must reproduce the above copyright notice, |
---|
| 12 | this list of conditions and the following disclaimer in the documentation |
---|
| 13 | and/or other materials provided with the distribution. |
---|
| 14 | * Neither the name of the Science & Technology Facilities Council (STFC) |
---|
| 15 | nor the names of its contributors may be used to endorse or promote |
---|
| 16 | products derived from this software without specific prior written permission. |
---|
| 17 | |
---|
| 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
---|
| 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
---|
| 20 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
---|
| 21 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS |
---|
| 22 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, |
---|
| 23 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
---|
| 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
---|
| 25 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
---|
| 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
---|
| 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
---|
[8650] | 28 | Created on 29 Oct 2012 |
---|
| 29 | |
---|
| 30 | @author: mnagni |
---|
| 31 | ''' |
---|
[8791] | 32 | from django.contrib.auth import authenticate, SESSION_KEY |
---|
[8650] | 33 | from django.core.context_processors import csrf |
---|
[8678] | 34 | from django.conf import settings |
---|
[8736] | 35 | from django_authopenid.views import not_authenticated, ask_openid,\ |
---|
[8748] | 36 | signin_failure, _build_context |
---|
[8736] | 37 | from django_authopenid.forms import OpenidSigninForm |
---|
| 38 | from django.db.models import Q |
---|
| 39 | from django.contrib.auth.backends import ModelBackend |
---|
| 40 | from django.shortcuts import render_to_response as render |
---|
[8769] | 41 | from django.contrib import messages |
---|
[8712] | 42 | |
---|
[8736] | 43 | from userdb_model.models import User |
---|
| 44 | |
---|
| 45 | import md5 |
---|
[8673] | 46 | import logging |
---|
[8712] | 47 | import base64 |
---|
[8736] | 48 | from django_authopenid.utils import get_url_host |
---|
| 49 | from django.core.urlresolvers import reverse |
---|
| 50 | import urllib |
---|
| 51 | from django.http import HttpResponseRedirect |
---|
| 52 | from django.contrib.auth.forms import AuthenticationForm |
---|
| 53 | from django import forms |
---|
| 54 | from django.contrib.auth.signals import user_logged_in |
---|
| 55 | from django.db.utils import DatabaseError |
---|
[8791] | 56 | from dj_security.exception import DSJOpenIDNotFoundError, PasswordNotMaches,\ |
---|
| 57 | UserNotFound |
---|
[8741] | 58 | from dj_security.middleware import _encode_authenticated_response |
---|
[8769] | 59 | from django.core.exceptions import ObjectDoesNotExist |
---|
[8791] | 60 | from dj_security import _redirect_field_name |
---|
[8712] | 61 | |
---|
[8673] | 62 | # Get an instance of a logger |
---|
[8681] | 63 | LOGGER = logging.getLogger(__name__) |
---|
[8673] | 64 | |
---|
[8748] | 65 | OPENID = 'openid.identity' |
---|
[8736] | 66 | |
---|
[8666] | 67 | def error_handle(request, context): |
---|
[8736] | 68 | form = CEDAAuthenticationForm() |
---|
[8650] | 69 | context['form'] = form |
---|
| 70 | context.update(csrf(request)) |
---|
[8748] | 71 | return render('login.html', context) |
---|
[8650] | 72 | |
---|
[8736] | 73 | class CedaUserAuthenticationBackend(ModelBackend): |
---|
| 74 | """ |
---|
[8748] | 75 | Extends Django's ``ModelBackend`` to allow login via username, |
---|
| 76 | or verification token. |
---|
[8736] | 77 | |
---|
| 78 | Args are either ``username`` and ``password`` |
---|
| 79 | and ``token``. In either case, ``is_active`` can also be given. |
---|
| 80 | |
---|
| 81 | For login, is_active is not given, so that the login form can |
---|
| 82 | raise a specific error for inactive users. |
---|
| 83 | For password reset, True is given for is_active. |
---|
| 84 | For signup verficiation, False is given for is_active. |
---|
| 85 | """ |
---|
| 86 | |
---|
[8739] | 87 | def __init__(self, *args, **kwargs): |
---|
| 88 | super(CedaUserAuthenticationBackend, self).__init__(*args, **kwargs) |
---|
| 89 | |
---|
[8736] | 90 | def authenticate(self, **kwargs): |
---|
| 91 | if kwargs: |
---|
| 92 | username = kwargs.pop("username", None) |
---|
| 93 | if username: |
---|
| 94 | username = Q(accountid=username) |
---|
| 95 | password = kwargs.pop("password", None) |
---|
| 96 | try: |
---|
| 97 | user = User.objects.get(username, **kwargs) |
---|
| 98 | except User.DoesNotExist: |
---|
[8791] | 99 | raise UserNotFound() |
---|
[8736] | 100 | pass |
---|
| 101 | else: |
---|
[8791] | 102 | if user.md5passwd == md5.new(password).hexdigest(): |
---|
[8736] | 103 | return user |
---|
[8791] | 104 | else: |
---|
| 105 | LOGGER.error("Wrong password for username: %s" % username) |
---|
| 106 | raise PasswordNotMaches() |
---|
[8736] | 107 | |
---|
| 108 | def get_user_byopenid(user_id): |
---|
| 109 | """ |
---|
| 110 | Returns a tbusers row specified by `user_id` |
---|
| 111 | - String **user_id** |
---|
| 112 | a user |
---|
| 113 | """ |
---|
| 114 | try: |
---|
| 115 | return User.objects.get(openid=user_id) |
---|
[8769] | 116 | except (DatabaseError, ObjectDoesNotExist) as ex: |
---|
[8736] | 117 | logging.error("Openid: %s - Not Found" % user_id) |
---|
| 118 | raise DSJOpenIDNotFoundError(ex) |
---|
| 119 | |
---|
[8712] | 120 | def logged_in(request): |
---|
[8736] | 121 | ''' |
---|
| 122 | Retrieves the user after the openid provider authenticated him/her |
---|
| 123 | ''' |
---|
| 124 | if SESSION_KEY not in request.session: |
---|
| 125 | if OPENID in request.session: |
---|
| 126 | login(request, get_user_byopenid(request.session[OPENID])) |
---|
| 127 | |
---|
[8741] | 128 | return _encode_authenticated_response_(request, context = {}) |
---|
[8680] | 129 | |
---|
[8741] | 130 | def _encode_authenticated_response_(request, context): |
---|
[8712] | 131 | context['redirect_url'] = \ |
---|
[8791] | 132 | base64.b64decode(request.session.get(_redirect_field_name(), '')) |
---|
[8776] | 133 | LOGGER.info("Redirecting to %s" % (context['redirect_url'])) |
---|
[8791] | 134 | return render('logged_in.html', context) |
---|
[8736] | 135 | |
---|
| 136 | class CEDAAuthenticationForm(AuthenticationForm): |
---|
| 137 | |
---|
| 138 | def __init__(self, request=None, *args, **kwargs): |
---|
| 139 | super(CEDAAuthenticationForm, self).__init__(request, *args, **kwargs) |
---|
| 140 | |
---|
| 141 | def clean(self): |
---|
| 142 | username = self.cleaned_data.get('username') |
---|
| 143 | password = self.cleaned_data.get('password') |
---|
| 144 | |
---|
| 145 | if username and password: |
---|
| 146 | self.user_cache = authenticate(username=username, |
---|
| 147 | password=password) |
---|
| 148 | if self.user_cache is None: |
---|
| 149 | raise forms.ValidationError( |
---|
| 150 | self.error_messages['invalid_login']) |
---|
| 151 | #elif not self.user_cache.is_active: |
---|
| 152 | # raise forms.ValidationError(self.error_messages['inactive']) |
---|
| 153 | self.check_for_test_cookie() |
---|
| 154 | return self.cleaned_data |
---|
| 155 | |
---|
| 156 | |
---|
| 157 | @not_authenticated |
---|
| 158 | def signin(request, template_name='authopenid/signin.html', |
---|
[8791] | 159 | redirect_field_name=_redirect_field_name(), |
---|
| 160 | openid_form=OpenidSigninForm, |
---|
[8748] | 161 | auth_form=CedaUserAuthenticationBackend, |
---|
[8760] | 162 | on_failure=None, extra_context={'is_login_page': True}): |
---|
[8736] | 163 | """Signin page. It manage the legacy authentification (user/password) |
---|
| 164 | and authentification with openid. |
---|
| 165 | |
---|
| 166 | :attr request: request object |
---|
| 167 | :attr template_name: string, name of template to use |
---|
| 168 | :attr redirect_field_name: string, field name used for redirect. by |
---|
| 169 | default 'next' |
---|
| 170 | :attr openid_form: form use for openid signin, by default |
---|
| 171 | `OpenidSigninForm` |
---|
| 172 | :attr auth_form: form object used for legacy authentification. |
---|
| 173 | By default AuthentificationForm form auser auth contrib. |
---|
| 174 | :attr extra_context: A dictionary of variables to add to the |
---|
| 175 | template context. Any callable object in this dictionary will |
---|
| 176 | be called to produce the end result which appears in the context. |
---|
| 177 | """ |
---|
| 178 | if on_failure is None: |
---|
| 179 | on_failure = signin_failure |
---|
| 180 | |
---|
[8892] | 181 | if not request.session.has_key(redirect_field_name): |
---|
[8791] | 182 | |
---|
[8892] | 183 | if request.GET.has_key(redirect_field_name): |
---|
[8893] | 184 | redirect = urllib.unquote_plus(request.GET.get(redirect_field_name)) |
---|
| 185 | LOGGER.debug("No redirect in session. Setting to: %s" % redirect) |
---|
| 186 | request.session[redirect_field_name] = redirect |
---|
[8892] | 187 | else: |
---|
[8893] | 188 | LOGGER.debug("No redirect in session. Setting to default: %s" % settings.LOGIN_REDIRECT_URL) |
---|
[8892] | 189 | request.session[redirect_field_name] = settings.LOGIN_REDIRECT_URL |
---|
| 190 | |
---|
[8736] | 191 | form1 = openid_form() |
---|
| 192 | form2 = auth_form() |
---|
| 193 | if request.POST: |
---|
[8791] | 194 | if not request.session.has_key(redirect_field_name): |
---|
| 195 | request.session[redirect_field_name] = settings.LOGIN_REDIRECT_URL |
---|
[8736] | 196 | if 'openid_url' in request.POST.keys(): |
---|
| 197 | form1 = openid_form(data=request.POST) |
---|
| 198 | if form1.is_valid(): |
---|
| 199 | redirect_url = "%s%s?%s" % ( |
---|
| 200 | get_url_host(request), |
---|
| 201 | reverse('user_complete_signin'), |
---|
[8791] | 202 | urllib.urlencode({ redirect_field_name: |
---|
| 203 | request.session[redirect_field_name]}) |
---|
[8736] | 204 | ) |
---|
| 205 | return ask_openid(request, |
---|
| 206 | form1.cleaned_data['openid_url'], |
---|
| 207 | redirect_url, |
---|
| 208 | on_failure=on_failure) |
---|
| 209 | else: |
---|
| 210 | # perform normal django authentification |
---|
| 211 | form2 = auth_form(data=request.POST) |
---|
| 212 | if form2.is_valid(): |
---|
[8741] | 213 | #login(request, form2.get_user()) |
---|
[8736] | 214 | if request.session.test_cookie_worked(): |
---|
[8791] | 215 | request.session.delete_test_cookie() |
---|
| 216 | response = HttpResponseRedirect(request.session.get(redirect_field_name, '')) |
---|
| 217 | return _encode_authenticated_response(request, |
---|
[8748] | 218 | response, |
---|
[8791] | 219 | request.session.get(redirect_field_name, ''), |
---|
[8748] | 220 | form2.get_user()) |
---|
[8791] | 221 | else: |
---|
| 222 | return signin_failure(request, "Wrong username and/or password") |
---|
| 223 | |
---|
[8736] | 224 | return render(template_name, { |
---|
| 225 | 'form1': form1, |
---|
| 226 | 'form2': form2, |
---|
[8791] | 227 | redirect_field_name: request.session.get(redirect_field_name, ''), |
---|
[8736] | 228 | 'msg': request.GET.get('msg','') |
---|
| 229 | }, context_instance=_build_context(request, extra_context=extra_context)) |
---|
| 230 | |
---|
[8789] | 231 | def signin_failure(request, message, template_name='signin.html', |
---|
[8791] | 232 | redirect_field_name=_redirect_field_name(), openid_form=OpenidSigninForm, |
---|
[8789] | 233 | auth_form=AuthenticationForm, extra_context=None, **kwargs): |
---|
| 234 | messages.add_message(request, messages.WARNING, message) |
---|
| 235 | LOGGER.warn(message) |
---|
| 236 | return render(template_name, { |
---|
| 237 | 'msg': message, |
---|
| 238 | 'form1': openid_form(), |
---|
| 239 | 'form2': auth_form(), |
---|
| 240 | redirect_field_name: request.REQUEST.get(redirect_field_name, '') |
---|
| 241 | }, context_instance=_build_context(request, extra_context)) |
---|
| 242 | |
---|
[8741] | 243 | def signin_success(request, identity_url, openid_response, |
---|
[8791] | 244 | redirect_field_name=_redirect_field_name(), **kwargs): |
---|
[8736] | 245 | |
---|
[8741] | 246 | #redirect_parameter = getattr(settings, 'REDIRECT_URL', 'r') |
---|
[8791] | 247 | redirect_url = request.session.get(redirect_field_name, '') |
---|
[8741] | 248 | LOGGER.debug("Redirecting to %s" % (redirect_url)) |
---|
| 249 | ''' |
---|
| 250 | Retrieves the user after the openid provider authenticated him/her |
---|
| 251 | ''' |
---|
| 252 | response = HttpResponseRedirect(redirect_url) |
---|
| 253 | |
---|
[8748] | 254 | if OPENID in request.REQUEST: |
---|
[8769] | 255 | try: |
---|
| 256 | return _encode_authenticated_response(request, |
---|
[8748] | 257 | response, |
---|
| 258 | redirect_url, |
---|
[8769] | 259 | get_user_byopenid(request.REQUEST[OPENID])) |
---|
| 260 | except DSJOpenIDNotFoundError as e: |
---|
| 261 | msg = "No user associated with OPENID %s" % (request.REQUEST[OPENID]) |
---|
| 262 | messages.add_message(request, messages.WARNING, msg) |
---|
| 263 | LOGGER.warn(msg) |
---|
[8788] | 264 | return response |
---|
[8741] | 265 | elif SESSION_KEY in request.session: |
---|
| 266 | response = HttpResponseRedirect(redirect_url) |
---|
| 267 | return _encode_authenticated_response(request, response, redirect_url) |
---|
| 268 | |
---|
| 269 | |
---|
| 270 | |
---|
[8736] | 271 | def login(request, user): |
---|
| 272 | """ |
---|
| 273 | Persist a user id and a backend in the request. This way a user doesn't |
---|
| 274 | have to reauthenticate on every request. Note that data set during |
---|
| 275 | the anonymous session is retained when the user logs in. |
---|
| 276 | Overrides the django.contrib.auth.login method |
---|
| 277 | """ |
---|
| 278 | if user is None: |
---|
| 279 | user = request.user |
---|
| 280 | # TODO: It would be nice to support different login methods, like signed cookies. |
---|
| 281 | if SESSION_KEY in request.session: |
---|
| 282 | if request.session[SESSION_KEY] != user.accountid: |
---|
| 283 | # To avoid reusing another user's session, create a new, empty |
---|
| 284 | # session if the existing session corresponds to a different |
---|
| 285 | # authenticated user. |
---|
[8740] | 286 | r_copy = None |
---|
[8791] | 287 | if request.session.has_key(_redirect_field_name()): |
---|
| 288 | r_copy = request.session[_redirect_field_name()] |
---|
[8736] | 289 | request.session.flush() |
---|
| 290 | request.user = user |
---|
[8791] | 291 | request.session[_redirect_field_name()] = r_copy |
---|
[8736] | 292 | else: |
---|
| 293 | request.session.cycle_key() |
---|
| 294 | request.session[SESSION_KEY] = user.userkey |
---|
[8741] | 295 | #request.session[BACKEND_SESSION_KEY] = user.backend |
---|
[8736] | 296 | if hasattr(request, 'user'): |
---|
| 297 | request.user = user |
---|
[8892] | 298 | user_logged_in.send(sender=user.__class__, request=request, user=user) |
---|