Ok, here's a stripped-down solution. I ended up creating a new SchoolUser user model with a OneToOne relation to my LegacyUser, to keep the LegacyUser model uncluttered. The SchoolUser implements all methods from AbstractBaseUser and PermissionsMixin but doesn't inherit from them, because I don't want the model fields that they contain.
I also kept the SchoolUser independent from the standard Django User (i.e. no AUTH_USER_MODEL='SchoolUser' in settings.py), so I can still create superuser accounts for myself and my colleagues, that are not connected to a school user. Here's the code: settings.py: [...] AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'school_auth.backends.SchoolModelBackend', ) school_auth/backend.py: from django.contrib.auth.backends import ModelBackend from .models import SchoolUser, LegacyUser class SchoolModelBackend(object): def authenticate(self, school_id=None, username=None, password=None, **kwargs): if LegacyUser.validate(school=school_id, username=username, password=password): # Password hash validation try: school_user = SchoolUser.objects.get(user__school=school_id, user__name=username) except SchoolUser.DoesNotExist: school_user = SchoolUser.objects.create_user(school_id=school_id, username=username) # Annotate the user object with the path of the backend. school_user.backend = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) return school_user # # if LDAP.validate(school=school_id, username=username, password=password): # pass # if WAYF.validate(school=school_id, username=username, password=password): # pass return None def get_group_permissions(self, user_obj, obj=None): raise NotImplementedError() def get_all_permissions(self, user_obj, obj=None): raise NotImplementedError() def has_perm(self, user_obj, perm, obj=None): if not user_obj.is_active: return False return perm in self.get_all_permissions(user_obj, obj) def has_module_perms(self, user_obj, app_label): if not user_obj.is_active: return False for perm in self.get_all_permissions(user_obj): if perm[:perm.index('.')] == app_label: return True return False def get_user(self, user_id): try: return SchoolUser.objects.get(pk=user_id) except SchoolUser.DoesNotExist: return None school_auth/forms.py: from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, PasswordChangeForm from django import forms from django.utils.translation import ugettext_lazy as _ from django.utils.text import capfirst from .models import LegacyUser, School, SchoolUser from .backends import SchoolModelBackend class SchoolAuthenticationForm(AuthenticationForm): school = forms.ModelChoiceField(queryset=School.objects.active(), empty_label=_('Please select a school')) username = forms.CharField(max_length=40) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) class Meta: model = LegacyUser fields = ['school', 'name', 'password'] def __init__(self, request=None, *args, **kwargs): """ The 'request' parameter is set for custom auth use by subclasses. The form data comes in via the standard 'data' kwarg. """ self.request = request self.user_cache = None super().__init__(*args, **kwargs) # Set the label for the "username" field. self.username_field = LegacyUser._meta.get_field(SchoolUser.USERNAME_FIELD) if self.fields['username'].label is None: self.fields['username'].label = capfirst(self.username_field.verbose_name) def clean(self): school = self.cleaned_data.get('school') username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') if school and username and password: self.user_cache = SchoolModelBackend().authenticate(school_id=school.pk, username=username, password=password) if self.user_cache is None: raise forms.ValidationError( self.error_messages['invalid_login'], code='invalid_login', params={'username': self.username_field.verbose_name}, ) else: self.confirm_login_allowed(self.user_cache) return self.cleaned_data school_auth/models.py: from django.contrib.auth.models import PermissionsMixin, BaseUserManager, AbstractBaseUser from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.utils.crypto import salted_hmac from ..legacy.models import LegacyUser, School class SchoolUserManager(BaseUserManager): def create_user(self, school_id, username): user = LegacyUser.objects.get(school=school_id, name=username) school_user = self.model(user=user) school_user.save() return school_user class SchoolUser(models.Model): """ Custom User model. We don't inherit AbstractBaseUser because we don't want the password field. """ USERNAME_FIELD = 'name' user = models.OneToOneField(User, null=False) last_login = models.DateTimeField(_('last login'), default=timezone.now) objects = SchoolUserManager() REQUIRED_FIELDS = [] @property def school(self): return self.user.school def __str__(self): return self.get_username() def get_username(self): return self.user.name @property def is_active(self): return self.user.active @property def is_staff(self): return self.user.is_admin() def natural_key(self): return self.user.school_id, self.user.name @staticmethod def is_anonymous(): return False @staticmethod def is_authenticated(): return True def set_password(self, raw_password): self.user.set_password(raw_password) def check_password(self, raw_password): return self.user.validate_password(raw_password) def set_unusable_password(self): pass @staticmethod def has_usable_password(): return True def get_full_name(self): return self.user.name def get_short_name(self): return self.user.name def get_session_auth_hash(self): """ Returns an HMAC of the password field. """ key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" return salted_hmac(key_salt, self.user.password).hexdigest() def email_user(self, subject, message, from_email=None, **kwargs): """ Sends an email to this User. """ from django.core.mail import send_mail send_mail(subject, message, from_email, [self.email], **kwargs) @property def is_superuser(self): # This can never be a Django superuser return False def get_group_permissions(self, obj=None): raise NotImplementedError() def get_all_permissions(self, obj=None): raise NotImplementedError() def has_perm(self, perm, obj=None): [...] # My custom per-app permissions def has_perms(self, perm_list, obj=None): for perm in perm_list: if not self.has_perm(perm, obj): return False return True def has_module_perms(self, app_label): [...] # My custom per-module permissions school_auth/urls.py: from django.conf.urls import url from django.contrib.auth.views import login, logout from .forms import SchoolAuthenticationForm urlpatterns = [ url(r'^login/$', login, kwargs={'authentication_form': SchoolAuthenticationForm, 'template_name': 'school_auth/login.html'}, name='school_auth.login'), url(r'^logout/$', logout, name='school_auth.logout'), ] Thanks, Erik -- You received this message because you are subscribed to the Google Groups "Django users" group. To unsubscribe from this group and stop receiving emails from it, send an email to django-users+unsubscr...@googlegroups.com. To post to this group, send email to django-users@googlegroups.com. Visit this group at http://groups.google.com/group/django-users. To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/BC66EBBA-A032-42B1-8DB0-F2CFFEE48BA6%40cederstrand.dk. For more options, visit https://groups.google.com/d/optout.