×
By the end of this chapter, you should be able to:
So far we've been building our forms with some standard HTML, but unfortunately we have been neglecting any kind of validation!
Form validation is very common and quite important. When you ask for someone's phone number, you want to ensure that what they type represents a valid phone number. Same thing goes for email addresses, credit card numbers, and so on.
There are two ways to verify form information: either on the client, or on the server. Client-side validation is definitely helpful; there are even a lot of validation tools available out of the box in HTML5. (To read more about this, check out MDN.)
However, it is essential to validate your inputs on the server side. Validation on the client side is strictly for a better user experience and can easily be bypassed. If you only validate on the client side, you are opening yourself up to all kinds of malicious attacks. Without server side validation, someone could make themselves an admin of your site, try to delete other users, and so on.
To make server side validation a breeze, we can use the Flask WTF module.
Let's create a new Flask project and explore how WTForms works with Flask.
mkvirtualenv flask-forms pip install flask flask-wtf touch {forms,app}.py
Inside of the forms.py
let's create our first form.
from flask_wtf import FlaskForm from wtforms import BooleanField, StringField, PasswordField, IntegerField, validators class SignupForm(FlaskForm): name = StringField('Name', [validators.Length(min=1)]) email = StringField('E-mail', [validators.Length(min=6, max=35)]) favorite_number = IntegerField('Favorite Number') password = PasswordField('Password', [ validators.DataRequired(), validators.EqualTo('confirm', message='Passwords must match') ]) confirm = PasswordField('Repeat Password') newsletter = BooleanField('Sign me up for your newsletter!')
Let's now create a little Flask app that makes use of this form. We'll only have two pages: a sign up page, and a welcome page for people who have signed up. Here's the app.py
:
from flask import Flask, request, redirect, url_for, render_template, flash from forms import SignupForm app = Flask(__name__) @app.route('/signup', methods=['GET','POST']) def signup(): # request.form will be empty on a GET request, but populated on a POST request since it will contain values that a user has entered form = SignupForm(request.form) # let's first see if it is a post request if request.method == 'POST': # now we can use the WTForms validate method to see if we have passed our validations for the form we created in forms.py. This method returns True or False if form.validate(): # we will cover flash messages more, but they are a one time message that is displayed to the user of our application flash("You have succesfully signed up!") return redirect(url_for('welcome')) # if the method is a GET - or if form.validate() returns False, our form will contain a dictionary called errors which contain the error messages to display to the user return render_template('signup.html', form=form) @app.route('/welcome') def welcome(): return render_template('welcome.html')
Notice that we're setting a flash message if the form is validated. We'll talk more about flash messages later on; for now, you can think of them as short messages to display to the user based on some action.
Next we need to write some HTML.
Create a templates
folder, and inside of it let's create three files: base.html
, signup.html
, and welcome.html
. The base
should look quite familiar:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Form Introduction</title> </head> <body> {% block content %} {% endblock %} </body> </html>
Next, we need to learn how to display our form in our view. Notice that in our app.py
we are passing our form into the render_template
function so that we have access to a form
variable in the view. Here's what that might look like in our signup.html
:
{% extends 'base.html' %} {% block content %} <form method="POST" action="{{url_for('signup')}}"> {% for field in form %} <p> {{ field.label }} {{ field }} <span> {% if field.errors %} {% for error in field.errors %} {{ error }} {% endfor %} {% endif %} </span> </p> {% endfor %} <button type="submit">Sign up!</button> </form> {% endblock %}
Finally, let's add to our base.html
, which includes some logic around how to deal with the flash message set by our server:
{% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} <p>{{ message }}</p> {% endfor %} {% endif %} {% endwith %}
And now inside of our welcome.html
, we can simply place the following and still see our flash messages. Make sure to put the above logic in your base.html
since you'll want your flash messages to appear on any page that they are sent.
{% extends 'base.html' %} <h1>Welcome to the app!</h1> {% block content %} {% endblock %}
At this point, you can go to localhost:3000/welcome
to see the welcome page. However, if you try to access /signup
you'll get an error:KeyError: 'A secret key is required to use CSRF.'
What does this mean, and how do we resolve the error? To answer this question, we need to understand an important concept in web security: CSRF.
One of the most common web attacks is Cross Site Request Forgery, or CSRF. In this attack, the hacker creates a form that looks real, and when users enter their credentials, the hacker captures the values in order to make malicious requests on that user's behalf. Before you continue, make sure to watch this video to really develop an understanding of why this is such a real problem.
The idea here is that once a user is authenticated, a cookie is set in the browser to remember that the user has logged in. While this is great, there's a real security issue here.
Imagine you've logged into to some website (say it's your local bank). Once you're logged in, a cookie has been sent to your browser from the bank's website and when you visit the website, you don't have to log in. So everything is going well until you head over to another website which has been made by someone malicious.
The second that you enter that site, a POST request is sent (via AJAX) to the bank's website to transfer some money to another account. If the bank's website is insecure, it will simply think that this is just the user who logged in, that has decided to transfer some money to another account! This is pretty terrifying as you can trigger CSRF attacks without the user even having to click or submit anything!
In order to prevent CSRF attacks, a token is usually sent from the server when the form is rendered and if that token is not sent back to the server from the form when it is submitted, a 422 (unprocessable entity) error will be thrown.
One of the benefits of Flask-WTF is that forms are enabled with CSRF protection by default. However, in order to generate the token, we need to set a secret key on the server.
For some examples of CSRF, check out this article.
In order to create a CSRF token, we need to first have a "secret key" on the server. This "secret key" is simply a string that is used to encrypt data that is stored on the server (we call that the session). This secret key is used to send and decrypt the CSRF token to make sure it is valid. This secret key can be configured on your server like this.
app.config['SECRET_KEY'] = 'any string works here'
But this is bad as well! If someone gets access to our server, they can see this key and decrypt tokens on their own! Ideally we need some way of hiding this variable. We do this using environment variables
.
When using virtualenv
we have access to a hook called postactivate
, which runs after the environment is created. Inside of here we can export
variables for our environment. So let's run code $VIRTUAL_ENV/bin/postactivate
and we should see a file where we can export values. Now let's add export SECRET_KEY=shhhh
and save the file, and now we can change our app.py
to this:
import os app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
You might also need to deactivate
your vitual environment and workon
it again, as it won't be aware of changes to the postactivate
file when you're already working on it.
Now if someone gets access to our server, they still won't be able to see what our secret key is!
At this point, you should be able to sign up successfully. And when you do, you should see the flash message on the welcome page!
For more on CSRF and Flask, check out the docs.
Finally, you may notice that when you iterate over the fields in the form, the hidden input with the CSRF token is included, you'll want to ignore that input when you're iterating. Here's how we could do that with a little bit of conditional logic!
{% extends 'base.html' %} {% block content %} <form method="POST" action="{{url_for('signup')}}"> {{form.hidden_tag()}} {% for field in form if field.widget.input_type != 'hidden'%} <p> {{ field.label }} {{ field }} <span> {% if field.errors %} {% for error in field.errors %} {{ error }} {% endfor %} {% endif %} </span> </p> {% endfor %} <button type="submit">Sign up!</button> </form> {% endblock %}
In the example above, we are working with a pretty simple example, but when we add SQLAlchemy in the mix, we have to be very mindful about one thing. The variables you create in your forms.py should always correspond to the columns in your database - what do we mean by that?
Let's imagine we had the following model:
class Author(db.Model): __tablename__ = 'authors' id = db.Column(db.Integer, primary_key=True) first_name = db.Column(db.Text) last_name = db.Column(db.Text) def __init__(self, first_name, last_name) self.first_name = first_name self.last_name = last_name
If we wanted to build a form that we could use for creating or editing an author, here's what that must look like!
class AuthorForm(FlaskForm): # notice that these variables first_name and last_name are the exact same as in our model above # this will make getting data from the form much simpler so try to always make those match up. first_name = StringField('First Name', [validators.DataRequired()]) last_name = StringField('Last Name', [validators.DataRequired()])
Very commonly you'll want to pass in exisiting values you can use a keyword argument of obj
and pass an object for prepopulated values. You can read more about that here.
That might look something like this (imagine we are working with authors as our resource):
@app.route('/authors/<int:id>/edit') def edit(id): found_author = Author.query.get(id) form = AuthorForm(obj=found_author) return render_template('edit.html', form=form)
Hopefully you're now starting to see why CSRF is such an issue and when we edit or create we can pass our form to a Jinja template and use form.hidden_tag()
or form.csrf_token
(form.hidden_tag()
is more commonly used) to display the token - and that's great!
But there's one more kind of form that we are not securing. Remember that when you delete a resource, we still need a form, which means we MUST have CSRF protection for that form.
So how do we add a CSRF token to our forms when deleting? There are two options.
So which one should you use? The short answer is that step 1 will be much faster, but we will show both.
In order to pass form.hidden_tag()
to our template, we need an instance of a form. So in our forms.py
we would add
class DeleteForm(FlaskForm): # since we do not have any fields in our form, we will just pass here # we are only creating this class so we can inherit from FlaskForm and get built-in CSRF protection pass
In our app.py
we would then have something like this
@app.route('/authors/<int:id>/', methods=["GET, DELETE", "PATCH"]) def show(id): found_author = Author.query.get(id) if request.method == b'PATCH': # notice for editing/creating we use a different form! form = AuthorForm(request.form) if form.validate(): # normal edit logic found_author.first_name = form.data.first_name found_author.last_name = form.data.last_name db.session.add(found_author) db.session.commit() flash('Edited Successfully!') return redirect(url_for('index')) else: # if we fail to edit, show the edit page again with error messages and values that the user has typed in! return render_template('edit.html', form=form) if request.method == b'DELETE': # even though our delete form just has a button, there still is some information in request.form - the CSRF token! delete_form = DeleteForm(request.form) # just make sure they did not tamper with the CSRF token or do something malicious if delete_form.validate(): # now that CSRF has been validated, go ahead and delete then redirect back! db.session.delete(found_author) db.session.commit() flash('Author Deleted!') return redirect(url_for('index')) # if they did, or if it is a GET request, just render the show page return render_template('show.html', delete_form=delete_form, found_author=found_author)
Finally, here is what our show.html
page might look like
{% extends 'base.html' %} {% block content %} <h1>Hi {{found_author.first_name}} {{found_author.first_name }}</h1> <form action="url_for('show')?_method=DELETE" method="POST"> {{delete_form.hidden_tag()}} <input type="submit" value="X"> </form> {% endblock %}
And now we have CSRF protection for our DELETE form! This might seem like a lot to add, but it is essential for securing your application.
Instead of creating an empty form, we can take a longer approach by first ensuring that our application includes CSRF protection (something we normally get by inheriting from FlaskForm).
In our app.py
make sure to include this import and then create an instance of the class with our app
variable.
from flask_wtf.csrf import CSRFProtect # add csrf protection without creating a FlaskForm (for deleting) app = Flask(__name__) csrf = CSRFProtect(app)
What this gives us is something we can add to our Jinja templates called csrf_token
. Here's what our show.html
page might look like:
{% extends 'base.html' %} {% block content %} <h1>Hi {{found_author.first_name}} {{found_author.first_name }}</h1> <form action="url_for('show')?_method=DELETE" method="POST"> <input type="hidden" name="csrf_token" value="{{ csrf_token()}}"/> <input type="submit" value="X"> </form> {% endblock %}
So far this seems good! However, we need to add quite a bit of validation in our app.py
- here is what a route for /users/<int:id>
might look like:
# add the top of your code make sure you include this now # validate_csrf is a function used to make sure the CSRF token is authentic # ValidationError is what is raised if the CSRF validation fails from flask_wtf.csrf import validate_csrf, ValidationError @app.route('/authors/<int:id>/', methods=["GET, DELETE", "PATCH"]) def show(id): found_author = Author.query.get(id) if request.method == b'PATCH': # notice for editing/creating we use a different form! form = AuthorForm(request.form) if form.validate(): # normal edit logic found_author.first_name = form.data.first_name found_author.last_name = form.data.last_name db.session.add(found_author) db.session.commit() flash('Edited Successfully!') return redirect(url_for('index')) else: # if we fail to edit, show the edit page again with error messages and values that the user has typed in! return render_template('edit.html', form=form) if request.method == b"DELETE": try: # validate_csrf will raise an error if the token does not match so we need to catch it using try/except validate_csrf(request.form.get('csrf_token')) db.session.delete(found_author) db.session.commit() return redirect(url_for('owners.index')) except ValidationError: # if someome tampers with the CSRF token when we delete an owner return render_template('owners/show.html', found_author=found_author) return render_template('owners/show.html', found_author=found_author)
In the code above, we are trying to run our validate_csrf method which either raises an error if validation fails or returns None
if the validation is successful. If it returns None
we delete the author - otherwise we catch the error and render the show page again.
Another common security risk when building forms is form hijacking. Let's imagine we have the following form:
<form action="/users" method="POST"> <input type="text" name="username"> <input type="submit" value="Create User"> </form>
This form seems pretty harmless, but what happens if someone opens the chrome dev tools and edits the HTML to look like this?
<form action="/users" method="POST"> <input type="text" name="username"> <input type="text" name="is_admin" value="true"> <input type="submit" value="Create User"> </form>
If that user submitted the form, what would happen if you actually had a column in your database called "is_admin"? A user could hypothetically create an account as an admin! Even though this may not be likely, it's incredibly dangerous. We need to make sure users can not hijack forms! On the server, it is very important to not just accept ALL parameters that a user can enter in a form, but be specific with the parameters that we want.
Flask-WTF fixes this problem, because the only fields it pays attention to are attributes in the form class that you define. (In the example above, this means name
, email
, favorite_number
, password
, confirm
, and newsletter
.) If you try to create any other inputs on the client side, they'll simply be ignored.
As you've seen, WTForms has quite a few fields you can use when building your forms:
IntegerField() StringField() TextInput() PasswordField() # input wiht type password BooleanField() # checkbox FormField(AnotherFormClass) # for nesting forms SelectField(choices=[('aim', 'AIM'), ('msn', 'MSN')])
Here is what a sample form for resetting a password could look like:
class ChangePassword(Form): password = PasswordField('New Password', [InputRequired(), EqualTo('confirm', message='Passwords must match')]) confirm = PasswordField('Repeat Password')
You can learn more about these here
Each of your fields can contain a list of validators. Here are some common ones:
DataRequired(message='Custom Message Here') # require that something exists in the input Email(message='Custom Message Here') # validate an email EqualTo(fieldname, message='Custom Message Here') # make sure values are equal
You can learn more about these, including validating a range or URL, here. You can also write your own custom validators for re-use:
def my_length_check(form, field): if len(field.data) > 50: raise ValidationError('Field must be less than 50 characters') class MyForm(Form): name = StringField('Name', [InputRequired(), my_length_check])
If you'd like to see an example of adding WTForms to a Flask application, feel free to watch the screencast below.
Complete the WTForms exercise.
When you're ready, move on to Deployment with Heroku