Django, CORS, CSRF - am I doing it right? - django

My setup (local) is the following:
Vue.js running on localhost:8080 (npm run serve)
REST API built with Django running on localhost:8000 (./manage-py runserver)
In order to enable this to work I've made the following additions:
ALLOWED_HOSTS = [
...
'localhost',
'localhost:8000',
'localhost:8080',
]
INSTALLED_APPS = [
...
'rest_framework',
'corsheaders',
]
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
CORS_ORIGIN_WHITELIST = (
'localhost:8080',
'localhost:8000',
)
CORS_ALLOW_CREDENTIALS = True
from corsheaders.defaults import default_headers
CORS_ALLOW_HEADERS = default_headers + (
'credentials',
)
One of my API functions:
#ensure_csrf_cookie
def try_login(request):
# this is just to get the initial CSRF token:
if request.method == "GET" or request.method == "OPTIONS":
return JsonResponse({'status': 'ok'})
# else, an actual login request:
else:
data = JSONParser().parse(request)
user = authenticate(request, username=data['user'] , password=data['pass'])
if user is not None:
login(request, user)
return JsonResponse({'login_succ': 'ok'});
else:
return JsonResponse({'login_succ': 'fail'});
Finally, in Vue:
api: function(endpoint, method, data) {
var headers = new Headers();
headers.append('content-type', 'application/json');
if (... this is not the first request ever ...)
{
csrftoken = document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*\=\s*([^;]*).*$)|^.*$/, "$1");
headers.append('X-CSRFToken', csrftoken);
}
method = method || 'GET';
var config = {
method: method,
body: data !== undefined ? JSON.stringify(data) : null,
headers: headers,
};
config['credentials'] = 'include';
return fetch(endpoint, config)
.then(response => response.json())
.catch((error) => { console.log(...); });
},
trylogin: function() {
// initial request: just to get the CSRF token
this.api(".../login/", "GET").then(
response => {
this.api(".../login/", "POST", {'username': ..., 'password': ...} ).then(
response => {
if ("login_succ" in response && res["login_succ"] == "ok")
{} // user is logged in
}
);
}
);
}
What happens now, afaiu, is that my initial API request (which does not have to be pointed to the endpoint equal to the subsequent POST request, right?) gets the CSRF token as a cookie. Every subsequent request reads this cookie and sets the X-CSRFToken header. The cookie itself is also being sent in the subsequent requests. I do not understand why is the token needed in both places.
Is this approach correct? Is everything I've done necessary? (Are there redundant parts?) I'm especially interested in the way that I should acquire the token in the first place, and in general with the token's lifecycle.
Thank you.

Related

Fastapi Testclient not able to send POST request using form-data

Currently I am doing Unit Testing in Fastapi using from fastapi.testclient import TestClient
def test_login_api_returns_token(session,client):
form_data = {
"username": "mike#gmail.com",
"password": "mike"
}
response = client.post(
"/api/login",
data=form_data,
headers={"content-type": "multipart/form-data"}
# headers={"content-type": "application/x-www-form-urlencoded"}
)
result = response.json()
assert response.status_code == 200
I am supposed to get token as response which I am getting when I run the fastapi application but not able to proceed with Unit Testing with the same.
Example of postman request for the same
How do I make sure form-data is being sent from TestClient?
api/login.py
#router.post("/login")
async def user_login(form_data: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(get_session)):
user = authenticated_user(form_data.username, form_data.password, session)
user = user[0]
if not user:
raise token_exception()
token_expires = timedelta(minutes=120)
token = create_access_token(username=user.username, user_id=user.id, expires_delta=token_expires)
token_exp = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
return {
"status_code": status.HTTP_200_OK,
"data":{
"username": user.username,
"token": token,
"expiry": token_exp['exp'],
"user_id": user.id
}
}
Try set the header to Content-Type form-data like
def test_login_api_returns_token(session,client):
form_data = {
"username": "mike#gmail.com",
"password": "mike"
}
response = client.post(
"/api/login",
json=form_data,
headers={ 'Content-Type': 'application/x-www-form-urlencoded'}
)
result = response.json()
assert response.status_code == 200

Difference between fetch and postman's results

