Django - How to add access token to Client.post in Django test? - python

So I have some code below. Every endpoint has an authentication process, which is below as well. I want to be able to attach an access token, which is in cls.user to the Client.post so that I can test all the endpoints and ensure they are authenticating properly as well. How can I do this? So ideally I'd be attaching <bearer> <access token> to request.Meta['HTTP_AUTHORIZATION']
test.py
import json
from cheers.models import *
from warrant import Cognito
from django.urls import reverse
from django.test import TestCase
from rest_framework import status
from cheers.models import GoalCategory, Post
from dummy_factory.Factories import UserFactory, GoalFactory
class PostTest(TestCase):
#classmethod
# Generates Test DB data to persist throughout all tests
def setUpTestData(cls) -> None:
cls.goal_category = 'health'
GoalCategory.objects.create(category=cls.goal_category, emoji_url='url')
cls.user = UserFactory()
cls.goal = GoalFactory()
user_obj = User.objects.get(pk=cls.user.phone_number)
goal_obj = Goal.objects.get(pk=cls.goal.uuid)
Post.objects.create(creator_id=user_obj, goal_id=goal_obj, body='Some text')
cls.user = Cognito(<Some login credentials>)
cls.user.authenticate(password=<password>)
def test_create(self):
response = self.client.post(reverse('post'),
data=json.dumps({'creator_id': str(self.user.uuid),
'goal_id': str(self.goal.uuid),
'body': 'Some text #Test'}),
content_type='application/json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Test authenticator function
def cognito_authenticator(view_func):
def wrapped_view(request, *args, **kwargs):
# Check the cognito token from the request.
token = request.META['HTTP_AUTHORIZATION'].split(' ')[1]
try:
jwt.decode_cognito_jwt(token)
except Exception:
# Fail if invalid
return Response("Invalid JWT", status=status.HTTP_401_UNAUTHORIZED) # Or HttpResponseForbidden()
else:
# Proceed with the view if valid
return view_func(request, *args, **kwargs)
return wrapped_view

You can set the header like this:
token = 'sometoken'
response = self.client.post(
reverse('post'),
data=json.dumps({
'creator_id': str(self.user.uuid),
'goal_id': str(self.goal.uuid),
'body': 'Some text #Test'
}),
content_type='application/json',
**{'HTTP_AUTHORIZATION': f'Bearer {token}'}
)
And then access the header using:
request.META['HTTP_AUTHORIZATION']

Related

Django to return a view with TokenAuthentication for WebView

I am trying to create a flutter app which will use webview to display authenticated data from my Django App.
Steps Involved:
Flutter app sends authentication request
Django validates the user credentials (user id & Password) and returns authtoken
Flutter then sends a request via a webview to a url (which requires login).
I would like to login the user in webapp using this token and return the webview.
If the url does not require authentcation, it works like a charm.
When the url requires authentication, I am redirected to the login page and I want users to bypass that using token authentication which is already aquired in Step 1
here is my Django view.
class QuizTake(FormView):
permission_classes = (IsAuthenticated,)
form_class = QuestionForm
template_name = 'question.html'
result_template_name = 'result.html'
single_complete_template_name = 'single_complete.html'
login_template_name='login.html'
def dispatch(self, request, *args, **kwargs):
self.quiz = get_object_or_404(Quiz, url=self.kwargs['quiz_name'])
print(self.kwargs['quiz_name'])
"""
Authenticate if the request has token authentication
"""
if self.quiz.draft and not request.user.has_perm('quiz.change_quiz'):
raise PermissionDenied
try:
self.logged_in_user = self.request.user.is_authenticated()
except TypeError:
self.logged_in_user = self.request.user.is_authenticated
if self.logged_in_user:
self.sitting = Sitting.objects.user_sitting(request.user,
self.quiz)
else:
self.sitting = self.anon_load_sitting()
if self.sitting is False:
print("sitting false")
if self.logged_in_user:
return render(request, self.single_complete_template_name)
else:
redirecturl = "/login/?next=/quiz/"+self.kwargs['quiz_name']+"/take/"
return redirect(redirecturl)
return super(QuizTake, self).dispatch(request, *args, **kwargs)
Flutter Code
class _QuizLauncherState extends State<QuizLauncher> {
final String url, authtoken;
final int userId;
String quizUrl;
_QuizLauncherState(this.url, this.authtoken,this.userId);
void initState() {
quizUrl = 'https://test.mysite.com/quiz/$url/take';
print(quizUrl);
//for reference https://test.mysite.com/quiz/56df5d90-7f67-45ff-8fe1-7c07728ba9ab/take/
super.initState();
}
Completer<WebViewController> _controller = Completer<WebViewController>();
final Set<String> _favorites = Set<String>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// This drop down menu demonstrates that Flutter widgets can be shown over the web view.
actions: <Widget>[
NavigationControls(_controller.future),
Menu(_controller.future, () => _favorites),
],
),
body: WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
Map<String, String> headers = {"Authorization": "Bearer " + authtoken};
webViewController.loadUrl(quizUrl, headers: headers);
},
),
);
}
}
Is this possible at all? If there are any alternate ways, please tell me. Basically, I am trying to access a url via webview which requires authentication, using authtoken. Please help.
You can use custom authentication classes like this, say if you are using Authorization header:
from rest_framework.authentication import BaseAuthentication
class MyCustomAuth(BaseAuthentication):
def authenticate(self, request):
auth_method, token = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
# Get your user via the token here
if you_got_your_user:
return user, None
return None # or raise AuthFailedException
class QuizTake(FormView):
authentication_classes = (MyCustomAuth, )
This still depends on how your token identifies the user though. For example if you are using JWT, there are existing authentication classes already that handles this for you.
EDIT:
Looked at knox documentation from here. If you used knox, then you should probably use their own TokenAuthentication class. Can you try with below code:
from knox.auth import TokenAuthentication
class QuizTake(FormView):
authentication_classes = (TokenAuthentication, )
You can use authentication from rest framework lib like as below code.
import base64
import binascii
from django.contrib.auth import authenticate, get_user_model
from django.middleware.csrf import CsrfViewMiddleware
from django.utils.translation import gettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING, exceptions
def get_authorization_header(request):
auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, str):
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
class BaseAuthentication:
raise NotImplementedError(".authenticate() must be overridden.")
def authenticate_header(self, request):
pass
class SessionAuthentication(BaseAuthentication):
user = getattr(request._request, 'user', None)
if not user or not user.is_active:
return None
self.enforce_csrf(request)
return (user, None)
def enforce_csrf(self, request):
def dummy_get_response(request):
return None
check = CSRFCheck(dummy_get_response)
check.process_request(request)
reason = check.process_view(request, None, (), {})
if reason:
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication):
keyword = 'Token'
model = None
def get_model(self):
if self.model is not None:
return self.model
from rest_framework.authtoken.models import Token
return Token
Or go through the below link for better understanding
[Toke Authorization]

