Djoser for DRF with Knox tokens - django-rest-framework

I'm trying to use djoser with token authentication, but using django-rest-knox tokens.
I have set the TOKEN_MODEL to knox.models.AuthToken, and the rest framework's DEFAULT_AUTHENTICATION_CLASSES to knox.auth.TokenAuthentication.
I naïvely thought that this would be enough, but it seems that Djoser's inbuilt serializers (create token, and token), don't work properly with the knox tokens. I tried overriding them with custom serializers, but I didn't get anywhere (which is not to say it's not possible, just that I'm bad at this).
It occurred to me that perhaps I should try using Knox's own login views... Is that possible, or can they not be mixed like that? (I'm mainly asking because I don't want to get it to 'work', but find that I've actually introduced a security hole in doing so).
Settings:
DJOSER = {
"TOKEN_MODEL": "knox.models.AuthToken",
"SERIALIZERS": {"token": "users.serializers.TokenSerializer"},
}
Where users.serializers.TokenSerializer is:
class TokenSerializer(serializers.ModelSerializer):
auth_token = serializers.CharField(source="token_key")
class Meta:
model = settings.TOKEN_MODEL
fields = ("auth_token",)
This is only slightly modified from the original Djoser TokenSerializer. It was throwing an error that AuthToken objects did not have a key attribute. Knox tokens seem to call it token_key, so I replaced the line:
auth_token = serializers.CharField(source="key") with auth_token = serializers.CharField(source="token_key")
Now, it doesn't throw an error, but it returns an empty token. Inspecting the actual db shows that it has saved a token with the correct user and creation time, but with 'null' for digest, salt, and token_key

Yes, it is possible to mixin's Djoser's and knox's additional view point. For that we are going to create an app name auth from where we are going to serve all authenticational related end-points. Now our project structure is like
MainProject
-auth
--__init__.py
--urls.py
-mainapp
....
Now in our auth app's urls we are going to serve our necessary end-points for authentication. For that we are going to take help from Djoser's urls link and Knox's urls link
And our auth's urls.py will be like following
from django.conf.urls import url, include
from django.contrib.auth import get_user_model
from djoser import views as djsoer_views
from knox import views as knox_views
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('users', djsoer_views.UserViewSet)
User = get_user_model()
djoser_urlpatterns = [
url(
r'^users/create/?$',
djsoer_views.UserCreateView.as_view(),
name='user-create'
),
url(
r'^users/delete/?$',
djsoer_views.UserDeleteView.as_view(),
name='user-delete'
),
url(
r'^users/activate/?$',
djsoer_views.ActivationView.as_view(),
name='user-activate'
),
url(
r'^{0}/?$'.format(User.USERNAME_FIELD),
djsoer_views.SetUsernameView.as_view(),
name='set_username'
),
url(r'^password/?$', djsoer_views.SetPasswordView.as_view(), name='set_password'),
url(
r'^password/reset/?$',
djsoer_views.PasswordResetView.as_view(),
name='password_reset'
),
url(
r'^password/reset/confirm/?$',
djsoer_views.PasswordResetConfirmView.as_view(),
name='password_reset_confirm'
),
url(r'^$', djsoer_views.RootView.as_view(), name='root'),
url(r'^', include(router.urls)), ### If you want to add user view set
]
knox_urlpatterns = [
url(r'login/', knox_views.LoginView.as_view(), name='knox_login'),
url(r'logout/', knox_views.LogoutView.as_view(), name='knox_logout'),
url(r'logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'),
]
urlpatterns = knox_urlpatterns + djoser_urlpatterns
Now we are going to add this urls under our main_app's urls
from django.urls import path
from django.conf import settings
auth_urls = include('auth.urls')
urlpatterns = [
path('api/auth/', auth_urls),
......
]
Now we are going to able to access every end-point like login as api/auth/login/ or user-create as api/auth/user/create/ etc.
What i can see that djoser add some additional UserViewset end-point by default mostly you may not like this , you should include what is really needed for you.

Related

How to use django groups and perms in django-rest-framework

