From 11827577d7cb5d3c53fe56d151d007b3a4abb6d2 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 17 Dec 2014 09:42:20 +0100 Subject: [PATCH] Re-implement the authenticate() to handle multiple user login An authentication backend can now return a sequence of users, the end-user is invited to choose one among those found. --- src/authentic2/backends/models_backend.py | 10 +++++ src/authentic2/utils.py | 75 +++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/authentic2/backends/models_backend.py b/src/authentic2/backends/models_backend.py index 09d5b38..4c8378a 100644 --- a/src/authentic2/backends/models_backend.py +++ b/src/authentic2/backends/models_backend.py @@ -20,16 +20,26 @@ def get_proxy_user_model(): def roles(self): return self.groups.values_list('name', flat=True) class Meta: proxy = True PROXY_USER_MODEL = ProxyUser return PROXY_USER_MODEL +class EmailModelBackend(ModelBackend): + def authenticate(self, email=None): + UserModel = get_proxy_user_model() + users = UserModel.objects.filter(email=email) + if len(users) == 1: + return users[0] + if users: + return users + return None + class ModelBackend(ModelBackend): """ Authenticates against settings.AUTH_USER_MODEL. """ def get_query(self, username, realm): UserModel = get_proxy_user_model() username_field = UserModel.USERNAME_FIELD diff --git a/src/authentic2/utils.py b/src/authentic2/utils.py index 1ec4299..8b7f754 100644 --- a/src/authentic2/utils.py +++ b/src/authentic2/utils.py @@ -1,21 +1,25 @@ import random import time import logging import urllib import six import urlparse from functools import wraps +import inspect +import pickle from importlib import import_module from django.conf import settings from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.contrib.auth import _clean_credentials +from django.contrib.auth.signals import user_login_failed from django.core import urlresolvers from django.http.request import QueryDict from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login from django import forms from django.forms.util import ErrorList from django.utils import html, http from django.utils.translation import ugettext as _ @@ -300,23 +304,16 @@ def find_authentication_event(request, nonce): '''Find an authentication event occurring during this session and matching this nonce.''' authentication_events = request.session.pop(constants.AUTHENTICATION_EVENTS_SESSION_KEY, []) for event in authentication_events: if event.get('nonce') == nonce: return event return None -def login(request, user, how, **kwargs): - '''Login a user model, record the authentication event and redirect to next - URL or settings.LOGIN_REDIRECT_URL.''' - auth_login(request, user) - record_authentication_event(request, how) - return continue_to_next_url(request, **kwargs) - def login_require(request, next_url=None, login_url='auth_login', **kwargs): '''Require a login and come back to current URL''' next_url = next_url or request.get_full_path() params = kwargs.setdefault('params', {}) params[REDIRECT_FIELD_NAME] = next_url return redirect(request, login_url, **kwargs) def redirect_to_logout(request, next_url=None, logout_url='auth_logout', **kwargs): @@ -434,8 +431,70 @@ def attribute_values_to_identifier(values): def csrf_token_check(request, form): '''Check a request for CSRF cookie validation, and add an error to the form if check fails. ''' if form.is_valid() and not getattr(request, 'csrf_processing_done', False): msg = _('The form was out of date, please try again.') form._errors[forms.forms.NON_FIELD_ERRORS] = ErrorList([msg]) + +def _get_backends(return_tuples=False): + backends = [] + for backend_path in settings.AUTHENTICATION_BACKENDS: + backend = load_backend(backend_path) + backends.append((backend, backend_path) if return_tuples else backend) + if not backends: + raise ImproperlyConfigured( + 'No authentication backends have been defined. Does ' + 'AUTHENTICATION_BACKENDS contain anything?' + ) + return backends + +def authenticate(**credentials): + """ + If the given credentials are valid, return a User object. + """ + for backend, backend_path in _get_backends(return_tuples=True): + try: + inspect.getcallargs(backend.authenticate, **credentials) + except TypeError: + # This backend doesn't accept these credentials as arguments. Try the next one. + continue + + try: + user = backend.authenticate(**credentials) + except PermissionDenied: + # This backend says to stop in our tracks - this user should not be allowed in at all. + return None + if user is None: + continue + # Allow backends to return multiple matches + if isinstance(user, (list, tuple)): + users = user + for user in users: + # Annotate the user object with the path of the backend. + user.backend = backend_path + return users + else: + # Annotate the user object with the path of the backend. + user.backend = backend_path + return user + + # The credentials supplied are invalid to all backends, fire signal + user_login_failed.send(sender=__name__, + credentials=_clean_credentials(credentials)) + +MULTIPLE_USER_SESSION_KEY = 'multiple_users' + +def login(request, user, how, multiple_login_url='a2-multiple-login', **kwargs): + '''Login a user or if user is a list of users, redirect so that user can + pick one. + ''' + if isinstance(user, (list, tuple)): + request.session[MULTIPLE_USER_SESSION_KEY] = pickle.dumps(user) + return redirect_to_login(request, login_url=multiple_login_url) + else: + auth_login(request, user) + record_authentication_event(request, how) + return continue_to_next_url(request, **kwargs) + + -- 1.9.1