Django Rest API Djoser Refresh and Access Token Problem - python

I am trying to get access and refresh tokens when the user registers using the /auth/users/ endpoint.
I have already extended the serializer and it is showing all custom fields. It registers the user successfully and returns the result as follows:
{
"mobile": "12345",
"driving_id": "478161839",
"full_name": "John Doe",
}
This is where I would want to have an access and refresh token. I read that djoser uses django simple jwt library to provide access and refresh tokens. This is the function to create the tokens manually which I am able to do but the only thing I am not understanding is where to return the data with other details.
from rest_framework_simplejwt.tokens import RefreshToken
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
}

I solved it but I do not know if it is the best way or not. I imported the userview set from djoser.views and then overrode the create method. I fetch the user and then create JWT tokens for it, add it to the response dict and return it.
from rest_framework import status
from djoser.views import UserViewSet
from djoser import signals
from djoser.compat import get_user_email
from rest_framework_simplejwt.tokens import RefreshToken
class CustomRegistrationView(UserViewSet):
def perform_create(self, serializer):
user = serializer.save()
signals.user_registered.send(
sender=self.__class__, user=user, request=self.request
)
context = {"user": user}
to = [get_user_email(user)]
if settings.SEND_ACTIVATION_EMAIL:
settings.EMAIL.activation(self.request, context).send(to)
elif settings.SEND_CONFIRMATION_EMAIL:
settings.EMAIL.confirmation(self.request, context).send(to)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
response_data = serializer.data
user = User.objects.get(username = response_data['username'])
refresh = RefreshToken.for_user(user)
response_data['refresh'] = str(refresh)
response_data['access'] = str(refresh.access_token)
return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)

Related

DRF Testing, Login view does work when loging in from Postman, but it throws error in APITestCase

