Search code examples
djangodjango-rest-frameworkdjango-viewsdjango-authentication

django authentication backend being ignored when specified


I have a two customer authentication backends: one for a standard login, and one for a two factor login.

In my settings.py, I have them both listed

AUTHENTICATION_BACKENDS = [
    'user_profile.auth_backends.TOTPBackend',
    'user_profile.auth_backends.StandardLoginBackend',
    'django.contrib.auth.backends.ModelBackend',
]

I have them stored in the app folder (user_profile) in a file called auth_backends.py and the class names match:

class TOTPBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, twofa_token=None, **kwargs):
        print("In TOTPBackend")
        # First, try to authenticate with the default method
        user = super().authenticate(request, username=username, password=password, **kwargs)
        
        if user is not None:
...

And

class StandardLoginBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        print("In StandardLoginBackend")

        # First, try to authenticate with the default method
        user = super().authenticate(request, username=username, password=password, **kwargs)
        
        if user is not None:
...

Then I based on the view called from some logic, I want to explicitly call one or the other...

class TwoFaLogin(APIView):
    permission_classes = (AllowAny,)

    def post(self, request):
        print("TwoFaLogin View")
        username = request.data['username']
        passwd = request.data['password']
        twofa_token = request.data['twofa_token']

        user = authenticate(request, username=username, password=passwd, twofa_token=twofa_token, backend='user_profile.auth_backends.TOTPBackend')

OR

class StandardLogin(APIView):
    permission_classes = (AllowAny,)

    def post(self, request):
        print("StandardLogin View")
        username = request.data['username']
        passwd = request.data['password']
        user = authenticate(request, username=username, password=passwd, backend='user_profile.auth_backends.StandardLoginBackend')

My challenge is that when making the authenticate call from the view, the backend parameter is being ignored - it is always calling them in order of the settings.py AUTHENTICATION_BACKENDS setting.

I cannot seem to find any explanation other than it should work... All advice I have found says to check the path, which I have and is correct. If I change the order in the settings.py list, it changes the call order. If the right call is on top for the right situation works as expected, but it will not call the specified backend - it just enumerates the list top to bottom.

Has anyone else run into this? Why is the backend parameter when I call authenticate not causing the call to use the backend I specify?

Thanks for any help.

BCBB


Solution

  • Firstly,

    The order of AUTHENTICATION_BACKENDS matters, so if the same username and password is valid in multiple backends, Django will stop processing at the first positive match.

    If a backend raises a PermissionDenied exception, authentication will immediately fail. Django won’t check the backends that follow. Auth Backend Django Docs

    As far as I have understood, you want separate endpoints for separate authenticate backend, and you are using django's authenticate function, and you are passing backend to it, by default the authenticate function loops through all the backends, and when it get's PermissionDenied it doesn't iterate anymore and directly throws the exception. Django's authenticate function implementation

    Here's Solution 1:

    Create a custom authenticate function.

    from django.contrib.auth import load_backend
    from django.contrib.auth import authenticate as django_authenticate
    from django.core.exceptions import PermissionDenied
    
    def your_custom_authenticate(request=None, **credentials):
        backend = credentials.get('backend')
        if backend:
            auth_backend = load_backend(backend)
            user = auth_backend.authenticate(request, **credentials)
            if not user:
                raise PermissionDenied("Authentication Failed")
        django_authenticate(request=request, **credentials)
    

    Then you can use this authenticate function based on your views

    class TwoFaLogin(APIView):
        permission_classes = (AllowAny,)
    
        def post(self, request):
            print("TwoFaLogin View")
            username = request.data['username']
            passwd = request.data['password']
            twofa_token = request.data['twofa_token']
    
            user = your_custom_authenticate(request, username=username, password=passwd, twofa_token=twofa_token, backend='user_profile.auth_backends.TOTPBackend')
    
    
    class StandardLogin(APIView):
        permission_classes = (AllowAny,)
    
        def post(self, request):
            print("StandardLogin View")
            username = request.data['username']
            passwd = request.data['password']
            user = your_custom_authenticate(request, username=username, password=passwd, backend='user_profile.auth_backends.StandardLoginBackend')
    
    

    In this solution, it will not iterate to the next authentication backend, and straight raise PermissionDenied.