django over https form redirection issues - django

I am using Nginx + supervisord to host a django site behind SSL. The site index loads fine. Everything locally works fine without HTTPS using local server. I am using Django 1.4.2
For some reason I get weird redirections.
When using admin if I edit any item I get redirected to home page.
When submitting new item for save I get 404 (but data is saved).
Non admin:
Again form submit returns me to homepage instead of "success".
The reason for going to homepage I can explain. My nginx redirects all not http traffic to https://localhost with a 301 redirect. So I am guessing django does not think I need secure URLs in places.
The problem is django is not assuming secure url or rather
request.is_secure is False.
I have noted this SO
Accessing Django Admin over HTTPS behind Nginx
Made the changes for proxy pass, i dont think it does anything to handle this. But here it is as is.
settings.py
SESSION_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
CSRF_COOKIE_SECURE = True
warning: I am fairly new to django.

I removed the Nginx redirection. Django already handles the redirection correctly.
This was the settings.py values I used together with my nginx proxy pass
SESSION_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
CSRF_COOKIE_SECURE = True
Nginx proxy
location / {
proxy_pass_header Server;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_connect_timeout 10;
proxy_read_timeout 10;
proxy_pass http://localhost:8000/;
}
where localhost:8000 is where gunicorn is running.
I still have other issues but redirection problem is solved.

Related

Configure nginx/gunicorn django behind a load balancer

I set up an nginx/gunicorn server with this tutorial. This worked like a charm with a local docker-compose file. Then I pushed the containers to AWS fargate, and set up a load balancer in front of the nginx. This worked too, but I got a "CSRF failed" exception, when trying to login to django admin.
This is because the host, port and protocol are not correctly forwarded from the user request though the load balancer and the nginx proxy to django gunicorn.
How do I have to configure nginx and django?
I ended up with this nginx configuration, which works behind a load balancer and with my local docker-compose config, where the user directly requests to nginx. rg_ is just the prefix for my project.
# Set proto and port to forwarded value, or to nginx value if
# forwarded value not set
map $http_x_forwarded_proto $rg_forwarded_proto {
default $scheme;
"~^(.*)$" $1;
}
map $http_x_forwarded_port $rg_forwarded_port {
default $remote_port;
"~^(.*)$" $1;
}
# Gunicorn proxy
upstream rg_serve {
server localhost:8100;
}
# Server
server {
listen 8090;
location / {
proxy_pass http://rg_serve;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $rg_forwarded_proto;
proxy_set_header X-Forwarded-Port $rg_forwarded_port;
proxy_set_header X-Forwarded-Host $host:$rg_forwarded_port;
proxy_redirect off;
}
location /static/ {
alias /home/app/rg_serve/web/static_collect/;
}
}
I have to set
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
in my django settings, to make it work.
What feels a bit strange is, that I have to add the port to X-Forwarded-Host to make it work in django. When I just do not add it, and set django settings to
USE_X_FORWARDED_HOST = True
USE_X_FORWARDED_PORT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Is does not work. I get the csrf exception.

flask redirect command goes to http rather than https

I have a flask app behind an nginx server. Nginx handles SSL termination and redirect to https. All http requests are redirected.
In flask, which I don't think should have to know about any of this, I have this bit of code:
#bp.route('/', methods=['GET', 'POST'])
def index_root():
"""Assign a session tag.
"""
return redirect(url_for('main.index', tag=make_new_tag()))
#bp.route('/D/<tag>/accueil', methods=['GET', 'POST'])
def index(tag):
return render_template('index.html', title='', tag=tag)
Now I have a problem: When someone requests http://example.com/, they are 301'd by nginx to https://www.example.com/, which is correct. Then flask 302's them to http://www.example.com/D/123/accueil, which is less good, because nginx will just 301 them to https://www.example.com/D/123/accueil (which is good, but I'd have preferred to skip that extra redirect).
Is this a configuration issue somewhere?
Note that in dev it's important that flask really not think about https. Indeed, flask shouldn't need to know anythint at all about https, and this is why I'm finding this a bit mystifying.
Thanks for any pointers.
The current way to achieve this appears to be passing x_proto=1 as an argument to Werkzeug's ProxyFix middleware:
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1 ,x_proto=1)
Be sure to set x_for correctly, as per the docs. This is set to the number of proxies, and must be correct for security reasons, otherwise Flask may trust some headers set by an internet user as if they'd come from the reverse proxy. The danger is setting this to 1 when there's no proxy.
Then in the nginx ensure you're setting the X-Forwarded-Proto header in the location block, before your proxy_pass:
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:9999/;
}
I haven't tested this with nginx but was able to test directly against the dev server and gunicorn by setting that header specifically in curl:
curl -is http://localhost:5000/ -H 'X-Forwarded-Proto: https'
This returns:
HTTP/1.0 302 FOUND
Location: https://localhost:5000/test_str/accueil`
So it appears to effect url_for which doesn't require modifying individual url_for() calls in your code, which is the case with accepted answer linked by #PGHE.
I've solved this problem on reverse proxy side (Nginx) by using X-Forwarded-HTTPS instead of X-Forwarded-Proto
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_pass http://localhost:9999/;
}

