How to replicate django authentication in several services - django

We have a web app using Django. We have the authentication system setup as described below. Now, we would like to take some part of the application to an independent service, but we would like to have the same valid token/authentication system. The new service will be using Django too, probably, so I would like to know if this is even possible or what options do I have to implement this behavior.
In the case that we do not use Django in the new service, there would be a way to still use the same logic to authenticate requests on the two services?
Authentication system
Right now, we are authenticating the requests using the rest_framework module, more precisely, the TokenAuthentication class.
This is the configuration:
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
...
'rest_framework',
'rest_framework.authtoken',
...
}
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated'
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'module.authentications.AuthorizedAuthentication',
)
}
And the code we use to authorize the requests:
import logging
from rest_framework.authentication import TokenAuthentication
from rest_framework.exceptions import AuthenticationFailed
class AuthorizedAuthentication(TokenAuthentication):
def authenticate(self, request):
response = TokenAuthentication.authenticate(self, request)
if response is None:
return None
return response
To authenticate in the views, we do something like this:
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class SomeView(APIView):
permission_classes = (IsAuthenticated,)
EDIT
The code is not complete so please do not comment about it, I have just copied to show you a simplified example about our configuration.

Related

How to use both simple jwt token authentication and BasicAuthentication?

I have an DRF api and I have implemented the simplejwt authentication system. It works well. It is usefull when I want to connect my api from external script (I don't need to store credential and just use the token).
However I also want to be able to use the DRF interface login when i reach my api from browser so I have implemented also the Basic and SessionAuthentication. Is it the good way to do that ?
in my settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
]
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
}
in my api views.py
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.decorators import permission_classes, authentication_classes
# Create your views here.
#api_view(['GET'])
##authentication_classes([SessionAuthentication, BasicAuthentication])
#permission_classes([IsAuthenticated])
def get_all(request):
# as a token is used, the user with this token is know in the requets
user = request.user
# show only mesures of user having the token provided
mesures = Mesure.objects.filter(user_id=user.id)
serializer = MesureSerializer(mesures, many=True)
return Response(serializer.data)
In my urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('mesures/', views.get_all),
path('mesure-add/', views.add_mesure),
path('token/', TokenObtainPairView.as_view(), name='obtain_tokens'),
path('token/refresh/', TokenRefreshView.as_view(), name='refresh_token'),
path('api-auth/', include('rest_framework.urls'))
]
As you can see I had to comment the #authentication_classes decorator to make it work for both with token and login. Do you believe this is a good way to proceed ?
You should be fine with this because as per the DRF documentation -
Because we now have a set of permissions on the API, we need to authenticate our requests to it if we want to edit any snippets. We haven't set up any authentication classes, so the defaults are currently applied, which are SessionAuthentication and BasicAuthentication.
Source: Authenticating with the API
Ref: Line 109: rest_framework/views.py and Line 40: rest_framework/settings.py

unable to make a successful call from android using retrofit with csrf token

I'm new to django and got struck with csrf tokens. I'm making a post request from android using retrofit to my django server which is using csrf protection. I had obtained the csrf token by making a get request first and then I'm passing this csrftoken from the body of POST request. However, my server is showing 'CSRF cookie not set' error. The server is responding well to the calls from POSTMAN but when I make calls from android, I get this error. I think there is some simple thing I'm missing, but I'm not able to figure it out.
Session based authorization is usually used in web-apps. In case of android apps which are backed by API.
So rather than you can do Token Based Authorization using rest_framework in Django.
In your settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', # <-- And here
],
}
Now migrate the migrations to the database.
python manage.py migrate
Run this command to generate token for the specific user.
python manage.py drf_create_token <username>
Now add this line to urls.py.
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
#Some other urls.
path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
]
Using this you can obtain token for any user by using its username & password by just passing them in request body.
So this will be our protected api. Add this class based view in your views.py
from rest_framework.permissions import IsAuthenticated,AllowAny # <-- Here
from rest_framework.views import APIView
from rest_framework.response import Response
class DemoData(APIView):
permission_classes = (IsAuthenticated,)
def post(self, request):
content = {'data': 'Hello, World!'}
return Response(content)
Now pass a header with the api name as 'Authorization' & value be like something 'Token 5a2b846d267f68be68185944935d1367c885f360'
This is how we implement Token Authentication/Authorization in Django.
For more info, click here to see official documentation.