Django messages middleware issue while testing post request

I'm trying to test an UpdateView that adds a message to the redirected success page. It seems my issue comes from messages because of pytest returns:
django.contrib.messages.api.MessageFailure: You cannot add messages without installing django.contrib.messages.middleware.MessageMiddleware
My test code is:
def test_authenticated_staff(self, rf):
langues = LanguageCatalog.objects.create(
lang_src='wz',
lang_dest='en',
percent='4'
)
req = rf.get(reverse("dashboard.staff:lang-update", kwargs={'pk': langues.pk}))
data = {'lang_src': 'it',
'lang_dest': 'en',
'percent': '34'}
req = rf.post(reverse(
"dashboard.staff:lang-update", kwargs={'pk': langues.pk}), data=data)
req.user = UserFactory()
resp = views.LangUpdateView.as_view()(req, pk=langues.pk)
I precise that the MessageMiddleware is present in MIDDLEWARE settings. I use Django==2.0.13.
I found the solution. In order to test a such request, you need first to annotate it with a session and then a message. Actually it means to add these lines:
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
# in your test method:
"""Annotate a request object with a session"""
middleware = SessionMiddleware()
middleware.process_request(req)
req.session.save()
"""Annotate a request object with a messages"""
middleware = MessageMiddleware()
middleware.process_request(req)
req.session.save()
# and then (in my case)
resp = views.LangUpdateView.as_view()(req, pk=langues.pk)
You can also move manual request annotation int a separate context manager that can be reused within multiple tests, the code would look like this then:
import contextlib
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
#contextlib.contextmanager
def middleware(request):
"""Annotate a request object with a session"""
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
"""Annotate a request object with a messages"""
middleware = MessageMiddleware()
middleware.process_request(request)
request.session.save()
yield request
def test_authenticated_staff(self, rf):
langues = LanguageCatalog.objects.create(
lang_src='wz',
lang_dest='en',
percent='4'
)
req = rf.get(reverse("dashboard.staff:lang-update", kwargs={'pk': langues.pk}))
data = {'lang_src': 'it',
'lang_dest': 'en',
'percent': '34'}
req = rf.post(reverse("dashboard.staff:lang-update", kwargs={'pk': langues.pk}), data=data)
req.user = UserFactory()
with middleware(req): # << !
resp = views.LangUpdateView.as_view()(req, pk=langues.pk)

Django: Basic Auth for one view (avoid middleware)