I have a simple login backend which has two routes login and get_user. After a user is logged in, a cookie is set such that it enables other routes like get_user. I tested this backend with Postman and after correct login, the cookie is set and get_user responds with user's data.
However, when I try to use fetch or axios in React and JS, I get problems. After I fetch login, I can see the cookie is sent, however, fetch get_user acts like cookie is not set at all.
I provided a minimal example to show that server side session somehow doesn't work with fetch:
Frontend:
<!DOCTYPE html>
<html>
<body>
<h1> Set value: </h1> <h2 id="set_value"></h2>
<h1> Get value: </h1> <h2 id="get_value"></h2>
</body>
<script>
// ..\..\Flask_general_framework\backend\venv\Scripts\Activate.ps1
async function Set_user_fetch()
{
// Get user
let user = await fetch('http://127.0.0.1:5000/set/gggg', {
'method': 'GET',
//credentials: 'include',
mode: 'cors',
credentials: "same-origin",
headers: {'Content-type': 'application/json', 'Accept': 'application/json',
'Access-Control-Allow-Origin': '*', // Required for CORS support to work
'Access-Control-Allow-Credentials': true, // Required for cookies, authorization headers with HTTPS},
'Access-Control-Allow-Headers': 'Content-Type, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Date, X-Api-Version, X-File-Name',
}
})
user = await user.json();
console.log("await user:", user);
document.getElementById("set_value").innerHTML = user.value;
}
async function Get_user_fetch()
{
let user = await fetch('http://127.0.0.1:5000/get', {
'method': 'GET',
//credentials: 'include',
credentials: "same-origin",
mode: 'cors',
headers: {'Content-type': 'application/json', 'Accept': 'application/json',
'Access-Control-Allow-Origin': '*', // Required for CORS support to work
'Access-Control-Allow-Credentials': true, // Required for cookies, authorization headers with HTTPS},
'Access-Control-Allow-Headers': 'Content-Type, X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Date, X-Api-Version, X-File-Name',
}
})
user = await user.json();
console.log("await user:", user);
document.getElementById("get_value").innerHTML = user.value;
}
Set_user_fetch().then( () => {
Get_user_fetch();
});
</script>
</html>
Backend:
from re import I
from flask import Flask, session
from flask_session import Session
from flask_cors import CORS
import redis
import datetime as dt
app = Flask(__name__)
CORS(app, supports_credentials=True)
app.config['SECRET_KEY'] = 'super secret key'
#app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_PERMANENT'] = True
#app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:9876')
app.config['PERMANENT_SESSION_LIFETIME'] = dt.timedelta(days=7).total_seconds()
server_session = Session()
server_session.init_app(app)
#app.route('/set/<value>', methods=['GET', 'POST'])
def set_value(value):
session['value'] = value
return {"value": value}
#app.route('/get', methods=['GET', 'POST'])
def get_value():
return {"value": session.get('value', 'None')}
app.run(host='127.0.0.1', port=5000, debug=True)
Server Side
In order to support cross-site cookies in modern browsers, you need to configure your server to use the Set-Cookie attribute SameSite=None (see Flask-specific example here). Unfortunately, this also requires the Secure attribute and an HTTPS enabled server.
For local development, you can get around this by serving your client and server on the same hostname, eg localhost with SameSite=Lax (or omitting SameSite which defaults to "Lax").
By "same hostname" I mean that if your frontend code makes requests to localhost:5000, you should open it in your browser at http://localhost:<frontend-port>. Similarly, if you make requests to 127.0.0.1:5000, you should open it at http://127.0.0.1:<frontend-port>.
Lax same-site restrictions don't come into play if only the ports differ.
Client Side
You have a few problems here...
You're sending headers in your request that do not belong there. Access-Control-Allow-* are response headers that must come from the server.
You set credentials to same-origin but are sending a request to a different host. To use cookies, you need to set credentials to "include". See Request.credentials.
You have no error handling for non-successful requests.
You're also setting a lot of redundant properties and headers and can trim down your code significantly.
async function Set_user_fetch() {
const res = await fetch("http://127.0.0.1:5000/set/gggg", {
credentials: "include",
});
if (!res.ok) {
throw new Error(`${res.status}: ${await res.text()}`);
}
const user = await res.json();
console.log("await user:", user);
document.getElementById("set_value").innerHTML = user.value;
}
async function Get_user_fetch() {
const res = await fetch("http://127.0.0.1:5000/get", {
credentials: "include",
});
if (!res.ok) {
throw new Error(`${res.status}: ${await res.text()}`);
}
const user = await res.json();
console.log("await user:", user);
document.getElementById("get_value").innerHTML = user.value;
}
If you were using Axios, you would set the withCredentials config to true
axios.get("http://127.0.0.1:5000/set/gggg", {
withCredentials: true
});