I am new to DRF and for learning I am writing test for my DRF Rest API. I am trying to test Login view, however, the test is failing even though I am providing correct credentials. Unusual thing is that it works perfectly fine when I am making a login request from Postman
I tried analyzing the data I provide, however I don't find any issues. In the test case I create new User and then try to log in.
My Test case:
def test_login_user(self):
"""
Ensure user can log in
"""
username = 'TestUserLogin'
password = 'test978453442'
url = reverse('login-list')
user = User.objects.create(
email='testUserLogin#test.com',
first_name='Matheus',
last_name='Smith',
password=password,
title='professor',
username=username,
groups=Group.objects.get(name=GROUPS[0])
)
response = self.client.post(url, {'username': username, 'password': password}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
My Token Generator class
class CustomExpiringObtainAuthToken(ObtainAuthToken):
def post(self, request, *args, **kwargs):
"""
Override!
Create token everytime this endpoint is called
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
if hasattr(user, 'auth_token'):
user.auth_token.delete()
token = Token.objects.create(user=user)
return Response({'token': token.key})
My Login View:
class LoginView(ViewSet):
serializer_class = AuthTokenSerializer
def create(self, request):
return CustomExpiringObtainAuthToken().as_view()(request=request._request)

Django RequestFactory doesn't store a User

I'm testing whether an API User can POST to a view if the User can pass the IsAuthenticated permission. Upon running the test I get the assertion error: AssertionError: 401 != 201.
When permission_classes = (permissions.IsAuthenticated,) is commented out and the test is ran again, I'm finding that request.user is turning out to be an AnonymousUser. Yet, I have a User created and attached to the request as shown below.
I'm not sure what is causing this, and looking to understand how I can pass a User instance to the request.
Note - I'm trying to do this in the Django testing API rather than use Django REST Framework.
tests.py
class TestUserPreferencesResource(TestCase):
'''Verify that a User is capable of setting preferences for their profile'''
#classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user('Mock', password="secret")
cls.user_prefs = json.dumps({
"age": ['Baby', 'Adult'],
"gender": ['Male'],
"size": ["Medium", "Large"]
})
cls.factory = RequestFactory()
cls.credentials = b64encode(b"Mock:secret").decode("ascii")
def test_user_preferences_settings(self):
request = self.factory.post(
reverse("pref-settings"),
data=self.user_prefs,
content_type="application/json",
headers = {
"Authorization": f"Basic {self.credentials}"
},
)
request.user = self.user
print(request.user)
response = UserPrefView.as_view()(request)
self.assertEqual(response.status_code, 201)
views.py
class UserPrefView(
CreateModelMixin,
UpdateModelMixin,
GenericAPIView):
queryset = UserPref.objects.all()
permission_classes = (permissions.IsAuthenticated,)
serilaizer_class = serializers.UserPreferenceSerializer
def post(self, request, format=None):
import pdb; pdb.set_trace()
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(user=request.user)
return Response(serializer.data)

How can I use the jwt token for authentication that i get from my login view

I need to create JWT token authentication, but I don't know how, could you explain me how to do it better or put some examples?
my view:
class UserLogin(generics.CreateAPIView):
"""
POST auth/login/
"""
# This permission class will overide the global permission
# class setting
permission_classes = (permissions.AllowAny,)
queryset = User.objects.all()
serializer_class = TokenSerializer
def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
user = auth.authenticate(request, username=username, password=password)
if user is not None:
auth.login(request, user)
return Response({
"token": jwt_encode_handler(jwt_payload_handler(user)),
'username': username,
}, status=200)
return Response(status=status.HTTP_401_UNAUTHORIZED)
You are creating the token in that view. After that, you need two other mechanism in place:
Your client should send this token the the API with each request, in the Authorization header, like:
Authorization: Bearer your_token
On the api side, you need to use an authentication class, that looks for Authorization header, takes the token and decodes it, and finds the user instance associated with the token, if the token is valid.
If you are using a library for drf jwt authentication, it should have an authentication class that you can use. If you are implementing it manually, you need to write an authentication class that subclasses DRF's BaseAuthentication class yourself. It could basically look like this:
class JwtAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth_header = request.META.get('HTTP_AUTHORIZATION')
if auth_header:
key, token = auth_header.split(' ')
if key == 'Bearer':
# Decode the token here. If it is valid, get the user instance associated with it and return it
...
return user, None
# If token exists but it is invalid, raise AuthenticationFailed exception
# If token does not exist, return None so that another authentication class can handle authentication
You need to tell DRF to use this authentication class. Add this to your settings file for that:
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': [
'path.to.JwtAuthentication',
...
]
}

When I test my POST route for Django Rest Framework API it returns a 401 not authenticated even though logged in

When I attempt to test my Create/POST route for my Django Rest Framework API I receive a response status code of 401 with the error detail telling me ErrorDetail(string=u'Authentication credentials were not provided.', code=u'not_authenticated'). The weird thing is I Django tells me I'm authenticated when I check is is_authenticated.
Does anyone have an idea what might be causing this? All relevant code provided below.
# test_api.py
def authorise_user_and_test_is_authenticated(self, user_id):
"""
Log in user and test this is successful
"""
user = User.objects.get(pk=user_id)
self.client.login(username=user.username, password=user.password)
authorised_user = auth.get_user(self.client)
return self.assertTrue(user.is_authenticated())
def test_create_project(self):
'''
When given valid parameters a project is created.
'''
user = User.objects.get(username="user_001")
self.authorise_user_and_test_is_authenticated(user.id) # pass of authenication and auth testing to method, when tested with is_authenicated() it returns true.
response = self.client.post('/api/user/{0}/project/create/'.format(user.id),
json.dumps({"model_name": "POSTed Project",
"description": "Project tested by posting",
"shared_users[]": [2]
}),
content_type='application/json')
self.assertEqual(response.status_code, 201)
# views.py
class MyCreateView(generics.GenericAPIView):
pass
serializer_class = FerronPageCreateAndUpdateSerializer
def get_queryset(self):
return User.objects.filter(pk=self.kwargs.get('user'))
def post(self, request, format=None, **kwargs):
# This dictionary is used to ensure that the last_modified_by field is always updated on post to be the current user
print request.data
request_data = {
'user': request.user.id,
'model_name': request.data['model_name'],
'description': request.data['description'],
'last_modified_by': request.user.id,
'shared_users': request.data.getlist('shared_users[]', [])
}
serializer = FerronPageCreateAndUpdateSerializer(data=request_data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# settings.py
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'
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication'
),
}
# url matcher
url(r'^user/(?P<user>\d+)/project/create/$', MyCreateView.as_view(), name='create-project')
class FerronPageCreateAndUpdateSerializer(serializers.ModelSerializer):
shared_users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), read_only=False)
description = serializers.CharField(max_length=300, trim_whitespace=True, required=False, allow_blank=True)
class Meta:
model = Project
fields = [
'pk',
'user',
'data',
'model_name',
'description',
'created_at',
'date_modified',
'shared_users',
'last_modified_by'
]
Turn's out the issue was here self.client.login(username=user.username, password=user.password) inside the authorise_user_and_test_is_authenticated(self, user_id) method.
The problem was that I was using the password an instance of a user I had already created. This meant when I gave the argument password=user.password, I was trying to log in using a password that had already been hashed. What I needed to do instead was log in with the original unhashed version of the password e.g. password='openseasame'.

Are sessions needed for python-social-auth

I'm building a django app with an API backend(built with DRF) and angularjs client. My goal is to completely decouple the server and client using JWT in place of sessions. I'm attempting to integrate python-social-auth(PSA) with django-rest-framework-jwt(DRFJWT), so my goal is to have an auth flow something to this:
User logs with Email/facebook via angular client -> client posts form to PSA's url -> PSA login/create user ->[!] DRFJWT creates token that it then sends back to client -> client stores token in local storage then uses token each request
[!]: This is currently where I'm struggling. My thinking is that I can modify the do_complete method in PSA like so
from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler
def do_complete(backend, login, user=None, redirect_name='next',
*args, **kwargs):
# pop redirect value before the session is trashed on login()
data = backend.strategy.request_data()
redirect_value = backend.strategy.session_get(redirect_name, '') or \
data.get(redirect_name, '')
is_authenticated = user_is_authenticated(user)
user = is_authenticated and user or None
partial = partial_pipeline_data(backend, user, *args, **kwargs)
if partial:
xargs, xkwargs = partial
user = backend.continue_pipeline(*xargs, **xkwargs)
else:
user = backend.complete(user=user, *args, **kwargs)
if user_is_active(user):
# catch is_new/social_user in case login() resets the instance
is_new = getattr(user, 'is_new', False)
social_user = user.social_user
login(backend, user, social_user)
payload = jwt_payload_handler(user)
return { 'token': jwt_encode_handler(payload) }
Is this the only way of doing what I'm trying to accomplish?
I'm also wondering if its okay from a best-practices standpoint to use sessions to manage the pipeline and JWT for auth?
I'm also using python-social-auth and django-rest-framework-jwt for user authentication.
The way I was able to integrate the two authentication systems together was by creating a custom view that takes in the 'access_token' provided by the oAuth provider and attempts to create a new user with it. Once the user is created, instead of returning the authenticated user/session I return the JWT token.
The following code snippets explain the solution.
Back-End
In my views.py file I included the following:
#psa()
def auth_by_token(request, backend):
"""Decorator that creates/authenticates a user with an access_token"""
token = request.DATA.get('access_token')
user = request.user
user = request.backend.do_auth(
access_token=request.DATA.get('access_token')
)
if user:
return user
else:
return None
class FacebookView(views.APIView):
"""View to authenticate users through Facebook."""
permission_classes = (permissions.AllowAny,)
def post(self, request, format=None):
auth_token = request.DATA.get('access_token', None)
backend = request.DATA.get('backend', None)
if auth_token and backend:
try:
# Try to authenticate the user using python-social-auth
user = auth_by_token(request, backend)
except Exception,e:
return Response({
'status': 'Bad request',
'message': 'Could not authenticate with the provided token.'
}, status=status.HTTP_400_BAD_REQUEST)
if user:
if not user.is_active:
return Response({
'status': 'Unauthorized',
'message': 'The user account is disabled.'
}, status=status.HTTP_401_UNAUTHORIZED)
# This is the part that differs from the normal python-social-auth implementation.
# Return the JWT instead.
# Get the JWT payload for the user.
payload = jwt_payload_handler(user)
# Include original issued at time for a brand new token,
# to allow token refresh
if api_settings.JWT_ALLOW_REFRESH:
payload['orig_iat'] = timegm(
datetime.utcnow().utctimetuple()
)
# Create the response object with the JWT payload.
response_data = {
'token': jwt_encode_handler(payload)
}
return Response(response_data)
else:
return Response({
'status': 'Bad request',
'message': 'Authentication could not be performed with received data.'
}, status=status.HTTP_400_BAD_REQUEST)
In my urls.py I included the following route:
urlpatterns = patterns('',
...
url(r'^api/v1/auth/facebook/', FacebookView.as_view()),
...
)
Front-End
Now that the backend authentication is wired up, you can use any frontend library to send the access_token and authenticate the user. In my case I used AngularJS.
In a controller file I call the API like so:
/**
* This function gets called after successfully getting the access_token from Facebook's API.
*/
function successLoginFbFn(response) {
var deferred = $q.defer();
$http.post('/api/v1/auth/facebook/', {
"access_token": response.authResponse.accessToken,
"backend": "facebook"
}).success(function(response, status, headers, config) {
// Success
if (response.token) {
// Save the token to localStorage and redirect the user to the front-page.
Authentication.setToken(response.token);
window.location = '/';
}
deferred.resolve(response, status, headers, config);
}).error(function(response, status, headers, config) {
// Error
console.error('Authentication error.');
deferred.reject(response, status, headers, config);
});
}
With this approach you can mix the two plugins. All sent tokens will be coming from django-rest-framework-jwt even though users can still authenticate themselves with the ones provided by sites such as Facebook, Google, Twitter, etc.
I only showed the approach to authenticate through Facebook, however you can follow a similar approach for other providers.
No, you do not need to use sessions(standard Django login system) with python-social-auth. What you need to make JWT and PSA work together is DRF.
Here's my solution:
I used standard PSA's url for making request too social /login/(?P<backend>[^/]+)/$, changed url in urls.py to match redirect from Facebook/Twitter to my own.
url(r'^complete/(?P<backend>[^/]+)/$', views.SocialAuthViewComplete.as_view()),
The point of using API is to have access to user data in request that PSA is doing. DRF allow you to do it if you have JWT authentication in DEFAULT_AUTHENTICATION_CLASSES
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),}
In views.py
from social.apps.django_app.views import complete
class SocialAuthViewComplete(APIView):
permission_classes = ()
def post(self, request, backend, *args, **kwargs):
try:
#Wrap up PSA's `complete` method.
authentication = complete(request, backend, *args, **kwargs)
except Exception, e:
exc = {
'error': str(e)
}
return Response(exc, status=status.HTTP_400_BAD_REQUEST)
return Response({'data': authentication}, status=status.HTTP_202_ACCEPTED)
Then I modified the do_complete method in PSA:
def do_complete(backend, login, user=None, redirect_name='next',
*args, **kwargs):
# pop redirect value before the session is trashed on login()
data = backend.strategy.request_data()
redirect_value = backend.strategy.session_get(redirect_name, '') or \
data.get(redirect_name, '')
is_authenticated = user_is_authenticated(user)
user = is_authenticated and user or None
partial = partial_pipeline_data(backend, user, *args, **kwargs)
if partial:
xargs, xkwargs = partial
user = backend.continue_pipeline(*xargs, **xkwargs)
else:
user = backend.complete(user=user, *args, **kwargs)
user_model = backend.strategy.storage.user.user_model()
if user and not isinstance(user, user_model):
return user
if is_authenticated:
if not user:
information = 'setting_url(backend, redirect_value, LOGIN_REDIRECT_URL'
else:
information = 'setting_url(backend, redirect_value, NEW_ASSOCIATION_REDIRECT_URL,LOGIN_REDIRECT_URL'
elif user:
# Get the JWT payload for the user.
payload = jwt_payload_handler(user)
if user_is_active(user):
is_new = getattr(user, 'is_new', False)
if is_new:
information = 'setting_url(backend, NEW_USER_REDIRECT_URL, redirect_value, LOGIN_REDIRECT_URL'
else:
information = 'setting_url(backend, redirect_value, LOGIN_REDIRECT_URL'
else:
return Response({
'status': 'Unauthorized',
'message': 'The user account is disabled.'
}, status=status.HTTP_401_UNAUTHORIZED)
else:
information = 'setting_url(backend, LOGIN_ERROR_URL, LOGIN_URL'
return { 'an information i may use in future': information,
'token': jwt_encode_handler(payload) # Create the response object with the JWT payload.
}
I tried pipelines and user association and it works correctly.
Also you always can modify another method from PSA, if you need it to works with JWT.

Categories

Resources