I need to provide http-basic-auth to one view.
I want to avoid modifying the middleware settings.
Background: This is a view which gets filled in by a remote application.
When you do a basic auth request, you're really adding credentials into the Authorization header. Before transit, these credentials are base64-encoded, so you need to decode them on receipt.
The following code snippet presumes that there's only one valid username and password:
import base64
def my_view(request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
token_type, _, credentials = auth_header.partition(' ')
expected = base64.b64encode(b'username:password').decode()
if token_type != 'Basic' or credentials != expected:
return HttpResponse(status=401)
# Your authenticated code here:
...
If you wish to compare to the username and password of a User model, try the following instead:
def my_view(request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
token_type, _, credentials = auth_header.partition(' ')
username, password = base64.b64decode(credentials).split(':')
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return HttpResponse(status=401)
password_valid = user.check_password(password)
if token_type != 'Basic' or not password_valid:
return HttpResponse(status=401)
# Your authenticated code here:
...
Please note that this latter version is not extremely secure. At first glance, I can see that it is vulnerable to timing attacks, for example.
You can try a custom decorator (as seems to be the recommended way here and here) instead of adding new middleware:
my_app/decorators.py:
import base64
from django.http import HttpResponse
from django.contrib.auth import authenticate
from django.conf import settings
def basicauth(view):
def wrap(request, *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]).decode(
"utf8"
).split(':', 1)
user = authenticate(username=uname, password=passwd)
if user is not None and user.is_active:
request.user = user
return view(request, *args, **kwargs)
response = HttpResponse()
response.status_code = 401
response['WWW-Authenticate'] = 'Basic realm="{}"'.format(
settings.BASIC_AUTH_REALM
)
return response
return wrap
Then use this to decorate your view:
from my_app.decorators import basicauth
#basicauth
def my_view(request):
...
This library could be used: https://github.com/hirokiky/django-basicauth
Basic auth utilities for Django.
The docs show how to use it:
Applying decorator to CBVs
To apply #basic_auth_requried decorator to Class Based Views, use
django.utils.decorators.method_decorator.
Source: https://github.com/hirokiky/django-basicauth#applying-decorator-to-cbvs
For those that already use django-rest-framework (DRF):
DRF has a BasicAuthentication class which, more-or-less, does what is described in the other answers (see source).
This class can also be used in "normal" Django views.
For example:
from rest_framework.authentication import BasicAuthentication
def my_view(request):
# use django-rest-framework's basic authentication to get user
user = None
user_auth_tuple = BasicAuthentication().authenticate(request)
if user_auth_tuple is not None:
user, _ = user_auth_tuple

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.

What's the proper way to test token-based auth using APIRequestFactory?

The query to my endpoint works fine (as long as I pass it a valid token), it returns the json representation of my response data.
The code in the service api that calls my endpoint, passing an auth token in the header:
headers = {'content-type': 'application/json',
'Authorization': 'Token {}'.format(myToken)}
url = 'http://localhost:8000/my_endpoint/'
r = session.get(url=url, params=params, headers=headers)
In views.py, I have a method decorator that wraps the dispatch method on the view (viewsets.ReadOnlyModelViewSet):
def login_required(f):
def check_login_and_call(request, *args, **kwargs):
authentication = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(authentication, str):
authentication = authentication.encode(HTTP_HEADER_ENCODING)
key = authentication.split()
if not key or len(key) != 2:
raise PermissionDenied('Authentication failed.')
user, token = authenticate_credentials(key[1])
return f(request, *args, **kwargs)
return check_login_and_call
I'm trying to write a test to authenticate the request using a token:
from rest_framework.authtoken.models import Token
from rest_framework.test import APIRequestFactory
from rest_framework.test import APITestCase
from rest_framework.test import force_authenticate
class EndpointViewTest(APITestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = User.objects.create_user(
username='user#foo.com', email='user#foo.com', password='top_secret')
self.token = Token.objects.create(user=self.user)
self.token.save()
def test_token_auth(self):
request = self.factory.get('/my_endpoint')
force_authenticate(request, token=self.token.key)
view = views.EndpointViewSet.as_view({'get': 'list'})
response = view(request)
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.render().content)['results']
For some reason, I cannot get the request to properly pass the token for this test. Using force_authenticate doesn't seem to change the header that I'm using for validating the token. The current output is raising "PermissionDenied: Authentication failed." because the token isn't being set on the request.
Is there a proper way to set this in the request header in my test or to refactor the way I'm using it in the first place?
I found a way to get the test to pass, but please post if you have a better idea of how to handle any of this.
request = self.factory.get('/my_endpoint', HTTP_AUTHORIZATION='Token {}'.format(self.token))
force_authenticate(request, user=self.user)
After changing the above two lines of the test, it seems to authenticate based on the token properly.
I wanted to test the authentication function itself, so forcing authentication wans't an option.
One way to properly pass the token is to use APIClient, which you already have imported.
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
response = client.get('/api/vehicles/')
That sets your given token into the request header and lets the back end decide if it's valid or not.
Sorry for digging this old thread up, but if someone is using APIClient() to do their tests you can do the following:
from rest_framework.test import APITestCase
from rest_framework.test import APIClient
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
class VehicleCreationTests(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_superuser('admin', 'admin#admin.com', 'admin123')
self.token = Token.objects.create(user=self.user)
def testcase(self):
self.client.force_login(user=self.user)
response = self.client.post('/api/vehicles/', data=vehicle_data, format='json', HTTP_AUTHORIZATION=self.token)
self.assertEqual(response.status_code, 201)
Really good resource that I've used to come up with this is django-rest-framework-jwt tests
The simpler way to force_authentication using a built-in method from APITestCase is:
class Test(APITestCase):
def setUp(self):
user1 = User.objects.create_user(username='foo')
self.client.force_authenticate(user=user1) # self.client is from APITestCase
... the rest of your tests ...

Categories

Resources