Saurav Kumar
Saurav Kumar

Reputation: 583

Django Calling Class Based Mixin from Another Class Based Mixin

My code is having two mixins, BasicAuthMixin and JWTAuthMixin as mentioned below. Just assume that self.authenticate method returns True and doesn't raise any exception:

from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View

class BasicAuthMixin(View):

    """
    Add this mixin to the views where Basic Auth is required.
    """
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        try:
            self.authenticate(request)
        except:
            return JsonResponse({'status': 403, 'message': 'Forbidden'}, status=403, content_type='application/json')
        return super(BasicAuthMixin, self).dispatch(request, *args, **kwargs)

class JWTAuthMixin(View):
    """
    Add this mixin to the views where JWT based authentication is required.
    """
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        try:
            self.authenticate(request)
        except:
            return JsonResponse({'status': 403, 'message': 'Forbidden'}, status=403, content_type='application/json')
        return super(JWTAuthMixin, self).dispatch(request, *args, **kwargs)

These mixins are being used in the views based upon the authentication needed.

The actual problem begins from here: I'm trying to create another mixin AllAuthMixin which when included in any view will automatically determine which mixins need to be called based upon the Authentication Header provided:

class AllAuthMixin(View):

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        auth = request.META.get('HTTP_AUTHORIZATION') or ''
        if auth.startswith('Bearer'):
            return JWTAuthMixin.as_view()(request, *args, **kwargs)
        elif auth.startswith('Basic'):
            return BasicAuthMixin.as_view()(request, *args, **kwargs)
        raise Exception('Unauthorized Access to Saurav APIs', 403)

Once I include AllAuthMixin in any of the view say /test it actually calls the appropriate Mixins but returns Method Not Allowed (GET): /test

I debugged and found that Method Not Allowed error message is coming from below line if I use basic auth:

return super(BasicAuthMixin, self).dispatch(request, *args, **kwargs)

Following illustrates a very simple example to call my view with basic auth:

>>> import requests
>>> requests.get('http://127.0.0.1:8000/test', auth=('UserName', 'Password'))
<Response [405]>

I'm not sure what I'm doing wrong here. Can anyone please help me figure it out the issue or any alternate way to achieve this. What I want is to re-use already declared mixins: BasicAuthMixn and JWTAuthMixin.

Upvotes: 3

Views: 999

Answers (1)

Yeray Diaz
Yeray Diaz

Reputation: 1770

There's a design issue here, both mixins are implemented intercepting the dispatch method and calling super. The way you're implementing AllAuthMixin by also calling dispatch means you need to have them both in its MRO and "trick" super into choosing the appropriate one which is not a good idea.

An alternative way of implementing AllAuthMixin is to not call dispatch but instantiate and call authenticate on them:

class AllAuthMixin(View):
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        auth = request.META.get('HTTP_AUTHORIZATION') or ''
        try:
            if auth.startswith('Bearer'):
                JWTAuthMixin().authenticate(request)  # raises on failed auth
            elif auth.startswith('Basic'):
                BasicAuthMixin().authenticate(request)
        except:
            raise Exception('Unauthorized Access to Saurav APIs', 403)

        return super(AllAuthMixin, self).dispatch(request, *args, **kwargs)

A nicer way to reuse code would be to separate the authentication into its own class and make individual mixins that make use of them. That way you'd have better separation of concerns.

Something like:

class BasicAuth(object):
    def authenticate(self, request):
        # raise if not authed
        print("Basic auth")

class JWTAuth(object):
    def authenticate(self, request):
        # raise if not authed
        print("JWT auth")

class AuthMixin(View):
    def authenticate(self, request):
        raise NotImplementedError('Implement in subclass')

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        try:
            self.authenticate(request)
        except:
            return JsonResponse({'status': 403, 'message': 'Forbidden'}, status=403)

        return super(AuthMixin, self).dispatch(request, *args, **kwargs)

class BasicAuthMixin(BasicAuth, AuthMixin):
    pass

class JWTAuthMixin(JWTAuth, AuthMixin):
    pass

class AllAuthMixin(AuthMixin):
    def authenticate(self, request):
        auth = request.META.get('HTTP_AUTHORIZATION') or ''
        try:
            if auth.startswith('Bearer'):
                return JWTAuth().authenticate(request)
            elif auth.startswith('Basic'):
                return BasicAuth().authenticate(request)
        except:
            return JsonResponse({'status': 403, 'message': 'Other'}, status=403)

class SomeView(AllAuthMixin, View):
    def get(self, request):
        return JsonResponse({'status': 200, 'message': 'OK'})

-- Original answer --

You're calling as_view for each mixin in AllAuthMixin, by calling as_view()(request, *args, *kwargs) you're forcing the mixin to respond to the request but since it doesn't have a get method it returns 405 Method not allowed as described in the docs.

You should be calling dispatch and also make AllAuthMixin inherit from both child mixins to properly pass self to dispatch. Like so:

class AllAuthMixin(JWTAuthMixin, BasicAuthMixin):
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        auth = request.META.get('HTTP_AUTHORIZATION') or ''
        if auth.startswith('Bearer'):
            return JWTAuthMixin.dispatch(self, request, *args, **kwargs)
        elif auth.startswith('Basic'):
            return BasicAuthMixin.dispatch(self, request, *args, **kwargs)
        raise Exception('Unauthorized Access to Saurav APIs', 403)

class SomeView(AllAuthMixin, View):
    def get(self, request):
        return JsonResponse({'status': 200, 'message': 'OK'})

Upvotes: 2

Related Questions