7

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?

4

2 回答 2

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)
于 2013-05-24T16:11:20.693 回答
4

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

于 2018-08-08T13:14:47.430 回答