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?
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)
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With