Testing a custom auth backend with Django RestFramework - django

I created a custom authentication backend for my DRF application.
I can't figure out how to test it.
Calling the client.post calls my authenticate function (cause that's in my view)
But I need to mock an internal method in my ModelBackend.
Kinda confused how to go about this?
View:
class Web3UserToken(APIView):
authentication_classes = []
permission_classes = []
def post(self, request, **kwargs):
public_address = request.data["public_address"]
web3 = Web3Backend()
user, token = web3.authenticate(request)
if token:
return JsonResponse({'token': token})
else:
return Response({'message': 'Missing token'}, status=400)
Test:
class TestWeb3AuthBackend(APITestCase):
def setUp(self):
#TODO: set up test user
self.client = APIClient()
#self.factory = APIRequestFactory()
def test_authenticatepasseswithexistinguser(self):
self.user = Web3User(public_address=TEST_PUBLIC_ADDRESS)
auth_backend = Web3Backend()
import ipdb; ipdb.sset_trace()
request = self.client.post('/api/token/', {'public_address': TEST_PUBLIC_ADDRESS, 'nonce': '0xsomething_random'},follow=True)
with mock.patch.object(auth_backend, '_check_nonce', return_value=True) as method:
token, user = auth_backend.authenticate(request)
self.assertTrue(token)
self.assertTrue(user)

I suggest using RequestFactory for creating a request and passing it to authenticate method, instead of sending a request via Django's test client. This is a unit test and its aim is to test authenticate method of Web3Backend. You don't need to test this functionality through an api call.

Related

Url not parsed when testing a ModelViewSet

I am trying to test a Django Rest Framework view. When I call my endpoint from a real api client, pk is correctly set. When it is called from the test, pk is None.
class ResourceViewSet(ModelViewSet):
serializer_class = ResourceSerializer
#action(detail=True)
def foo(self, request, pk=None):
print(pk) # None when called by the test
def test_foo(client: Client, db):
request = factory.post(f'/api/resource/1/foo/')
view = ResourceViewSet.as_view({'post': 'foo'})
response = view(request)
How should I fix my test?
When testing the view directly as you are doing, you are bypassing the url resolving/mapping logic. Therefore, you should pass the parameters as args/kwargs, in the end you are calling the foo function:
def test_foo(client: Client, db):
request = factory.post(f'/api/resource/1/foo/')
view = ResourceViewSet.as_view({'post': 'foo'})
response = view(request, pk=1)
If you'd like to test the whole stack, also the urls, I'd recommend you use the APIClient.

django-axes is not getting the request argument

I recently added django-axes to my Django project. It is suppose to work out the box with django-restframework. However, I am using django-rest-framework-simplejwt to handle authentication. But it should still work out the box since the only thing that is required for django-axes is passing Django's authentication method the request object which it does in it's source code (line 39 and 43).
When I try to authenticate, I get this error from django-axes:
axes.exceptions.AxesBackendRequestParameterRequired: AxesBackend requires a request as an argument to authenticate
You need to add requests to the authentication function. See sample code below.
serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def _authenticate_user_email(self, email, password, request):
# This is key: Pass request to the authenticate function
self.user = authenticate(email=email, password=password, request=request)
return self.user
def validate(self, attrs):
password = attrs.get('password')
email = attrs.get('email')
request = self.context.get('request') # <<=== Grab request
self.user = self._authenticate_user_email(email, password, request)
# All error handling should be done by this code line
refresh = self.get_token(self.user)
data = {}
data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)
return data
views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from authy.serializers import MyTokenObtainPairSerializer
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
urls.py
from authy.views import MyTokenObtainPairView
url(r'^/auth/api/token/$', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
It is also worth mentioning that the simple jwt lib uses the authenticate function, however it does not pass the request parameter. Therefore you need call authenticate, get_token and return data object yourself.
In addition, if you have extended the AbstractBaseUser model of django. And set the USERNAME_FIELD. Then use the param username instead of email. E.g: authenticate(username=email, password=password, request=request)
Use this:
from axes.backends import AxesBackend
class MyBackend(AxesBackend)
def authenticate(self, request=None, *args, **kwargs):
if request:
return super().authenticate(request, *args, **kwargs)
This would skip the AxesBackend in AUTHENTICATION_BACKENDS if the request is unset and would weaken your security setup.
source: https://github.com/jazzband/django-axes/issues/478

Unit Test Using Forcing Authentication Django Rest Framework

I'm building RESTful API services using django rest framework, I've reached the point where i have to create an automated test for my RESTful API services.
The sessionList api require token authentication, in case the user doesn't have the token he won't be able to access the session collection.
The api worked fine when I've tested it using POSTMAN and the real browser.
SessionList:
class SessionList(generics.ListCreateAPIView):
authentication_classes = [TokenAuthentication, ]
permission_classes = [IsAuthenticated, ]
throttle_scope = 'session'
throttle_classes = (ScopedRateThrottle,)
name = 'session-list'
filter_class = SessionFilter
serializer_class = SessionSerializer
ordering_fields = (
'distance_in_miles',
'speed'
)
def get_queryset(self):
return Session.objects.filter(owner=self.request.user)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Then i've created an automated test using DRF test
RunningSessionTest:
class RunningSessionTest(APITestCase):
def test_get_sessions(self):
factory = APIRequestFactory()
view = views.SessionList.as_view()
user = User.objects.create_user(
'user01', 'user01#example.com', 'user01P4ssw0rD')
request = factory.get('http://localhost:8000/sessions/')
force_authenticate(request, user=user)
response = view(request)
assert Response.status_code == status.HTTP_200_OK
def test_get_sessions_not_authenticated_user(self):
factory = APIRequestFactory()
view = views.SessionList.as_view()
user = User.objects.create_user(
'user01', 'user01#example.com', 'user01P4ssw0rD')
request = factory.get('http://localhost:8000/sessions/')
response = view(request)
assert Response.status_code == status.HTTP_401_UNAUTHORIZED
The issue: in both cases, if the user has the token or not the response value is HTTP_200_OK
I've tried to solve the problem by trying different methods to implement the test. I've used APIRequestFactory, Also i've used the APIClient but i got the same result. To be honest after reading the document many times i couldn't understand the differences between the APIClient and the APIRequestFactory.
The result of the test :
Traceback (most recent call last):
File "C:\python_work\DjnagoREST\01\restful01\RunKeeper\tests.py", line 67, in test_get_sessions_not_authenticated_user
assert Response.status_code == status.HTTP_401_UNAUTHORIZED
AssertionError
I will be grateful to your help.
I guess you will need to change Response.status_code to response.status_code.
As it turned out Response.status_code (as from rest_framework.response.Response)
is equal to 200 :D

request.POST returns old values after updating it in custom middleware - django 1.11.9

I am using django 1.11.9
I want to add client_id and client_secret to the django POST request.
Here is how my middleware.py file looks like:
class LoginMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# auth_header = get_authorization_header(request)
# Code to be executed for each request before
# the view (and later middleware) are called.
#Add Django authentication app client data to the request
request.POST = request.POST.copy()
request.POST['client_id'] = '12345678'
request.POST['client_secret'] = '12345678'
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
Middleware is being successfully processed when I check it with a debugger. Thought when a view is called the 'client_id' and 'client_secret' fields are missing in the request.
After some experimenting i figure out that request is not getting updated and when it is called in a different view, it returns old values.
I am later using request in rest_framework_social_oauth2. And this is the point when 'client_id' and 'client_secret' disappear.
class ConvertTokenView(CsrfExemptMixin, OAuthLibMixin, APIView):
"""
Implements an endpoint to convert a provider token to an access token
The endpoint is used in the following flows:
* Authorization code
* Client credentials
"""
server_class = SocialTokenServer
validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
oauthlib_backend_class = KeepRequestCore
permission_classes = (permissions.AllowAny,)
def post(self, request, *args, **kwargs):
import pdb ; pdb.set_trace()
# Use the rest framework `.data` to fake the post body of the django request.
request._request.POST = request._request.POST.copy()
for key, value in request.data.items():
request._request.POST[key] = value
url, headers, body, status = self.create_token_response(request._request)
response = Response(data=json.loads(body), status=status)
for k, v in headers.items():
response[k] = v
return response
I need to add client_id and client_secret to the request body, so it can be later used by rest_framework_social_oauth2.
What could be the problem? How to properly update the request?
As you're working with request and processing a request, you have to implement process_request method, so the result will be something like:
class LoginMiddleware(object):
def process_request(self, request):
request.session['client_id'] = '12345678'
and then in your view:
def your_view(request):
client_id = request.session['client_id']

Django test client http basic auth for post request

everyone. I am trying to write tests for RESTful API implemented using django-tastypie with http basic auth. So, I have the following code:
def http_auth(username, password):
credentials = base64.encodestring('%s:%s' % (username, password)).strip()
auth_string = 'Basic %s' % credentials
return auth_string
class FileApiTest(TestCase):
fixtures = ['test/fixtures/test_users.json']
def setUp(self):
self.extra = {
'HTTP_AUTHORIZATION': http_auth('testuser', 'qwerty')
}
def test_folder_resource(self):
response = self.client.get('/api/1.0/folder/', **self.extra)
self.assertEqual(response.status_code, 200)
def test_folder_resource_post(self):
response = self.client.post('/api/1.0/folder/', **self.extra)
self.assertNotEqual(response.status_code, 401)
GET request is done well, returning status code 200. But POST request always returns 401. I am sure I am doing something wrong. Any advice?
Check out this question. I've used that code for tests using both GET and POST and it worked. The only difference I can see is that you have used base64.encodestring instead of base64.b64encode.
Otherwise, if that doesn't work, how are you performing the HTTP Authentication? I wrote and use this function decorator:
import base64
from django.http import HttpResponse
from django.contrib.auth import authenticate, login
def http_auth(view, request, realm="", must_be='', *args, **kwargs):
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2:
if auth[0].lower() == "basic":
uname, passwd = base64.b64decode(auth[1]).split(':')
if must_be in ('', uname):
user = authenticate(username=uname, password=passwd)
if user is not None and user.is_active:
login(request, user)
request.user = user
return view(request, *args, **kwargs)
# They mustn't be logged in
response = HttpResponse('Failed')
response.status_code = 401
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
return response
def http_auth_required(realm="", must_be=''):
""" Decorator that requires HTTP Basic authentication, eg API views. """
def view_decorator(func):
def wrapper(request, *args, **kwargs):
return http_auth(func, request, realm, must_be, *args, **kwargs)
return wrapper
return view_decorator
I've found a reason of my problem. DjangoAuthorization checks permissions with django premissions framework, since I don't use it in my project — all post/put/delete requests from non superuser are unauthorized. My bad.
Anyway, thanks a lot to you, guys, for responses.
On Python 3
#staticmethod
def http_auth(username, password):
"""
Encode Basic Auth username:password.
:param username:
:param password:
:return String:
"""
data = f"{username}:{password}"
credentials = base64.b64encode(data.encode("utf-8")).strip()
auth_string = f'Basic {credentials.decode("utf-8")}'
return auth_string
def post_json(self, url_name: AnyStr, url_kwargs: Dict, data: Dict):
"""
Offers a shortcut alternative to doing this manually each time
"""
header = {'HTTP_AUTHORIZATION': self.http_auth('username', 'password')}
return self.post(
reverse(url_name, kwargs=url_kwargs),
json.dumps(data),
content_type="application/json",
**header
)