The situation:
I am using React in the front-end and a Flask api server. I am wanting to send the data from React to the api and once I have done this I would like to use WTForms to run validations on the data before handling it. The question may seem similar to CSRF Protection with Flask/WTForms and React , but this does not answer the question, please take a look through I have put a lot of effort in writing a good question.
What I have
Currently the data is being sent successfully as a json object, where the keys match the names within the wtform structure, the aim is to get wtforms to take that json data and insert it into the object and and handle from there as normal
The JSON object being sent
{'username': 'tster', 'fullname': 'Tester test', 'phone': '038287827216', 'email': '[email protected]', 'password': 'Tester1010', 'repeatPassword': 'Tester1010'}
The entire concept is to sent this from the React component and add it to wtforms in order to validate accordingly and work with.
Python code:
I have tried multple different approaches, this being the latest:
@bp.route('/api/register_user', methods=['POST', 'GET'])
def register():
""" End-point to register users """
if request.method == 'GET':
return ('', {'csrf_token': generate_csrf()})
elif request.method == 'POST':
req = request.get_json(force=True)
if validate_csrf(request.headers['X-Csrftoken']):
print('validated')
return{"message": "posted"}
The concept was that if I couldn't get it to work with wtforms I could generate the csrf_token manually, validate it and once thats validated run manual validations against the data instead of relying on wtforms, however it doesn't seem to validate accordingly. This taken from https://flask-wtf.readthedocs.io/en/stable/api.html#flask_wtf.csrf.generate_csrf
Prior to this I tried:
@bp.route('/api/register_user', methods=['POST', 'GET'])
def register():
""" End-point to register users """
form = RegistrationForm()
print(request.headers)
if request.method == 'GET':
print(form.csrf_token._value())
return ('', {'csrf_token': form.csrf_token._value()})
elif request.method == 'POST':
req = request.get_json(force=True)
form = RegistrationForm.from_json(req)
print(form.username)
return{"message": "posted"}
else:
return { 'errors': form.errors }
The idea behind this was to send the csrf_token as with other cases, due to the fact that in the prior link I read:
Generate a CSRF token. The token is cached for a request, so multiple calls to this function will generate the same token.
time_limit -- Number of seconds that the token is valid. Default is WTF_CSRF_TIME_LIMIT or 3600 seconds (60 minutes).
After reading this I logically thought that this would make sense then that I could essentially get the csrf_token from a standard form object, as you would normally operate with them and then when the request method is POST I could rerender the form and insert the data from the JSON within the request, I got the idea from https://wtforms-json.readthedocs.io/en/latest/
This however is met with the same behavior of not being able to access the data in the form object receiving multiple errors from:
print(form.username.data)
AttributeError: 'tuple' object has no attribute 'data'
print(form.username)
(<UnboundField(StringField, ('username',), {'validators': [<wtforms.validators.DataRequired object at 0x000002A3B600A6D0>]})>,)
Thus I couldn't even access the data I was inserting into the form object. (If it even inserted as I assummed it would.)
Prior to this:
I tried the standard way of operating with wtforms:
@bp.route('/api/register_user', methods=['POST', 'GET'])
def register():
""" End-point to register users """
form = RegistrationForm()
print(request.headers)
if request.method == 'GET':
print(form.csrf_token._value())
return ('', {'csrf_token': form.csrf_token._value()})
elif form.validate_on_submit():
req = request.get_json(force=True)
return{"message": "posted"}
else:
return { 'errors': form.errors }
Now it never got past validation, which is strange because the validators are simply DataRequired(). However it did get past the form.is_submitted() when tested as such, but I got the same errors when printing:
print(form.username.data)
AttributeError: 'tuple' object has no attribute 'data'
print(form.username)
(<UnboundField(StringField, ('username',), {'validators': [<wtforms.validators.DataRequired object at 0x000002A3B600A6D0>]})>,)
Also with this the react code had to change slightly in order to work, or else it send back bad request the request had to be:
handleSubmit = (details) => {
const finalSend = {
'csrf_token': this.state.csrf_token,
'username': this.state.usernameInput,
'fullname': this.state.fullnameInput,
'phone': this.state.phoneInput,
'email': this.state.emailInput,
'password': this.state.passwordInput,
'repeatPassword': this.state.repeatPasswordInput
}
axios({
method: 'post',
url: '/api/register_user',
data: finalSend,
headers: {
'content-type': 'application/json'
}
})
.then(res => res.json()).catch(e => console.log(e));
}
The Question
So after all this what I am basically asking is:
- How can I get wtforms to take the data sent as a JSON object?
- Would it be better to operate separately from WTForms, if so how?
- If I do the above how can I set up CSRF security, as I tried recently, and it isn't working as needed.
Extra Needed:
The JSON that is sent by the component is in the form of:
{'csrf_token': 'ImQ5MjhlY2VlYzM5Zjg0NmY4ZTg0NDk5ZjNlMjlkNzVlZGM4OGZhY2Ui.YBPbuw.D6BW8XpwEyXySCaBBeS0jIKYabU', 'username': 'charlie', 'fullname': 'charlie char', 'phone': '344444444', 'email': '[email protected]', 'password': 'charlieandcindy', 'repeatPassword': 'charlieandcindy'}
The WTForm that I am using:
class RegistrationForm(FlaskForm):
username = StringField('username', validators=[DataRequired()]),
fullname = StringField('fullname', validators=[DataRequired()]),
email = BooleanField('email', validators=[DataRequired(), Email()]),
phone = StringField('phone', validators=[DataRequired()]),
password = StringField('password', validators=[DataRequired()]),
repeatPassword = StringField('repeatPassword', validators=[DataRequired(), EqualTo('password')])
def validate_username(self, username):
print("validating username")
I have also consulted the following websites looking for a solution: Flask wtforms - 'UnboundField' object is not callable, dynamic field won't init properly https://wtforms.readthedocs.io/en/2.3.x/forms/ https://wtforms-json.readthedocs.io/en/latest/ Flask-WTF set time limit on CSRF token https://flask-wtf.readthedocs.io/en/stable/api.html#flask_wtf.csrf.generate_csrf How do I get the information from a meta tag with JavaScript? https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#uploading_json_data https://flask-wtf.readthedocs.io/en/stable/csrf.html#javascript-requests CSRF Protection with Flask/WTForms and React
Once more I have tried my best finding the solution and put a lot of effort into writing a good question, if you need any more just ask.
EDIT
Working on this some more I have played around with wtforms_json some more and have got it to poulate slightly, however it only populates the repeatPassword field, which I am unsure why.
In order to do this I have had to turn off csrf validation and using the following code in the route:
@bp.route('/api/register_user', methods=['POST'])
def register():
""" End-point to register users """
json = request.get_json()
form = RegistrationForm.from_json(json)
print(request.get_json())
print(form.data)
if form.validate():
print("validated")
return{"message": "posted"}
else:
return { 'errors': form.errors }
The result when printing form.data is coming with the repeatPassword being set but nothing else and not sure why... source to this advancement is here WTForms-JSON not working with FormFields
I found the answer too this.
In order to do this I ended up using the wtforms_json from json methodas below:
As well as had to adjust the forms:
The only downside to this is I had to turn off the csrf_token within the config with: