I'm trying to add change password functionality to a Django / React app. I'm struggling to find any way to make this work. I'm using django-rest-auth and django-rest-framework.
I would be happy to either submit the password change request from a React page, or else to redirect the user to the native Django template-driven page - leaving my SPA, but acceptable to get the functionality working.
However, the change password page requires the user to be logged in, and I can't see how to do this from the React app. I've set up the Django page with a custom template, but if I link to it from the app then the URL redirects to login:
If I copy the request to cURL and run it in a terminal, I see this error:
# api/urls.py
from django.urls import include, path
from django.contrib.auth import views
from django.conf.urls import include, url
urlpatterns = [
...
path('password_change/', views.PasswordChangeView.as_view(template_name='account/password_change.html'), name='password_change'),
]
account/password_change.html
{% extends 'account/base.html' %}
{% block body_block %}
<h1>Change your password</h1>
<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
{% endblock %}
I log in to my app and then navigate to where the change password page should be:
http://localhost:8000/api/v1/password_change/
This is then redirected to:
http://localhost:8000/accounts/login/?next=/api/v1/password_change/
My guess is that this is because the endpoint requires authentication and it's not been provided. But although I have a stored authentication token, I can't find out how to send this as part of the weblink and previous experience with reset password suggests that it's not the token Django would be looking for from a template.
I've hunted for ages and can't find any example of how to add change password to a React app. Surely this must be something every React app needs? What basic thing am I missing?
Edit: thanks to Rik Schoonbeek for the answer, which is that the endpoint supplied by djang-rest-auth can be used directly. Somehow I missed this in all my confusion. In case it helps anybody else, here is sample working code that uses JavaScript fetch and hard-coded passwords instead of data from the form, just to demonstrate the process.
This code also demonstrates detecting an error in the fetch command, which doesn't work quite like you might expect. There are probably better ways to do it, but this does at least give you a hook.
Working code:
export const changePassword = () => (dispatch) => {
const token = localStorage.getItem('jwtToken');
const data = {
'new_password1': 'othertext',
'new_password2': 'othertext',
'old_password': 'sometext'
};
var formData = new FormData();
// Push our data into our FormData object
for(var name in data) {
formData.append(name, data[name]);
}
const headers = {
'Authorization': `Token ${token}`,
};
return fetch('/api/v1/rest-auth/password/change/', {
headers,
'method': 'POST',
'body': formData,
})
.then(res => {
if(res.ok) {
console.log('successfully changed password');
return res.json();
} else {
console.log('error changing password');
}
})
.then(res => {
console.log('Change password res ', res);
});
};
And important! add this to settings.py, otherwise the password will be changed regardless of whether the old password is correct:
OLD_PASSWORD_FIELD_ENABLED = True
And if you want the user to stay logged in after they have reset their password, add this too:
LOGOUT_ON_PASSWORD_CHANGE = False
If you use token authentication, like I do, you can authorize the user by sending the token in the header of the request to change the password. You need to add "Token " in front of the token though (see example below).
This is how I did it, using axios, and token authentication.
const response = await axios.post(
"http://127.0.0.1:8000/rest-auth/password/change/",
values,
{
headers: { Authorization: "Token " + this.props.loginToken }
}
);
The password change endpoint requires the following data to be sent:
new_password1
new_password2
old_password
As explained here in the django-rest-auth docs
So, the "values" parameter in my above axios request is a json containing those values.
Related
I'm trying to get an understanding of how CSRF tokens work, currently my goal is to create a situation where the CSRF attack is possible. I'm hosting two Django apps locally on different ports. I access one by localhost:8000, the other by 127.0.0.1:5000 -- that ensures cookies are not shared between apps.
There's an API view
class ModifyDB(APIView):
def post(self,request,format=None):
if request.user.is_authenticated:
return Response({'db modified'})
else:
return Response({'you need to be authenticated'})
which shouldn't be accessed by unauthenticated users.
The "malicious" site has a button that's supposed to trigger the attack when a user is logged on the target site:
const Modify = () => {
const onClick = async e => { e.preventDefault();
const instance = axios.create({
withCredentials: true,
baseURL: 'http://localhost:8000',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
})
const res = await instance.post('/api/modifydb');
return res.data
}
return (
<button class = 'btn' onClick = {onClick}> send request </button>
)
}
My authentication settings are as follows:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'my_proj.settings.CsrfExemptSessionAuthentication',
],
}
where CsrfExemptSessionAuthentication is a custom class that disables csrf protection for my educational purposes:
class CsrfExemptSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
return
django.middleware.csrf.CsrfViewMiddleware is also disabled.
Both CORS_ALLOW_ALL_ORIGINS and CORS_ALLOW_CREDENTIALS are set to true. My question is: what am I doing wrong, because when I'm logged in on the attacked site this axios request from the malicious site returns "you need to be authenticated" response.
I expected that somehow sessionid cookie would be included and request would be successful (which is the logic behind this type of attack if I'm understanding it correctly).
UPDATE
I tried to use a form
<form action="http://localhost:8000/api/modifydb" method="POST">
<input type="submit" value="make a malicious request"/>
</form>
instead of the axios request. It works with this additional setting:
SESSION_COOKIE_SAMESITE = None
while my previous code for request still doesn't work even with this new setting.
Now, I don't get what is the difference between POST requests from a form and from axios regarding cookies attachment. And does SESSION_COOKIE_SAMESITE if enabled prevents this type of attack completely?
I have the following setup :
a frontend app developed with VueJS on which I have a registration form component, including few standard fields, a mandatory checkbox (user agreement), and a Google Recaptcha V2 (vue-recaptcha) checkbox
a backend API developed with Django Rest Framework, with a specific route to request Google Recaptcha web service (using rest_framework_recaptcha application)
Route declaration in urls.py
urlpatterns = [
...
path('recaptcha_verify/', RecaptchaVerifyView.as_view(), name='recaptcha_verify'),
...
]
DRF view :
class RecaptchaVerifyView(APIView):
allowed_methods = ["POST"]
def post(self, request, *args, **kwargs):
serializer = ReCaptchaSerializer(data=request.data)
if serializer.is_valid():
return Response({'success': True}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Simple serializer based on rest_framework_recaptcha:
from rest_framework_recaptcha.fields import ReCaptchaField
class ReCaptchaSerializer(serializers.Serializer):
token = ReCaptchaField()
Frontend VueJS component :
<v-form>
<v-checkbox
v-model="consentCheckbox"
:rules="[rules.required]"
>
<template v-slot:label>
<div>User agreement message</div>
</template>
</v-checkbox>
<div align="center">
<vue-recaptcha
ref="recaptcha"
#verify="onVerify"
:sitekey="sitekey"
:loadRecaptchaScript="true"
>
</vue-recaptcha>
</div>
</v-form>
And the methods associated to the component (handleRegister is called when the submit button is clicked) :
onVerify(response) {
this.message = false;
this.recaptchaResponse = response;
this.recaptchaVerified = true;
},
async handleRegister() {
// Check if recaptcha has been ticked
if (!this.recaptchaVerified) {
return true; // prevent form from submitting
}
else if ( this.$refs.form.validate() ) {
// Check recaptcha by calling backend (DRF) specific route (itself calling Google API)
var response = await RecaptchaService
.verifyRecaptcha(this.recaptchaResponse)
.catch(error => this.message = (error.response && error.response.data) || error.message || error.toString());
if (response.data.success) {
// process
}
}
Finally, the VueJS service dedicated to calling recaptcha API on backend:
import axios from 'axios';
const API_URL = process.env.VUE_APP_API_HOST_NAME + '/api/v1/accounts/';
class RecaptchaService {
async verifyRecaptcha(token) {
return axios.post(API_URL + 'recaptcha_verify/',
{
token: token,
})
.then((response) => {
return response;
})
.catch((error) => {
//catch error
}
I don't have any issue working in a development environment (frontend and backend accessible on localhost). Yet, when deployed in production (with https enabled), I face the following problem:
Checking the user agreement checkbox first, then the Recaptcha works fine
Checking the Recaptcha then the user agreement returned a 403 Forbidden error, with the message CSRF token missing or incorrect
Once I get the previous error, I always get the error when filling the form again, no matter the order of checking boxes
I don't get why:
the problem does not occur on localhost
the order of filling form lead to different behaviours
the problem only occurs on my registration form while I do have several other forms (login, sending data, etc.) that work fine
Any ideas to help me understand/investigate the problem ? I have read many posts related to Django and CSRF or CORS related issues but they are often related to django templates or SessionAuthentication.
It's not easy to guess the problem without seeing the code.
However, since localhost works maybe the base url in production in your frontend is not https:// but http://.
I have a Django powered web-app which also utilizes the Django REST framework. I want to read and write to my database using the API generated by Django REST.
I can do this successfully when I hardcode the password of the current user, but I can't do this when I attempt to pass in the retrieved password from Django, as Django does not store the password as plain text. (I know I can change this, but I am hoping there is a better way).
Essentially, my logic flow is this:
user logs in using Django login form
user would like to write new data to database, but will need API token
Obtain API token (already generated when they sign up)
use this token to authenticate and POST JSON data to REST framework
I can do all of these steps above when I hardcode the password as plain text in my script, but I would like to "retrieve" the password and pass it automatically if possible.
Code below works as described above:
views.py
class DefaultsListView(LoginRequiredMixin,ListView):
model = models.DefaultDMLSProcessParams
template_name = 'defaults_list.html'
login_url = 'login'
def get_queryset(self):
return models.DefaultDMLSProcessParams.objects.filter
(customerTag=self.request.user.customerTag)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
testing = super().get_context_data(**kwargs)
userName = str(self.request.user.username)
passWord = str(self.request.user.password)
url = 'http://127.0.0.1:8000/api-token-auth/'
context = {'tokenURL': url, 'user':userName, 'pass':passWord}
return context
template.html
<div class = "debugging">
<p id = "csrf">{% csrf_token %}</p>
<p id = "tokenURL">{{tokenURL}}</p>
<p id = "user">{{user}}</p>
<p id = "pass">{{pass}}</p>
</div>
script.js
var csrfToken = document.querySelector("#csrf input").value;
var user = document.getElementById("user").innerHTML;
var pass = document.getElementById("pass").innerHTML;
var xhr = new XMLHttpRequest();
var url = "http://127.0.0.1:8000/api-token-auth/";
xhr.open("POST", url,);
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader( "X-CSRFToken", csrfToken);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var json = JSON.parse(xhr.responseText);
console.log(json.token);
}
};
var data = JSON.stringify({"username": user, "password": 'myhardcodedpass' });
xhr.send(data);
This script will log the API token of the current user to the console as expected. But instead of hardcoding the password, I would like to just pass it as a variable, but the password is a very long string as it is not stored as plain text (var pass). So trying to use pass is rejected, as it obviously isn't the correct password when passed as text.
Is there a way to decrypt this password, or translate it somehow? Or perhaps an easier way to retrieve the API token from Django REST?
First and foremost, you should definitely NOT store the password as plain text. That is never the solution in any case, and only will cause problems.
I have a few suggestions that you can potentially use, but the easiest would be to simply use a JSON Web Token which can easily be integrated with Django Rest Framework using the django-rest-framework-jwt library.
Your workflow would basically be as follows:
User logs in successfully, which returns a JWT to authenticate the user
The script would be included in the header of the request, and the middleware would be able to successfully identify and authenticate the user.
Hopefully this gives you an idea on how to move forward. Best of luck
I have a complex problem in sending and receiving data in react to django with axios.
I'm not using REST API. This is my Handel function which is related with my signup form tag and after each click on submit button this function executes:
HandelSignUp(e){
e.preventDefault();
let Username = this.refs.username.value;
let Pass = this.refs.pass.value;
let Email =this.refs.email.value;
axios({
url:'http://127.0.0.1:8000/signupAuth/',
mothod:'post',
data:{
username:this.Username,
pass:this.Pass,
email:this.Email
},
headers: {
"X-CSRFToken": window.CSRF_TOKEN,
"content-type": "application/json"
}
}).then(respons =>{
console.log(respons);
})
.catch(err =>{
console.log(err);
});
and also this is my django urls.py :
urlpatterns = [
path('signupAuth/',ReactApp_View.signupRes),
]
ReactApp_View is my views.py in ReactApp file which I imported correctly.
ok now let's see my views.py:
def signupRes(request):
body_unicode = request.body.decode('utf-8')
data = json.loads(myjson)
return HttpResponse(request.body)
after all when I fill my signup fields and then click on button I see this massage in console log of my browser:
Failed to load http://127.0.0.1:8000/signupAuth/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access.
What should I do?
and an extra question: what happen in the given url in my axios?
just open your website with the same host url you are trying to call. Use http://127.0.0.1:8000/ instead of localhost
I have an application and I want the user to be able to change the password, while staying logged in.
I'm doing this:
request.user.set_password(password)
request.user.save()
request.user.backend='django.contrib.auth.backends.ModelBackend'
auth_login(request, request.user)
It changes the password and I log in again to keep the user session. But apparently if I do this the CSRF_TOKEN which I have saved in my js client as a variable is no longer valid and I can't use it for POST requests.
Is there a way to renew the CSRF_TOKEN and send it to the client?
From the view, you can get the token with this:
from django.core.context_processors import csrf
print unicode(csrf(request)['csrf_token'])
As far as how to update your js client I would need more details about it before giving good advice. I imagine it would be as simple as returning the new token from the login request. I imagine your login is ajax so you could return the new token in the response and write some JS to update your stored token:
import json
return HttpResponse(json.dumps({"csrf_token": unicode(csrf(request)['csrf_token'])), content_type="application/json")