store UUID in session using middleware

I want to tag all requests with a UUID
(if the request doesn't have it in the first place).
I want to store the UUID in the session, so I wrote this middleware.
class MachineIDMiddleware:
"""
tags requests with machine UUIDs.
The machine-ID is set in the session.
"""
MID_KEY = "machine_id"
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
print(request.session.get(self.MID_KEY))
if self.MID_KEY not in request.session:
# set the machine-ID for the request
# if it has not been set already (making
# sure that it is serializable).
next_id = str(uuid.uuid4())
request.session[self.MID_KEY] = next_id
return self.get_response(request)
However, from my client, I noticed that the UUID keeps changing for every request.
From my client, I noticed that the sessionid cookie also changed for every request made.
As a result, a new UUID was generated for every request.
This is not what I want, though. I want to maintain only one UUID per person (who might be anonymous).
How can I achieve this?
Thanks a lot!
EDIT
export const Adapter = axios.create({
baseURL: baseURL,
headers: {
"Content-Type": "application/json"
}
});
Adapter.interceptors.request.use(
(request) => {
const token = tokenSelector(store.getState());
if (token) {
request.headers.Authorization = `Token ${token}`;
}
return request;
},
(error) => {
return Promise.reject(error);
}
);
Adapter.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// handle unauthorized errors.
if (error.response.status === 401) {
store.dispatch(clearToken());
history.replace(SLUGS.login);
}
// handle internal server errors.
if (error.response.status === 500) {
toast.dark("Something went wrong. Please try again later.");
}
// handle server ratelimits.
if (error.response.status === 429) {
toast.dark("You are being ratelimited.");
}
return Promise.reject(error);
}
);
This is how I send requests from the frontend.
I use axios. I checked my cookies in the developer tools panel
and couldn't see the sessionid cookie there.
EDIT 2
Chrome devtools shows me the following error and is not
setting the sessionid cookie properly. Is this the reason maybe?
** Answer (SOLVED)**
setting the following variables in my settings.py file
made sure that chrome set the cookies correctly.
# CORS configuration
ALLOWED_HOSTS = ["*"]
CORS_ALLOW_ALL_ORIGINS = True
CSRF_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
CORS_ALLOW_CREDENTIALS = True
SESSION_COOKIE_HTTPONLY = False
Here is your correct __call__ function
def __call__(self, request):
print(request.session.get("MID_KEY"))
if "MID_KEY" not in request.session:
next_id = str(uuid.uuid4())
request.session["MID_KEY"] = next_id.
return self.get_response(request)
The key shall be a string (constant) not a variable

When I set CSRF_COOKIE_HTTPONLY = True then 403 (Forbidden) error occurred

