8

I've made an authentication class just like that :

Token Authentication for RESTful API: should the token be periodically changed?

restapi/settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        # 'rest_framework.authentication.TokenAuthentication',
        'restapi.authentication.ExpiringTokenAuthentication',
    ),
    'PAGINATE_BY': 10
}

restapi/authentication.py

import datetime
from rest_framework.authentication import TokenAuthentication

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        # This is required for the time comparison
        utc_now = datetime.utcnow()
        utc_now = utc_now.replace(tzinfo=pytz.utc)

        if token.created < utc_now - timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return token.user, token

restapi/tests.py

def test_get_missions(self):
    """
    Tests that /missions/ returns with no error
    """
    response = self.client.get('/missions/', HTTP_AUTHORIZATION = self.auth)

In my tests, I have an exception AttributeError: 'WSGIRequest' object has no attribute 'successful_authenticator'

Why am I having this error? How to fix it?

Community
  • 1
  • 1
Benjamin Toueg
  • 10,511
  • 7
  • 48
  • 79

2 Answers2

6

The problem comes from the line:

utc_now = datetime.utcnow()

which causes AttributeError: 'WSGIRequest' object has no attribute 'successful_authenticator'.

It's been a while since I've stumbled upon such a misleading error message.

Here is how I solved it:

restapi/authentication.py

import datetime
from django.utils.timezone import utc
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        utc_now = datetime.datetime.utcnow().replace(tzinfo=utc)

        if token.created < utc_now - datetime.timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return (token.user, token)
Benjamin Toueg
  • 10,511
  • 7
  • 48
  • 79
  • I had a similar problem caused trying to access `user.username` on a custom user model – Alex L Mar 18 '14 at 10:52
  • I just encountered this kind of issue as well, that is, a general exception in authentication. It looks like the exception gets masked as a successful authentication but without request.user and auth objects set. – jacob Dec 31 '14 at 17:04
  • Guys, similar problem [here](http://stackoverflow.com/questions/28427180/wsgirequest-object-has-no-attribute-auth) can you help? – Jahongir Rahmonov Feb 10 '15 at 08:44
  • Then similar solution, have you tried changing your naive datetime object? – Benjamin Toueg Feb 10 '15 at 08:52
5

For me this had nothing to do with the custom authentication classes but with how the method calls defined in my dispatch() function were handled. For example, I had the following setup:

class MyView(GenericAPIView):
    queryset = MyModel.objects.all()
    lookup_field = 'my_field'

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super().dispatch(request, *args, **kwargs)

The self.get_object function calls two things I was interested in. Mainly the self.check_object_permissions function and the get_object_or_404 function. For me it went wrong in the check_object_permissions function if it would return False. This would result in a call to self.permission_denied() which would then try to access the successful_authenticator attribute on the request. But because the request is not set to a rest_framework.request.Request the attribute would not exist. This is because the dispatch method sets this correct request during it's progression. Since this is done after the self.get_object() call it never gets set correctly.

Personally this quite surprised me since the dispatch() function is usually used to determine if certain critical things are true or not (i.e. if the object exists and if the user has permission). If that's all fine we continue with handling the request. But apparently there's no way to do this with this view. The only way I could resolve it was to move the self.get_object() call to my get() method like so:

class MyView(GenericAPIView):
    queryset = MyModel.objects.all()
    lookup_field = 'my_field'

    def get(self, request, *args, **kwargs):
        my_object = self.get_object()

I also tried moving around manual calls to self.check_object_permissions and get_object_or_404 in the dispatch() function, but to no avail. One requires the other (permission check needs to object, and to get the object you have to do the 404 check). And trying to call the super before the permission and 404 check means that the get() function will get called first, basically negating the effect you were trying to get in the dispatch function in the first place. So as far as I can tell this is the only way to resolve it in this case.

Also see this issue on django-rest-framework for some interesting information: https://github.com/encode/django-rest-framework/issues/918

Bono
  • 4,757
  • 6
  • 48
  • 77