django rest_framework viewset has no route

I started a new django application with django rest_framework. One thing is odd though - when I try the example from the quickstart it works fine: I get a route at http://localhost:8000/users/ that I can query. But it doesn't work with my own app which is as minimal as it could be. My route http://localhost:8000/listings/ is not available and I get no error. I'm using django 1.8.2 and djangorestframework 3.1.3.
settings.py:
#...
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_swagger',
'debug_toolbar',
'listing',
)
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
#...
urls.py:
from rest_framework import routers, serializers, viewsets
from listing.models import Listing
class ListingSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Listing
fields = ('description',)
class ListingViewSet(viewsets.ViewSet):
queryset = Listing.objects.all()
serializer_class = ListingSerializer
router = routers.DefaultRouter()
router.register(r'listings', ListingViewSet)
urlpatterns = router.urls
models.py:
from django.db import models
class Listing(models.Model):
description = models.TextField()
Edit:
The error:
Page not found (404)
Request Method: GET
Request URL: http://127.0.0.1:8000/listings/
Using the URLconf defined in djangoway.urls, Django tried these URL patterns, in this order:
^__debug__/
^$ [name='api-root']
^\.(?P<format>[a-z0-9]+)$ [name='api-root']
The current URL, listings/, didn't match any of these.
Edit:
Answered my question and took the code down, because there was nothing odd with it.
I should have used the ModelViewSet:
class ListingViewSet(viewsets.ModelViewSet):
^^^^^
Thanks to xordoquy for pointing it out in the issue I opened.

Unit Testing Django Rest Framework Authentication at Runtime