How to handle complex required permissions in any API using django-rest-framework?
For example, you have three tier support operators who have access to APIs, but they should only have access to respective endpoint and as the last tier, superadmin should have everything in hands.
How to solve this simple problem by django group and permissions?
There are lots of answers being related to AND or OR the perms in permission_classes like
def get_permissions(self):
return (
IsAuthenticated & (IsCommitteeHead|IsCommitteeMember|IsAdminUser)
)()
or
permission_classes = [IsAuthenticated &(IsCommitteeHead|IsCommitteeMember|IsAdminUser)]
but I think hard coding always makes the code a complete mess!
Create new permission model permission from django base model permission like this:
from rest_framework.permissions import DjangoModelPermissions
class FullDjangoModelPermissions(DjangoModelPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
And in view class like this:
class SomeAwuful(GenericAPIView):
...
permission_classes = [FullDjangoModelPermissions]
...
Then you can go in Django admin dashboard and set who has access to what, or creating group to access features, like this:

How can i use Simple JWT for custom User Model

I am managing my User Model (for customers), but i don't know how can i use simple-jwt for my Customer Model with it's custom Login View.
models.py
from django.db import models
class Customer(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(max_length=100)
password = models.CharField(max_length=100)
and also, i want to manage my customer session by saving the Refresh Token,
can anyone please tell me how can i achieve that.
After some research, I came to the conclusion that Simple JWT doesn't have any special support for user models that are not derived from AbstactUserModel.
It means if you are managing the Customer model, either you need to extend it from AbstactUserModel or you should use jwt instead of Simple jwt.
First install simple jwt with the command
pip install djangorestframework-simplejwt
You must then configure your django project to use the libary with following option in settings.py
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
...
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
...
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': settings.SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
In your root urls.py add the following
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
...
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
...
]
Simple JWT by default only sends user id in the token. You can customize it further to send more information in your serializers.py by doing the following
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""Customizes JWT default Serializer to add more information about user"""
#classmethod
def get_token(cls, user):
token = super().get_token(user)
token['name'] = user.name
token['email'] = user.email
token['is_superuser'] = user.is_superuser
token['is_staff'] = user.is_staff
return token
Then in your views.py add the following
from users import serializers
class CustomTokenObtainPairView(TokenObtainPairView):
# Replace the serializer with your custom
serializer_class = serializers.CustomTokenObtainPairSerializer
You can read more here
I could fix this problem adding 'rest_framework_simplejwt' to INSTALLED_APPS after the app that has the custom user model.
in your views.py file, import this
from rest_framework_simplejwt.tokens import RefreshToken
next, create a refresh token in def post - the one you use for logging in
def post(self, request):
...
refresh = RefreshToken.for_user(user)
...
return the generated refresh token and access token in the response body
return Response(
{
'refresh': str(refresh),
'access': str(refresh.access_token),
},
status = status.HTTP_200_OK
)
What I have suggested is a simple and super basic implementation of what I believe you are looking for. I got it from here. But if you want a better and more customized, neater implementation, refer this article.

Getting user details from access token in Django rest framework -simple JWT

I am using React and Django rest framework for a project. I use Django rest framework simple JWT for authentication. Now, I want to display the username in the navbar after the user logs in. So, is there a way in simple JWT for returning user details from the access token generated after authentication, just like Djoser returns user credentials when supplied the access token?
Sorry if this question is silly but I was not able to find a solution to this anywhere.
if you want to obtain the information of the owner of the token you can consult it in REQUEST.
class ViewProtect(APIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def post(self, request, format=None):
token_user_email = request.user.email
token_user_username = request.user.username
pass
About the backend, basically I use this library
from restframework_simplejwt.tokens import AccessToken
The function AccessToken() take as input the string access_token_str and return the object access_token_obj.
To get the user_id, you can use the instruction
user_id=access_token_obj['user_id'].
In the following example I have created the function
get_user_from_access_token_in_django_rest_framework_simplejwt().
This function is just a wrapper around AccessToken()
Full code:
#Path current file
#/blabla/django/project004/core/view.py
from restframework_simplejwt.tokens import AccessToken
from django.contrib.auth.models import User
#Example data.
#access_token_str = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU'
def get_user_from_access_token_in_django_rest_framework_simplejwt(access_token_str):
access_token_obj = AccessToken(access_token_str)
user_id=access_token_obj['user_id']
user=User.objects.get(id=user_id)
print('user_id: ', user_id )
print('user: ', user)
print('user.id: ', user.id )
content = {'user_id': user_id, 'user':user, 'user.id':user.id}
return Response(content)
Credits:
#davesque;
https://github.com/jazzband/djangorestframework-simplejwt/issues/140
Update.
The string access_token_str I write in the file is just an example. You should pass it as argument.
Here's how I've done it.
On Django, I followed the steps described on this page in order to add the user's name inside the JWT token : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/customizing_token_claims.html
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
#classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add name to token
token['name'] = user.get_full_name()
# You can add other information into the token here
return token
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
Then, I updated my urls.py to use the custom view:
path('token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
Finally, in my Vue.js application, I installed the jwt-decode package and used it like this:
const token = localStorage.getItem('access_token');
const decoded = jwt_decode(token);
console.log(decoded)
// decoded.name contains the user's full name
Note that I store the access token in the local storage beforehand.

Parameters URL with DRF routers

I'm using Django Rest Framework for created a API. In this project i want to capture parameters in the URL. For example i want to capture the username and password of a user and my idea is like this:
http://localhost:8000/accounts/login/?unsername=username&password=password
But i cant, I' usin routers and django-filter, but i cant get the parameters url. My project files are there:
view.py:
class AccountsData(viewsets.ModelViewSet):
queryset = models.UserData.objects.all()
serializer_class = serializers.AccountsDataSerializer
permission_classes = (IsAuthenticated,)
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ['username', 'password']
lookup_url_kwarg = 'username'
#action(methods=['get'], detail=True, url_name='login', url_path='login')
def login(self, request, pk=None):
return Response({"Login successfully"}, 200)
urls.py:
from api import views
router = routers.SimpleRouter()
router.register(r'accounts', views.AccountsData)
Request query parameters have nothing to do with routing, they are passed with the request independently of how you configure the route. You have access to them in request.query_params, for example, request.query_params.get('username') would get the value of the username parameter.
Being said that, your idea has a terrible mistake: password or any kind of confidential data should NEVER go in query parameters, you should use an http verb that carries the data in its body (POST, for example).

How to disable the browsable API for non staff users (is_staff=False)?

in my case I am using Django REST Framework (DRF) as internal api. it is not intended to be consumed by regular users. therefore I would like to disable it for regular users.
an admin (is_staff=True) should be able to access it and see it:
https://restframework.herokuapp.com/
a non staff user (is_staff=False) should just get the JSON response of a GET request like:
https://restframework.herokuapp.com/?format=json
he should not(!) see the browsable api. this applies for the root view and all endpoints.
to configure this, I applied the following:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication'],
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
}
my endpoints are the following (to keep the example simple I just show 1):
# api/urls.py
from django.urls import include, path
from rest_framework import routers
from . import views
app_name = 'api'
router = routers.DefaultRouter() # browseable api
router.register('segments', views.SegmentViewSet)
# there are a lot more...
urlpatterns = [
path('', include(router.urls)),
]
based on answer https://stackoverflow.com/a/58894198/420953 my settings.py looks like this:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication'],
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
# enable JSON renderer by default
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
and my api/views.py:
# api/views.py
from django_filters import rest_framework as drf_filters
from rest_framework import filters, renderers, viewsets
from . import serializers
from segment.models import Segment
class StaffBrowsableAPIMixin:
def get_renderers(self):
"""
add BrowsableAPIRenderer if user is staff (regular users see JSONRenderer response)
"""
# explicitly set renderer to JSONRenderer (the default for non staff users)
rends = [renderers.JSONRenderer]
if self.request.user.is_staff:
# staff users see browsable API
rends.append(renderers.BrowsableAPIRenderer)
return [renderer() for renderer in rends]
class SegmentViewSet(StaffBrowsableAPIMixin, viewsets.ReadOnlyModelViewSet):
queryset = Segment.objects.all()
serializer_class = serializers.SegmentSerializer
this works fine for all endpoints (when a regular user calls the endpoint via GET, they only see the JSON, not the browsable API). Unfortunately it does not work for APIRootView (the root view of the api, e.g. https://restframework.herokuapp.com/).
how to get this to work for APIRootView as well?
I believe you can lock the base URL of your API down pretty simply (the mixin should probably be moved to another file but just kept everything together for clarity):
# api/urls.py
from django.urls import include, path
from rest_framework import permissions, renderers, routers
from . import views
app_name = 'api'
class StaffBrowsableAPIMixin:
def get_renderers(self):
"""
add BrowsableAPIRenderer if user is staff (regular users see JSONRenderer response)
"""
# explicitly set renderer to JSONRenderer (the default for non staff users)
rends = [renderers.JSONRenderer]
if self.request.user.is_staff:
# staff users see browsable API
rends.append(renderers.BrowsableAPIRenderer)
return [renderer() for renderer in rends]
class CustomAPIRootView(StaffBrowsableAPIMixin, routers.APIRootView):
permission_classes = (permissions.IsAdminUser,)
class CustomDefaultRouter(routers.DefaultRouter):
APIRootView = CustomAPIRootView
router = CustomDefaultRouter() # browseable api
router.register('segments', views.SegmentViewSet)
# there are a lot more...
urlpatterns = [
path('', include(router.urls)),
]
The permission_classes will handle not showing any of your endpoints to non-Admin users but the Browsable API template will still be shown. To remove that as well, you need to change the renderer using the StaffBrowsableAPIMixin.
Original Answer
One way to do this is using DRF's renderer settings and methods.
In your settings.py:
REST_FRAMEWORK = {
# Only enable JSON renderer by default.
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
And in your views.py:
from rest_framework import generics, renderers
class StaffBrowsableMixin(object):
def get_renderers(self):
"""
Add Browsable API renderer if user is staff.
"""
rends = self.renderer_classes
if self.request.user and self.request.user.is_staff:
rends.append(renderers.BrowsableAPIRenderer)
return [renderer() for renderer in rends]
class CustomListApiView(StaffBrowsableMixin, generics.ListAPIView):
"""
List view.
"""
# normal stuff here
Basically, use StaffBrowsableMixin for any APIView you want the BrowsableAPI to be enabled for staff.
Similar question, as linked above in comments, and my answer there as well: https://stackoverflow.com/a/58762483/4599228

Resources