I'm using Django Rest Framework 3 and would like to test the CSRF verification.
First, I initialize the DRF APIClient:
client = APIClient(enforce_csrf_checks=True)
Then I set a password on a user so I can login and get a session:
superuser.set_password('1234')
superuser.save()
client.login(email=superuser.email, password='1234')
Now we need a CSRF token. For that I simply create a request and retrieve the token from the cookies.
response = client.request()
csrftoken = client.cookies['csrftoken'].value
When inspecting the code, this seems to work, I get back a valid looking CSRF token. I then do the POST request, passing in the csrfmiddlewartoken parameter:
data = {'name': 'My fancy test report', 'csrfmiddlewaretoken': csrftoken}
response = client.post(API_BASE + '/reports', data=data, format='json')
assert response.status_code == status.HTTP_201_CREATED, response.content
The problem is, this fails:
tests/api/test_api.py:156: in test_csrf_success
assert response.status_code == status.HTTP_201_CREATED, response.content
E AssertionError: {"detail":"CSRF Failed: CSRF token missing or incorrect."}
E assert 403 == 201
E + where 403 = <rest_framework.response.Response object at 0x7f7bd6453bd0>.status_code
E + and 201 = status.HTTP_201_CREATED
What's the correct way to test CSRF verification with DRF?
EDIT
So, after researching this a bit, I discovered the following:
Django will not necessarily set a CSRF token in the header, unless it is rendering a template that explicitly has the csrf_token template tag included. This means that you need to request a page that renders a form with a csrf token, or you need to create a token-requesting view that is decorated with ensure_csrf_cookie.
Because the csrf token is unique per session, it is possible to create a generic token-setting view that looks something like the following:
from django.views.decorators.csrf import ensure_csrf_cookie
#ensure_csrf_cookie
def token_security(request):
return HttpResponse() # json or whatever
Then, any time you wish to POST to a CSRF protected endpoint and there is no CSRF token in the cookies, issue a GET against this view and it should set the cookie, which can then be used to POST.
Original answer below:
The following works in my tests (I am using factories to create User objects, but you could create them manually):
class TestLoginApi(APITestCase):
def setUp(self):
self.client = APIClient(enforce_csrf_checks=True)
self.path = reverse("registration:login")
self.user = UserFactory()
def tearDown(self):
self.client.logout()
def _get_token(self, url, data):
resp = self.client.get(url)
data['csrfmiddlewaretoken'] = resp.cookies['csrftoken'].value
return data
def test_login(self):
data = {'username': self.user.username,
'password': PASSWORD}
data = self._get_token(self.path, data)
# This should log us in.
# The client should re-use its cookies, but if we're using the
# `requests` library or something, we'd have to re-use cookies manually.
resp = self.client.post(self.path, data=data)
self.assertEqual(resp.status_code, 200)
etc.
If this is all done dynamically, you must also be sure that your view sets a cookie on the GET, because according to the Django docs (see the Warning), it will not be set automatically if you are not POSTing back from a template that had the {% csrf_token %} set.
If you need to set it, that looks something like this (in your DRF views.py):
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
#method_decorator(ensure_csrf_cookie)
def get(self, request, *args, **kwargs):
return SomeJson...
Finally, for my Django Rest Framework views, I had to make sure that the POSTs were csrf protected as well (but this does not look like a problem you are having):
from django.views.decorators.csrf import csrf_protect
#method_decorator(csrf_protect)
def post(self, request, *args, **kwargs):
return SomeJson...
Related
I'm writing a custom Authentication middleware that check the incoming requests for the "Authorization" key in the header, which contains a token.
I'm using this token to check with a third-party (Microsoft Graph) for the validity of the user. MS Graph will respond with an object like below
# the response object
{
'#odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity',
'businessPhones': ['xxx'],
'displayName': 'xxx',
'givenName': 'xxx',
'id': 'xxx',
'jobTitle': None,
'mail': 'xxx',
'mobilePhone': None,
'officeLocation': None,
'preferredLanguage': 'xxx',
'surname': 'xxx',
'userPrincipalName': 'xxx'
}
EDIT: Adding custom middleware code here:
class AuthenticationMiddleware(MiddlewareMixin):
if not request.user.is_authenticated:
if "Authorization" in request.headers:
# Make a request to MS Graph with the given token
# to get user details and append to request
token = request.headers["Authorization"]
elif "accessToken" in request.GET:
token = request.GET["accessToken"]
else:
token = None
if token:
url = "https://graph.microsoft.com/v1.0/me/"
payload = {}
headers = {"Authorization": "Bearer {}".format(token)}
response = requests.request("GET", url, headers=headers, data=payload)
if response.ok:
request.custom_user = response.json()
else:
request.custom_user = AnonymousUser
else:
request.custom_user = AnonymousUser
Now I want to design this to work just like Django's default authentication backend with proper group and permission. How can I work on a LazyObject to be able to check for user's group membership and permission?
UPDATE
It looks like there's also a custom backend authentication that works like this.
Is it doing the same thing as I'm doing with the middleware?
from django.contrib.auth.backends import BaseBackend
class MyBackend(BaseBackend):
def authenticate(self, request, token=None):
# Check the token and return a user.
...
you should custom an Middleware like the below, and add it to middlewares in settings
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# todo: do something you want in response
return response
see also:https://docs.djangoproject.com/en/3.1/topics/http/middleware/
EDIT:
Is it doing the same thing as I'm doing with the middleware?
no, it's not.
the most different is that
The backend is used for connecting with database, and the middleware is aim to process the request. You can find more examples code in django.middleware package.
and if you want to custome how to save the infomation to database eg: customer the authenticate method, you should customer a backend for the work. otherwise you may custome an middleware to process all of the requests. Because it is easy to customize middleware.
After a user logs in, they're sent the session cookie inside an HttpResponse object. I want to add an additional field 'foo' to the session, just like I had done
request.session['foo'] = 'bar'
The above doesn't work because the login request itself doesn't have the session object itself, only subsequent requests have the cookie.
Also, doing
response.set_cookie("foo", "bar")
doesn't seem to associate the cookie with session (request.session['foo'] throws an error on subsequent requests).
How can I do this?
If you consider using a custom login view, you can do something like this:
def custom_login(request):
if request.method == 'GET':
if not request.user.is_authenticated
# here, user is not logged in.
request.session['my_data'] = 'my value'
return render(request, 'login.html' , {})
elif request.method == 'POST':
# get login credentials and authenticate user
# see -> https://docs.djangoproject.com/en/2.0/topics/auth/default/#authenticating-users
return HttpResponseRedirect('/homepage')
Or, if you want to use built-in login views, you can manipulate session data at middleware level. Just write a custom middleware like this:
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
if request.path == '/login' and request.method == 'GET' and not request.user.is_authenticated:
request.session['data'] = 123
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
And don't forget to add this SimpleMiddleware to the MIDDLEWARE list in settings.py. You can find more about middlewares here.
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']
I this article I have read that token based authentication is stateless, meaning that the servers don't keep record of logged in users.
On the other hand in the django-rest-auth API docs there is a logout endpoint mentioned. What is it for?
During log out, the authentication token issued to the user is deleted. You can check out the logout source where it calls request.user.auth_token.delete(). Therefore, the next time the user logs in, a new token will be issued.
In the event you are using the JWT option with django-rest-auth, the logout behavior doesn't actually appear to delete the JWT tokens. So it seems to be essentially doing nothing. Now I'm still new to JWT, but based on what I've learned - it appears that this isn't even necessary. So just delete on the client and you'll be okay. Still it would be nice to have a JUST-DESTROY-THE-JWT-TOKEN-BECAUSE-WHO-NEEDS-POTENTIALLY-UNEXPIRED-DANGEROUS-STUFF-HANGING-AROUND option. But sadly, I don't think rest_framework_jwt supports this.
rest_framework_simplejwt (https://github.com/davesque/django-rest-framework-simplejwt) does seem to support this "blacklisting" which is promising! (rest_framework_simplejwt.token_blacklist) And yet, alas rest_auth does not support simplejwt, straight up - ah well. If someone figures out how to do this, I'd be game.
But back to what's going on with rest-auth's jwt implementation...
So when you are logging out, it does attempt to delete the django token, but I don't believe that's what you are doing here since you should have the following in your settings.py if you are using the JSONWebTokenAuthentication class:
settings.py (for JWT)
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication', # USED BY JWT.
'rest_framework.authentication.TokenAuthentication', # IGNORED BY JWT
)
Now when django-rest-auth goes to execute /logout, let's see what they are doing from their source code on https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/views.py
views.py (from rest-auth source)
from django.contrib.auth import (
login as django_login,
logout as django_logout
)
from .models import TokenModel
#-------snip-------
class LogoutView(APIView):
"""
Calls Django logout method and delete the Token object
assigned to the current User object.
Accepts/Returns nothing.
"""
permission_classes = (AllowAny,)
def get(self, request, *args, **kwargs):
if getattr(settings, 'ACCOUNT_LOGOUT_ON_GET', False):
response = self.logout(request)
else:
response = self.http_method_not_allowed(request, *args, **kwargs)
return self.finalize_response(request, response, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.logout(request)
def logout(self, request):
try:
request.user.auth_token.delete()
except (AttributeError, ObjectDoesNotExist):
pass
if getattr(settings, 'REST_SESSION_LOGIN', True):
django_logout(request)
response = Response({"detail": _("Successfully logged out.")},
status=status.HTTP_200_OK)
if getattr(settings, 'REST_USE_JWT', False):
from rest_framework_jwt.settings import api_settings as jwt_settings
if jwt_settings.JWT_AUTH_COOKIE:
response.delete_cookie(jwt_settings.JWT_AUTH_COOKIE)
return response
models.py (from rest-auth source)
from django.conf import settings
from rest_framework.authtoken.models import Token as DefaultTokenModel
from .utils import import_callable
# Register your models here.
TokenModel = import_callable(
getattr(settings, 'REST_AUTH_TOKEN_MODEL', DefaultTokenModel))
So unless you've set the REST_AUTH_TOKEN_MODEL attribute in your settings to a custom token model (see https://github.com/Tivix/django-rest-auth/blob/master/docs/configuration.rst - which is doubtful, let's all be honest here. Who is doing this...), then it's just trying to delete the DefaultTokenModel - which is the django_token/session authentication model we aren't using.
I do hope things migrate to simplejwt as it's much more actively supported.
And I also hope this brain-dump/jwt digging helps someone.
P.S. A little more on JWT, optimal uses and blacklisting. https://dev.to/_arpy/how-to-log-out-when-using-jwt-4ajm
I am wondering if the request is actually being made via http. In my app I have a test that looks like
class Authenticate(APITestCase):
def setUp(self):
self.client = APIClient()
self.password_for_admin = '123456'
self.admin = User.objects.create_superuser(username='myname', email='email#email.com', password='123456')
self.token = Token.objects.create(user=self.admin)
def test_authenticate(self):
""" comment """
self.client.credentials(HTTP_AUTHORIZATION='Basic ' + base64.b64encode('{}:{}'.format(self.admin.username, self.password_for_admin)))
response = self.client.post('/api/authenticate/')
print response
And in my view I've got:
#api_view(('POST',))
def authenticate(request, format=None):
""" comment """
import pprint
log.debug(pprint.pprint(request))
try:
"asdlfjl"
except Exception, e:
response = "An error occurred, {}".format(e)
return Response(response)
My settings looks like:
INSTALLED_APPS = (
...
'django.contrib.sessions',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
...
)
The request is being printed out as None in my log file. I need to get the session. I tried request.session (which was None) and that's what led me to this question.
I figured it out. The server does send a request using the testserver domain. This was sort of a misleading question and the code was wrong. The user is already authenticated using the rest basic backend by the time they reach this view method.
Through much research I found out that the user was being authenticated by rest but the login method doesn't get called by the rest backend. Since login doesn't get called from a rest backend the session is never attached to the request. I changed the authenticate method to login and I simply called login by doing this:
...
#api_view(('POST',))
def login(request, format=None):
try:
from django.contrib.auth import login
if request.user and request.user.is_active:
login(request, request.user)
...
response = ...
else:
response = {}
except Exception, e:
response = "An error occurred, {}".format(e)
return Response(response)