How can I use two Django authentication backends and give different access depending on which is used?

Viewed 138

I have two different classes of user for a Django-based event registration system. Let's call them "Registrant" and "Staffer".

These two are not mutually exclusive, and I see no reason not to use the same User model for both classes (with a flag to distinguish them).

A Registrant can sign in by verifying their identity with a third-party API, and can then register for an event.

A Staffer has access to Django Admin, and can manage events. I do not trust the third-party API to sufficiently authenticate somebody who has Staffer access as, while it does request personal information to identify someone, it doesn't ask for anything as secure as a password. I therefore want Staffers to enter in a password instead of authenticate via the third-party API.

I see that I can write a custom auth backend in Django, and I can customise the default User model to suit my needs regarding aforementioned flags (though I suspect the default is_staff) flag might be sufficient).

Unfortunately, while I know that I can use multiple authentication backends, I am quite stuck working out how to:

a) determine which authentication backend to use (e.g. use the Registrant auth backend in the frontend, and the Staffer auth backend in the Admin) b) only allow users authenticated via the Staffer auth backend to access the Admin

Can anybody please point me in the right direction?

Apologies for the lack of code; I'm re-working the auth system from scratch (because the previous version was really badly hacked together) and I don't quite know where to start.

EDIT: There is some useful info at Django Multiple Authentication Backends Based On Status, but because I would like to use the same User model for both user classes, and both auth backends would request different types of credentials (e.g. one requesting a password, the other not), I need to think more about how to tackle this.

2 Answers

The answer to "determine which authentication backend to use" is quite simple, you simply don't need to. As specified in the documentation when you call the authenticate() function Django tries each of the AUTHENTICATION_BACKENDS in turn until one either returns a user or raises a PermissionDenied exception.

Also if you look at the default backend's code [GitHub], if no username or password is passed in, it just returns None. You would need to implement your backend in a similar manner so that if the credentials you expect aren't passed in you should just return None and let the other (possible) backends handle it.

For "only allow users authenticated via the Staffer auth backend to access the Admin" you can override the default admin site and override the has_permission method:

from django.contrib import admin
from django.contrib.auth import BACKEND_SESSION_KEY

class MyAdminSite(admin.AdminSite):
    def has_permission(self, request):
        """
        Return True if the given HttpRequest has permission to view
        *at least one* page in the admin site.
        """
        try:
            backend_path = request.session[BACKEND_SESSION_KEY]
            staffer_backend = backend_path == "path.to.StafferBackend"
            return request.user.is_active and request.user.is_staff and staffer_backend
        except KeyError:
            return False

And then in an apps.py in one of your apps:

from django.contrib.admin.apps import AdminConfig

class MyAdminConfig(AdminConfig):
    default_site = 'myproject.admin.MyAdminSite'

And finally in your setting replace 'django.contrib.admin':

INSTALLED_APPS = [
    ...
    'myproject.apps.MyAdminConfig',  # replaces 'django.contrib.admin'
    ...
]

If I had this problem I would use an inherited model for each of them, making username as the default login in staff and email on the registrant, extending both the BaseUser model.

To select with Auth backend I just set a checkbox or build different URLs to login.

Related