I basically want to turn TokenAuthentication on but only for 2 unit tests. The only option I've seen so far is to use #override_settings(...) to replace the REST_FRAMEWORK settings value.
REST_FRAMEWORK_OVERRIDE={
'PAGINATE_BY': 20,
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'rest_framework_csv.renderers.CSVRenderer',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
#override_settings(REST_FRAMEWORK=REST_FRAMEWORK_OVERRIDE)
def test_something(self):
This isn't working. I can print the settings before and after the decorator and see that the values changed but django doesn't seem to be respecting them. It allows all requests sent using the test Client or the DRF APIClient object through without authentication. I'm getting 200 responses when I would expect 401 unauthorized.
If I insert that same dictionary into my test_settings.py file in the config folder everything works as expected. However like I said I only want to turn on authentication for a couple of unit tests, not all of them. My thought is that Django never revisits the settings for DRF after initialization. So even though the setting values are correct they are not used.
Has anyone run into this problem and found a solution? Or workaround?
The following workaround works well for me:
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.authentication import TokenAuthentication
try:
from unittest.mock import patch
except ImportError:
from mock import patch
#patch.object(APIView, 'authentication_classes', new = [TokenAuthentication])
#patch.object(APIView, 'permission_classes', new = [IsAuthenticatedOrReadOnly])
class AuthClientTest(LiveServerTestCase):
# your tests goes here
Just thought I'd mention how I solved this. It's not pretty and if anyone has any suggestions to clean it up they are more than welcome! As I mentioned earlier the problem I'm having is documented here (https://github.com/tomchristie/django-rest-framework/issues/2466), but the fix is not so clear. In addition to reloading the DRF views module I also had to reload the apps views module to get it working.
import os
import json
from django.conf import settings
from django.test.utils import override_settings
from django.utils.six.moves import reload_module
from rest_framework import views as drf_views
from rest_framework.test import force_authenticate, APIRequestFactory, APIClient
from apps.contact import views as cm_views
from django.core.urlresolvers import reverse
from django.test import TestCase
from unittest import mock
REST_FRAMEWORK_OVERRIDE={
'PAGINATE_BY': 20,
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'rest_framework_csv.renderers.CSVRenderer',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
def test_authenticated(self):
with override_settings(REST_FRAMEWORK=REST_FRAMEWORK_OVERRIDE):
# The two lines below will make sure the views have the correct authentication_classes and permission_classes
reload_module(drf_views)
reload_module(cm_views)
from apps.contact.views import AccountView
UserModelGet = mock.Mock(return_value=self.account)
factory = APIRequestFactory()
user = UserModelGet(user='username')
view = AccountView.as_view()
# Test non existent account
path = self.get_account_path("1thiswillneverexist")
request = factory.get(path)
force_authenticate(request, user=user)
response = view(request, account_name=os.path.basename(request.path))
self.assertEquals(response.status_code, 200, "Wrong status code")
self.assertEqual(json.loads(str(response.content, encoding='utf-8')), [], "Content not correct for authenticated account request")
# Reset the views permission_classes and authentication_classes to what they were before this test
reload_module(cm_views)
reload_module(drf_views)
Wow that's annoying.
Here's a generic contextmanager that handles the reloading. Note that you can't import the subobject api_settings directly because DRF doesn't alter it on reload, but rather reassigns the module-level object to a new instance, so we just access it from the module directly when we need it.
from rest_framework import settings as api_conf
#contextmanager
def override_rest_framework_settings(new_settings):
with override_settings(REST_FRAMEWORK=new_settings):
# NOTE: `reload_api_settings` is a signal handler, so we have to pass a
# couple things in to get it working.
api_conf.reload_api_settings(setting="REST_FRAMEWORK", value="")
with mock.patch.multiple(
"rest_framework.views.APIView",
authentication_classes=api_conf.api_settings.DEFAULT_AUTHENTICATION_CLASSES,
):
yield
api_conf.reload_api_settings(setting="REST_FRAMEWORK", value="")
NOTE: If you're changing other aspects of the settings, you may also have to patch the following APIView attributes:
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
settings = api_settings

Why do I get "CSRF cookie not set" when POST to Django REST framework?

I am getting the error "CSRF cookie not set" returned when trying to POST to a simple test app using the Django REST framework. I've tried it with Django 1.4 and the Django 1.6.2. I am using the Django REST framework v 2.3.13.
I have tried using the #csrf_exempt decorator, but it doesn't help.
This is a very simple app, with no user registration / login etc.
Any ideas why I'm getting this error?
Update: I have updated my urls.py as shown below and it is now working!!
Here's my code:
urls.py
from django.conf.urls import patterns, url
from quickstart import views
urlpatterns = patterns('',
url(r'^api_add/$', views.api_add, name='api_add'),
)
views.py
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
#api_view(['POST'])
def api_add(request):
return Response({"test": 'abc'})
settings.py
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
)
post.sh
curl -X POST -H "Content-Type: application/json" -d '
{
"name": "Manager",
"description": "someone who manages"
}' http://127.0.0.1:8000/api_add/
Use the #csrf_exempt-decorator:
from django.views.decorators.csrf import csrf_exempt
#api_view(['POST'])
#csrf_exempt
def api_add(request):
return Response({"test": 'abc'})
Update:
If you never need csrf-checks, remove the middleware. Seach for MIDDLEWARE_CLASSES in settings.py and remove 'django.middleware.csrf.CsrfViewMiddleware',.
Django-Rest-Framework automatically adds #csrf_exempt to all APIView (or #api_view).
Only exception is the SesssionAuthentication which forces you (correctly) to use CSRF, see the docs on CSRF or the DRF source
I solved this like this:
#api_view(['POST'])
#csrf_exempt
def add(request):
....
to:
#csrf_exempt
#api_view(['POST'])
def add(request):
.....
I had the similar issue. I tried using #csrf_exempt but it did not work.
I changed ALLOWED_HOSTS = '*' to ALLOWED_HOSTS = [] and it worked for me on local.