CORS error while consuming calling REST API with React

I created a restful api with django-rest-framework accessible with this URL http://192.168.33.10:8002/scenarios/ and I'm creating a React app to make calls to the api an d consume its data.
I'm using fetch to make calls to the api
componentWillMount: function(){
this.setState({Problemstyle: this.props.Problemstyle})
fetch('http://192.168.33.10:8002/scenarios/')
.then(result=>result.json())
.then(result=> {
this.steState({items:result})
})
},
when i run my app i get an error in my browser
Fetch API cannot load http://192.168.33.10:8002/scenarios/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://192.168.33.10:8001' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I'm not sure on how to solve this problem as i'm just starting to use React
Please Note: This solution is not for production configuration. This is merely a workaround for easier setup while development. Please refrain from using this in production configuration.
Install django-cors-headers through pip install django-cors-headers
Then, add in installed apps 'corsheaders'.
Add the setting,
CORS_ORIGIN_ALLOW_ALL = True
and,
ALLOWED_HOSTS = ['*']
This should do the trick.
UPDATE
You'll also need to add it to the middlewares,
MIDDLEWARE = [ # Or MIDDLEWARE_CLASSES on Django < 1.10
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
The currently accepted answer potentially opens sites up to security risks:
Why does the error happen:
In order for your AJAX request to work well, there are two things that need to happen:
The request has to be accepted by the server.
The returned request must be accepted by the browser, so that the client-side can do something with it.
The error that the OP reports, indicates that the second part of this process is failing. This is because if the request is sent from domain that is different to the server returning the request, the browser won't accept it unless the appropriate headers are set (that is, the server has given permission for the browser to read it).
How to fix it:
Now to fix this, we can use django-cors-headers. This will add the apropriate headers, so that the browser accepts the returned response. To install run:
pip install django-cors-headers
and add it to your middleware:
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
Now, you need to add the domain you are sending your AJAX request from, to the list of allowed domains:
CORS_ALLOWED_ORIGINS = [
"www.example.com",
"http://127.0.0.1:8000",
...
]
What about CORS_ORIGIN_ALLOW_ALL?
Do not use this unless you have a specific need to. Setting this to true, will mean that, any origin will be able to make a request to your API, and get a response. Unless you are making a public API, you probably won't need to do this. More likely you will only need to serve a single domain or a few domains (maybe you have a front-end, served from a different place to your API etc.)
If you are happy for any domain to access your API then you can set the following:
CORS_ORIGIN_ALLOW_ALL = True
If you do this, you will also need to set the following:
ALLOWED_HOSTS = ['*']
The reason for this, is Django will only accept certain hosts by default, so there's no point setting CORS_ORIGIN_ALLOW_ALL = True unless you're actually going to accept requests from anyone (that is the part 1 in the explanation above).
Note that by setting allowed hosts to a wildcard, you open yourself up to HTTP host header attacks. Make sure you understand these, and have made sure you are not affected. You can read more about them in the django docs.
Also note: if you have not set your ALLOWED_HOSTS and you are wondering why your requests are working, it is because when DEBUG=True certain hosts are allowed automatically, http://127.0.0.1:8000 etc.
Using django-cors-headers
Start by installing django-cors-headers using pip
pip install django-cors-headers
You need to add it to your project settings.py file:
INSTALLED_APPS = (
##...
'corsheaders'
)
Next you need to add corsheaders.middleware.CorsMiddleware middleware to the middleware classes in settings.py
MIDDLEWARE_CLASSES = (
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.BrokenLinkEmailsMiddleware',
'django.middleware.common.CommonMiddleware',
#...
)
You can then, either enable CORS for all domains by adding the following setting
CORS_ORIGIN_ALLOW_ALL = True
Or Only enable CORS for specified domains:
CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = (
'http//:localhost:8000',
)
See Yinon_90's answer for a better working version of this concept.
I wanted to propose a solution that does not require altering the behavior of the Django or React apps.
In production, you might want to serve both apps on the same domain/from the same server, under different paths. This wouldn't cause any CORS conflict in production.
Of course, we want to debug Django and utilize React HMR & Dev tools while debugging. For this, I've spun up an nginx docker container:
docker-compose.yml:
version: '3.8'
services:
web:
image: nginx:latest
ports:
- 8080:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
command: [nginx-debug, '-g', 'daemon off;']
environment:
- NGINX_HOST=localhost
- NGINX_PORT=80
nginx.conf:
Django debug is on port 8000, React HMR is on port 3000. I've allowed three paths to go to the Django app, /accounts, /api, and /admin. The rest goes to the React app (and into the React Router system)
events {}
http{
server {
listen 80;
server_name localhost;
location ~* /(accounts|api|admin) {
proxy_pass http://host.docker.internal:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://host.docker.internal:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
After docker-compose up, manage.py runserver, and npm start, both apps are available at localhost:8000!
UPDATE (12.12.22)
Just add to the package.json file the following line:
"proxy": "http://localhost:8000"
OLD ANSWER
Based on Joe Sadoski's great idea,
I suggest an improvement to his solution that also supports react hot-reload-on-changes:
(All other proposed solutions here do not work in chrome since the last upgrades in 2022)
version: '3.1'
services:
web:
image: nginx:latest
ports:
- 8080:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
command: [nginx-debug, '-g', 'daemon off;']
environment:
- NGINX_HOST=localhost
- NGINX_PORT=80
network_mode: host
restart: unless-stopped
My change here is: network_mode: host
Now the nginx.conf looks like this:
events {}
http{
server {
listen 80;
server_name localhost;
// this block handle react hot-reload
location /sockjs-node {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
// this block handle API calls
location ~* /(accounts|api|admin) {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
// this block handle all the rest (react/statics...)
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
Here I change the upstreams to point on localhost so both react server and django should run on the host computer in ports 3000 and 8000.
So, after docker-compose up, manage.py runserver, and npm start, both apps are available at http://localhost!

CSRF verification Failed - Referer is insecure while host is secure

I upgraded Django from 1.8 to 1.9. Afterwards, I get this error on my localhost after the Django admin login:
Referer checking failed - Referer is insecure while host is secure.
Everything works fine in production.
Below is a snippet of my settings.py file:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
I had this error when I was switching from ssl setup to no ssl and forgot to remove last line from upstream configuration in nginx config:
location / {
proxy_pass http://127.0.0.1:8085;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host; #:8080;
#proxy_set_header X-FORWARDED-PROTO https;
}
Those lines in your settings.py file are fine on production because you're using an SSL certificate attached to your domain. However, on local you're probably using http://localhost:8000 or something similar. If you try to connect via https://localhost:{{YOUR_PORT_NUMBER}} you'll most likely get an error like ERR_SSL_PROTOCOL_ERROR.
The issue is in lines 167-168 of django/django/middleware/csrf.py. When you're using https on production, request.is_secure() is returning True...which requires that the HTTP_REFERER also be true or you'll get the error you referenced.
One solution would be to adjust your settings.py file depending on whether you're in your local or production environment. That way you can add those three lines to a settings_production.py file that imports other settings that are common to both localhost and your production server. Your localhost would use a different set of settings that don't include those lines.

How to host a Django project in a subpath?

I am building an API with Django REST framework which is served via Gunicorn and Nginx. The project "exampleproject" has to run at a subpath such as: https://100.100.100.100/exampleproject (example IP address). I do not have a domain name registered for the IP.
Currently, the start page renders as expected at https://100.100.100.100/exampleproject. However a the resource path for "products" does not work. Instead of https://100.100.100.100/exampleproject/products the start page displays https://100.100.100.100/products - which does not work.
I configured the subpath for exampleproject in /etc/nginx/sites-enabled/default as follows:
server {
# ...
location /exampleproject/ {
proxy_pass http://localhost:8007/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
When I manually visit https://100.100.100.100/exampleproject/products Nginx records the following in /var/log/nginx/access.log:
"GET /products/ HTTP/1.1" 404 151 "-"
I found here that one needs to add the following setting to Django's configuration in settings.py:
FORCE_SCRIPT_NAME = '/exampleproject'
This seems to rewrite all paths for nested resources.
Just remove the trailing slash from the proxy pass URL, like this:
proxy_pass http://localhost:8007;
If proxy_pass is specified without a URI, the request URI is passed to the server in the same form as sent by a client when the original request is processed, then keeping the /exampleproject/ part.
See: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
Then you will need to configure Django to understand the URLs under /exampleproject/.
First, you can prepend the subpath to all URLs by updating the urls.py file like this:
urlpatterns = [url(r'^exampleproject/', include(urlpatterns))]
And, in case you are serving statics and media just update in the settings:
MEDIA_URL = '/exampleproject/media/'
STATIC_URL = '/exampleproject/static/'
Of course you can create a custom setting to replicate this or to make the app work without a subpath as well, I believe this is a better solution than FORCE_SCRIPT_NAME because in that case I found we need to update the Django URLs anyways.