In my settings.py:
...
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = False
CORS_ALLOW_CREDENTIALS = True
authenticate.py:
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.conf import settings
from rest_framework import exceptions
from rest_framework.authentication import CSRFCheck
def enforce_csrf(request):
"""
Enforce CSRF validation.
"""
check = CSRFCheck()
# populates request.META['CSRF_COOKIE'], which is used in process_view()
check.process_request(request)
reason = check.process_view(request, None, (), {})
print(reason)
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class CustomAuthentication(JWTAuthentication):
def authenticate(self, request):
.....
validated_token = self.get_validated_token(raw_token)
enforce_csrf(request)
return self.get_user(validated_token),validated_token
Error:
CSRF token missing or incorrect.
Forbidden: /photos/photo_support/
when I set CSRF_COOKIE_HTTPONLY = False then all work very well.
What's the reason when I set CSRF_COOKIE_HTTPONLY = True then they me throw 403 Forbidden error.
My Frontend is ReactJS.
TestMe.js:
Axios.defaults.withCredentials = true
Axios.defaults.xsrfCookieName = 'csrftoken';
Axios.defaults.xsrfHeaderName = 'X-CSRFToken';
const TestMe = () => {
....
const payHandle = () => {
Axios.post('http://localhost:8000/photos/photo_support/', {
data:data
})
.then(res => {
console.log(res.data)
})
.catch(error => alert(error.message))
}
...
I take it you are reading the CSRF token from the cookie at this point. As mentioned in the docs, setting CSRF_COOKIE_HTTPONLY=True doesn't provide you any practical benefit so if everything works with it being set to False, you should probably continue to do so.
However if you still need to set it to True, then you will need to read the value of the CSRF token from the hidden form field as mentioned in the docs again. These snippets are from the documentation:
Read the value like so:
{% csrf_token %}
<script>
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
</script>
Send it in your AJAX request like so:
const request = new Request(
/* URL */,
{
method: 'POST',
headers: {'X-CSRFToken': csrftoken},
mode: 'same-origin' // Do not send CSRF token to another domain.
}
);
fetch(request).then(function(response) {
// ...
});
I'm not well versed in React or Axios so you may have to adapt this concept to your needs.

request.user.is_authenticated() consistently returns false (Django)

The problem I'm having
I'm currently using Django v1.9 as a back-end for my Angular2 app (I'm not using the Django REST Framework yet, just using Django's authentication system and dumping JSON)
I'm trying to authenticate the user, log them in, and then allow them to edit their profile.
The first two steps seem to work. However, I'm having some trouble with request.user.is_authenticated() - it consistently returns false, even though I have called the login() function on the user previously.
The part that seems to work
#csrf_exempt
def userlogin(request):
body_unicode = request.body.decode('utf-8')
body = json.loads(body_unicode)
input_u = body['uname']
input_p = body['pword']
worked = False
user = authenticate(username=input_u, password=input_p)
if user is not None:
login(request, user)
context = { "login_data" : { "logged_in" : True, "user_id" : user.id } }
else:
context = { "login_data" : { "logged_in" : False, "user_id" : 0 } }
return HttpResponse(json.dumps(context), content_type="application/json")
The part I'm struggling with
#ensure_csrf_cookie
def user(request):
is_auth = False
if request.user.is_authenticated():
is_auth = True
context = { "is_auth" : is_auth }
return HttpResponse(json.dumps(context), content_type="application/json")
Note: I'm using is_authenticated() (function) and not is_authenticated (property) as I'm on Django v1.9 and not v.1.10 (source). I was previously making the mistake of checking for the property and it always returned true, but when I'd try to return the ID of the user from the request object it would always be null.
I keep getting false here. This is the first time I've tried auth with Django, so I just wanted to ask some questions here:
Am I doing something terribly wrong? I think I have all of the stuff I need in my settings:
INSTALLED_APPS = [
'search.apps.SearchConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders'
]
I also have 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware' in my middleware.
How exactly does Django know that the user is authenticated? I assume that since I have sessions activated, it checks for the session cookie. However, I suspect this could be the issue. On inspection, I had a cookie placed this afternoon for localhost. However, since then I've signed in and not been able to update it. I even tried Django's in-built test cookie function (source) but it wouldn't work when I tested it. My settings should be okay, I have the following:
INSTALLED_APPS = ['django.contrib.sessions']
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
MIDDLEWARE_CLASSES = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
...
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
...]
I suspect I'm missing something obvious but I've been reading other threads for a while now with no luck.
Thanks, guys!
Nick
Found the solution to my own problem from another problem I was having.
The issue is that Angular wasn't sending cookies to the Django server. As Angular is using CORS by default, I had to add { withCredentials : true } to my RequestOptions object.
Example:
editUser(userdata) {
console.log("UserService: createUser function called");
console.log(JSON.stringify(userdata));
if(this.validateData(userdata)) {
let headers = new Headers({
'Content-Type': 'application/json',
'X-CSRFToken': this.getCookie('csrftoken')
});
let options = new RequestOptions({ headers: headers, withCredentials: true });
return this._http
.post(
this._editUserUri,
JSON.stringify(userdata),
options)
.map(res => {
console.log(res.json());
return res.json();
})
}
}
Explained thoroughly here: Angular2 and Django: CSRF Token Headache
You don't have the contrib.auth app in INSTALLED_APPS.