I am using requests to log into my Django site for testing (and yes, I know about the Django TestClient, but I need plain http here). I can log in and, as long as I do get requests, everything is OK.
When I try to use post instead, I get a 403 from the csrf middleware. I've worked around that for now by using a #crsf_exempt on my view, but would prefer a longer term solution.
This is my code:
with requests.Session() as ses:
try:
data = {
'username': self.username,
'password': self.password,
}
ses.get(login_url)
try:
csrftoken = ses.cookies["csrftoken"]
except Exception, e:
raise
data.update(csrfmiddlewaretoken=csrftoken)
_login_response = ses.post(login_url, data=data)
logger.info("ses.cookies:%s" % (ses.cookies))
assert 200 <= _login_response.status_code < 300, "_login_response.status_code:%s" % (_login_response.status_code)
response = ses.post(
full_url,
data=data,
)
return self._process_response(response)
The login works fine, and I can see the csrf token here.
INFO:tests.helper_fetch:ses.cookies:<RequestsCookieJar[<Cookie csrftoken=TmM97gnNHs4YCgQPzfNztrAWY3KcysAg for localhost.local/>, <Cookie sessionid=kj6wfmta
However, the middleware sees cookies as empty.
INFO:django.middleware.csrf:request.COOKIES:{}
I've added the logging code to it:
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
try:
csrf_token = _sanitize_token(
request.COOKIES[settings.CSRF_COOKIE_NAME])
# Use same token next time
request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
# import pdb
# pdb.set_trace()
import logging
logger = logging.getLogger(__name__)
logger.info("request.COOKIES:%s" % (request.COOKIES))
Am I missing something with way I call request's session.post? I tried adding cookie to it, made no difference. But I can totally see why crsf middleware is bugging out. I thought the cookies were part of the session, so why are they missing in my second post?
response = ses.post(
self.res.full_url,
data=data,
cookies=ses.cookies,
)
This variation, inspired by How to send cookies in a post request with the Python Requests library?, also did not result in anything being passed to csrf middleware:
response = ses.post(
self.res.full_url,
data=data,
cookies=dict(csrftoken=csrftoken),
)
For subsequent requests after the login, try supplying it as header X-CSRFToken instead.
The following worked for me:
with requests.Session() as sesssion:
response = session.get(login_url)
response.raise_for_status() # raises HTTPError if: 400 <= status_code < 600
csrf = session.cookies['csrftoken']
data = {
'username': self.username,
'password': self.password,
'csrfmiddlewaretoken': csrf
}
response = session.post(login_url, data=data)
response.raise_for_status()
headers = {'X-CSRFToken': csrf, 'Referer': url}
response = session.post('another_url', data={}, headers=headers)
response.raise_for_status()
return response # At this point we probably made it
Docs reference: https://docs.djangoproject.com/en/dev/ref/csrf/#csrf-ajax
You could also try to use this decorator on your view, instead of the csrf_exempt. I tried to reproduce your issue, and this worked as well for me.
from django.views.decorators.csrf import ensure_csrf_cookie`
#ensure_csrf_cookie
def your_login_view(request):
# your view code
Related
In my Django project I use python3-saml to login with SSO. The login works like expected but the logout is failing with an error message 'No hostname defined'. I really don't know how to solve this as the only parameter passed to logout is the request and request is missing 'http_host' and 'server_name', read here.
My logout part looks like following:
def get(self, request, pkuser=None):
try:
get_user_model().objects.get(pk=pkuser)
except get_user_model().DoesNotExist:
return redirect('HomePage')
logger = logging.getLogger('iam')
logger.info('IAM logout')
auth = OneLogin_Saml2_Auth(request, custom_base_path=settings.SAML_FOLDER)
logger.info('account logout')
# OneLogin_Saml2_Utils.delete_local_session()
try:
auth.logout(
name_id=request.session['samlNameId'],
session_index=request.session['samlSessionIndex'],
nq=request.session['samlNameIdNameQualifier'],
name_id_format=request.session['samlNameIdFormat'],
spnq=request.session['samlNameIdSPNameQualifier']
)
logger.info('account logout success')
OneLogin_Saml2_Utils.delete_local_session()
logger.info('account logout: deleted local session')
except Exception as e:
logger.info('account logout failed: {}'.format(str(e)))
logout(request)
return redirect('HomePage')
Maybe I'm using the wrong package...? Any help or advice will be appreciated.
I think this is happening because the logout method is missing http_host and server_name.
To fix this issue modify the request object to include the http_host and server_name attributes before calling the logout method.
def get(self, request, pkuser=None):
try:
get_user_model().objects.get(pk=pkuser)
except get_user_model().DoesNotExist:
return redirect('HomePage')
logger = logging.getLogger('iam')
logger.info('IAM logout')
auth = OneLogin_Saml2_Auth(request, custom_base_path=settings.SAML_FOLDER)
logger.info('account logout')
request.http_host = 'your_http_host'
request.server_name = 'your_server_name'
try:
auth.logout(
name_id=request.session['samlNameId'],
session_index=request.session['samlSessionIndex'],
nq=request.session['samlNameIdNameQualifier'],
name_id_format=request.session['samlNameIdFormat'],
spnq=request.session['samlNameIdSPNameQualifier']
)
logger.info('account logout success')
OneLogin_Saml2_Utils.delete_local_session()
logger.info('account logout: deleted local session')
except Exception as e:
logger.info('account logout failed: {}'.format(str(e)))
logout(request)
return redirect('HomePage')
In the handler for your ACS URL you will also be creating an object from OneLogin_Saml2_Auth(), which theoretically is working. Check if the setup for the request to that is different to the setup here.
One thing that stands out between this and my code is that my one has a line prior to the constructor request_annotated = saml2_prepare_request(request) (all the names in this line will likely be different for you other than request, but the format would remain the same) where you are passing request_annotated to the OneLogin_Saml2_Auth constructor rather than request. If so, duplicate that line. The library is expecting that there are some specific things annotated to the request dictionary. The code for this function in my Django is:
def saml2_prepare_request(request):
return {
'http_host': request.META['HTTP_HOST'],
'script_name': request.META['PATH_INFO'],
'server_port': request.META['SERVER_PORT'],
'get_data': request.GET.copy(),
'post_data': request.POST.copy()
}
concurrency to request django app with two url, each url request header I give one id, like urla id=1, urlb id=2, but when i get from django view from request header, I get urla's id from request header is 2 ...so confused...post body is ok
client code
#coding=utf-8
import threadpool
import requests
HOST = 'http://127.0.0.1:8000'
urls = ['/health', '/x2']
headers = {'Content-Type': 'application/json', 'HOST': 'xx.com'}
pool = threadpool.ThreadPool(5)
def request_api(n):
url = urls[n%2]
print('---------------------start request url {}'.format(url))
headers.update(url=url)
body = dict(
url=url
)
r = requests.post(url=HOST + url, headers=headers, json=body)
print(r.status_code)
reqs = threadpool.makeRequests(request_api, range(5))
[pool.putRequest(req) for req in reqs]
pool.wait()
print('over')
server code
# coding=utf-8
from rest_framework.views import APIView
from django.http.response import HttpResponse
class ApiHealthCheck(APIView):
def post(self, request, request_id='', **kwargs):
print('api_health_check: {},{},{}'.format(request.path, request.META.get('HTTP_URL'), request.data))
return HttpResponse("ok")
class ApiHealthCheck2(APIView):
def post(self, request, request_id='', **kwargs):
print('api_health_check2: {},{},{}'.format(request.path, request.META.get('HTTP_URL'), request.data))
return HttpResponse("ok")
server print
api_health_check: /health,/health,{u'url': u'/health'}
api_health_check2: /x2,/health,{u'url': u'/x2'}
api_health_check: /health,/health,{u'url': u'/health'}
api_health_check2: /x2,/x2,{u'url': u'/x2'}
api_health_check: /health,/health,{u'url': u'/health'}
attention one of them have wrong,path not match META's content,body is ok
I am sorry, I found the problem, not django problem, it is my fault.
headers = {'Content-Type': 'application/json', 'HOST': 'xx.com'}
headers is a global variable, concurrent request can concurrent modify this header, so request django, django get wrong header
I am trying to write a small restful api application, i am using Chrome Postman extension for sending requests to the app .
I believe that my code does not have mistakes but every time i am sending post request a 400 Bad Request error raising , here is my code:
#api_route.route('/api', methods=['GET'])
def api():
return jsonify({'message':'Api v1.0'})
#api_route.route('/api', methods=['POST'])
def create_user():
data = request.get_json()
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message', 'New User Created!'})
The json data that i am sending looks like this:
{"email" : "Test", "password" : "123123123"}
Why i am getting the 400 error ??
Update:
Screenshots for the requests using Postman:
GET Request
POST Request
Here i am initiating api route inside api controller :
from flask import Blueprint
api_route = Blueprint(
'api',
__name__
)
from . import views
then i am registering it inside def create_app() function :
from .api import api_route
app.register_blueprint(api_route)
Here are the extensions that i am using in my application:
toolbar = DebugToolbarExtension()
assets_env = Environment()
cache = Cache()
moment = Moment()
htmlminify = HTMLMIN()
csrf = CSRFProtect()
jac = JAC()
googlemap = GoogleMaps()
session = Session()
principal = Principal()
I solved the problem, i've initiated CSRFProtect with app so i need to include X-CSRFToken in all my requests, so i have two choices:
1 - To include the csrf_token in request.headers for all the requests
2 - Using #csrf.exempt decorator that coming with flask_wtf.csrf
For now i am using #csrf.exempt, so it become like this:
#api_route.route('/api', methods=['GET','POST'])
#csrf.exempt
def create_user():
if request.method == 'GET':
return jsonify({'message' : 'API v1.0'})
elif request.method == 'POST':
data = request.get_json()
hashed_password = generate_password_hash(data['password'], method='sha256')
new_user_api = Api(email=data['email'], password=hashed_password)
db.session.add(new_user_api)
db.session.commit()
return jsonify({'message' : 'New user created!'})
return return jsonify({'message' : 'No user has been added!'})
Thanks for #MrPyCharm for his interests , salute :) .
A good approach would be to structure your views as follows:
Instead of creating view with same route for different request methods, you can handle the request methods in the same view:
#api_route.route('/api', methods=['GET', 'POST'])
def api():
if request.method == 'GET':
return jsonify({'message':'Api v1.0'})
else:
data = request.get_json(force=True)
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message': 'New User Created!'})
# Just in case the if condition didn't satisfy
return None
A note for anyone else experiencing this with PostMan and Flask - you will also hit a HTTP 404 if your URL in PostMan is HTTPS but your Flask app only handles HTTP.
With the following test, the token is not recognised as valid. In my manual test, it's working so I'm missing something in the way the password is generated I guess.
def test_actual_reset_password(self):
new_password = "myNewPassword012*"
token_generator = PasswordResetTokenGenerator()
user = UserFactory.create()
token = token_generator.make_token(user=user)
response = self.assert_page_loading(path="/forgot-password/reset/{0}/".format(token))
print response
# That loads the page with the error message mentioning that the token was already used
# So I cannot carry on:
form = response.form
form['new_password1'] = new_password
form['new_password2'] = new_password
response = form.submit()
In the django source code, in the PasswordResetForm, I've found this code; I can't see what the difference is:
def save(self, ..., token_generator=default_token_generator, ...):
"""
Generates a one-use only link for resetting password and sends to the
user.
"""
...
for user in self.users_cache:
...
c = {
...
'token': token_generator.make_token(user),
...
}
...
send_mail(subject, email, from_email, [user.email])
Ok, I was just searching for info on how to do this and your question prompted me to figure it out myself. I'm not sure if you're still working on this, but here's how I got it to work:
from django.core import mail
# First we get the initial password reset form.
# This is not strictly necessary, but I included it for completeness
response = self.c.get(reverse('password_reset'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template_name, 'authentication/password_reset_form.html')
# Then we post the response with our "email address"
response = self.c.post(reverse('password_reset'),{'email':'fred#home.com'})
self.assertEqual(response.status_code, 302)
# At this point the system will "send" us an email. We can "check" it thusly:
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Password reset on example.com')
# Now, here's the kicker: we get the token and userid from the response
token = response.context[0]['token']
uid = response.context[0]['uid']
# Now we can use the token to get the password change form
response = self.c.get(reverse('password_reset_confirm', kwargs={'token':token,'uidb64':uid}))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template_name, 'authentication/password_reset_confirm.html')
# Now we post to the same url with our new password:
response = self.c.post(reverse('password_reset_confirm',
kwargs={'token':token,'uidb36':uid}), {'new_password1':'pass','new_password2':'pass'})
self.assertEqual(response.status_code, 302)
And that's it! Not so hard after all.
This is how I did it for a functional test:
def test_password_reset_from_key(self):
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import base36_to_int, int_to_base36
user = User.objects.all()[:1].get()
token = default_token_generator.make_token(user)
self.get("/accounts/password/reset/key/%s-%s/" % (int_to_base36(user.id), token))
self.selenium.find_element_by_name("password1").send_keys("password")
self.selenium.find_element_by_name("password2").send_keys("password")
self.selenium.find_element_by_name("action").submit()
alert = self.selenium.find_element_by_css_selector(".alert-success")
self.assertIn('Password successfully changed.', alert.text)
The project I'm working on has some data that needs to get passed to every view, so we have a wrapper around render_to_response called master_rtr. Ok.
Now, I need our 404 pages to run through this as well. Per the instructions, I created a custom 404 handler (cleverly called custom_404) that calls master_rtr. Everything looks good, but our tests are failing, because we're receiving back a 200 OK.
So, I'm trying to figure out how to return a 404 status code, instead. There seems to be an HttpResponseNotFound class that's kinda what I want, but I'm not quite sure how to construct all of that nonsense instead of using render_to_response. Or rather, I could probably figure it out, but it seems like their must be an easier way; is there?
The appropriate parts of the code:
def master_rtr(request, template, data = {}):
if request.user.is_authenticated():
# Since we're only grabbing the enrollments to get at the courses,
# doing select_related() will save us from having to hit database for
# every course the user is enrolled in
data['courses'] = \
[e.course for e in \
Enrollment.objects.select_related().filter(user=request.user) \
if e.view]
else:
if "anonCourses" in request.session:
data['courses'] = request.session['anonCourses']
else:
data['courses'] = []
data['THEME'] = settings.THEME
return render_to_response(template, data, context_instance=RequestContext(request))
def custom_404(request):
response = master_rtr(request, '404.html')
response.status_code = 404
return response
The easy way:
def custom_404(request):
response = master_rtr(...)
response.status_code = 404
return response
But I have to ask: why aren't you just using a context processor along with a RequestContext to pass the data to the views?
Just set status_code on the response.
Into your application's views.py add:
# Imports
from django.shortcuts import render
from django.http import HttpResponse
from django.template import Context, loader
##
# Handle 404 Errors
# #param request WSGIRequest list with all HTTP Request
def error404(request):
# 1. Load models for this view
#from idgsupply.models import My404Method
# 2. Generate Content for this view
template = loader.get_template('404.htm')
context = Context({
'message': 'All: %s' % request,
})
# 3. Return Template for this view + Data
return HttpResponse(content=template.render(context), content_type='text/html; charset=utf-8', status=404)
The secret is in the last line: status=404
Hope it helped!