18 minutes read

We interact with a large number of websites every day. A kind of interaction where we need to submit information like our personal details for logging in to a bank or a choice on what TV series we prefer, HTML forms are the thing working behind the scenes.

For handling HTML forms there are two main problems, rendering them on the client side and consuming their content on the server side. Modern web frameworks solve this problem and each framework has its way. For Flask, Flask WTForm is the library that solves this problem and also provides value-added features to simplify development. In this topic, we will take a quick recap of Flask WTForm and then dive into some of the advanced problems it solves like CSRF handling, File uploads, and form field validations. Let's get started.

Flask-WTForms a recap

Let us say we want to capture user information on logging. An example form to do so is shown here.

A user login form

To model this form we would need a Flask WTForm-based Form class with fields reflecting the actual UI form fields. We use the form fields to create our form class and use appropriate form field classes like StringField, IntegerField, etc to model the data we will receive via the form.

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField

class LoginForm(FlaskForm):
    # Notice how labels are defined in flask form
    name = StringField(label="NAME")
    password = PasswordField(label="PASSWORD")
    confirm_password = PasswordField("CONFIRM_PASSWORD")

Now for the problem of rendering it to the UI, we will look at the Jinja template. In a form, there are mainly labels and input fields. The labels are picked from the form fields and the rendering of input fields is controlled by calling the field like a function, form.name()

<!-- we are using bootstrap classes form-group and form-control -->
<form method="POST" action="/">
    <div class="form-group">
            {{ form.name.label }}
            {{ form.name(class_="form-control", size=20) }}
    </div>

    <div class="form-group">
            {{ form.password.label }}
            {{ form.password(class_="form-control", size=20) }}
    </div>

    <div class="form-group">
            {{ form.confirm_password.label }}
            {{ form.confirm_password(class_="form-control", size=20) }}
    </div>
    <input type="submit" value="Go">
</form>

And finally, this is what the Flask app handler method looks like. When we have a GET request, we render the Jinja template with an instance of the LoginForm class. And when we have a POST request, we read the form.data content to display it on the server side.

from flask import Flask, request, render_template, url_for
app = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def handle_request():
    form = LoginForm()
    if request.method == "GET":
        return render_template("form.html", form=form)
    if request.method == "POST":
        print(form.data["name"])
        print(form.data["password"])
        print(form.data["confirm_password"])
        return redirect(url_for('handle_request'))

With this quick recap, we see how simple it is to create and use a Flask WTForm object on the client and server side. Additionally, Flask WTF has some amazing value add features, let us see them one by one.

CSRF

CSRF refers to an attack on the website where the attackers exploit the trust the website has in users. Let us say you are logged in as a user on a website like a bank or a movie-watching website. When you send requests to the website, the browser invisibly adds cookies and session information to that request, so that the site verifies you as an authenticated user.

In a CSRF attack, the attackers impersonate the user by first sending a malicious webpage to the user, and then making requests with all of the user authentication and cookies from the user environment. The attacker can modify the request URL parameters to execute unwanted actions like a fraudulent bank transaction, delete the user account from the respective website, or change the account password. To prevent this attack experts recommend using a cryptographically-strong token i.e. a CSRF token that can verify on the server side if the request is legitimate. Furthermore, the token is passed invisibly through HTML forms as submitting data on the website is mostly done by form.

Flask WTForm has built-in support for CSRF which is enabled by default. Let us understand it with an example.

This is how effectively the LoginForm class looks from the perspective of fields defined in the class, csrf_token is hidden, and it gets auto-defined.

class LoginForm(FlaskForm):
    # other fields like name, password and confirm_password
    # csrf_token = CSRFTokenField('csrf_token') # HIDDEN !!

The value for csrf_token is generated cryptographically using the following two parameters:

  1. SECRET_KEY: Flask Application Secret key ( Set as WTF_CSRF_SECRET_KEY in the Fllask app config)

  2. CSRF_FIELD_NAME: the default value csrf_token (Set as WTF_CSRF_FIELD_NAME in the Flask app config)

When we render a form in an HTML template, we should render the CSRF token in the template, (it stays hidden in the UI, but is visible in dev-tools). It can be rendered as {{ form.hidden_tag() }} as well.

<form method="POST" action="/">
   
       {{ form.csrf_token }}

    <div class="form-group">
        {{ form.name.label }}
        {{ form.name(class_="form-control", size=20) }}
    </div>
   <!-- Definition of other fields like name, password etc here -->
    <input type="submit" value="Go">
</form>

It is cached for requests so new requests will get the same csrf token. When we instantiate a form for GET requests, this token is generated. When it is posted with the form, a validation happens on the csrf token. The validation checks if the csrf token is present in session if it is matching with what was sent and if it is not expired. The expiry of a CSRF token is configurable, check WTF_CSRF_TIME_LIMIT in the official documentation.

If all the above criteria are satisfied the CSRF_TOKEN is deemed right and the server accepts the incoming request. Code-wise, CSRF validation can be triggered when calling the form.validate_on_submit() or explicitly we can call flask_wtf.csrf.validate_csrf (check git repo for csrf). For local testing, if you have to disable CSRF Validation you can set a variable in the Flask app as app.config["WTF_CSRF_ENABLED"] = False

File upload

HTML forms are also used to support uploading files to the server. Flask WTForm supports it. Let us say for our login scenario we want the user to be able to upload an identity proof for us to validate manually and let the user know later in case it does not match. We would save the file provided by the user to a server-side location, for manual verification later.

from flask_wtf import FlaskForm
from flask_wtf.file import FileField

class LoginForm(FlaskForm):
    # other form fields like name, password, confirm_password defined above
    file = FileField() # A new Field type for file upload

The only change in the Flask Form is a new field type FileField.

<html>
<head>
<title>Upload</title>
</head>
<body>
<form method="post" enctype="multipart/form-data">
    <!-- other fields like name, password, confirm_password defined here -->
    {{ form.file }}
    <input type="submit">
</form>
</body>
</html>

The points to note are as follows:

  • enctype = "multipart/form-data": this is used to specify encoding type for form, more info in The HTML DOM API

  • {{form.file}}: we don't put labels around form upload, it is only the attribute that we refer

When the right parameters are passed, the HTML form looks as follows in chrome dev tools:

<form method="post" enctype="multipart/form-data"> 
<!-- omitting other field definitions -->
    <input id="file" name="file" type="file">
    <input type="submit">
</form>

On the server side, this is how it looks.

@app.route('/', methods=['GET', 'POST'])
def handle_request():
    form = LoginForm()
    if request.method == "POST":
        # logic to handle other fields like name, password etc. is omitted here
        f = form.data["file"] # File uploaded
        f.save(path_to_save) # path_to_save is location for file to be saved
        return render_template('success.html', fname=f.filename) # Showing a success page to user
     # .... further code omitted below

In the backend, we get the file object in the server like any other form field. We save it locally to the server directory and the file upload is done.

Form field validations

Validators are classes defined on each field that enable checks on a field when it is being received at the server end. Let us understand it with an example, considering the following form class.

from wtforms import StringField, PasswordField
class LoginForm(FlaskForm):
    name = StringField('NAME')
    password = PasswordField('PASSWORD')
    confirm_password = PasswordField('CONFIRM_PASSWORD')

Now when this form is received on the server side, make sure that:

  • the name field is populated

  • password length is between 8-15 characters

  • password has at least one digit in it.

  • password and confirm_password have the same value

Now, we can have an implementation where we check all this in the server:

@app.router('/', methods=['GET', 'POST'])
def handle_form():
    form = LoginForm()
    if request.method == 'POST':
        name = form.data["name"]
        password = form.data["password"]
        confirm_password = form.data["confirm_password"]

        if len(name) > 0 and \
           8 <= len(password) <= 15 and \
           any(x in password for x in '0123456789') and \
           password == confirm_password:
            pass # Do form processing here

Now as the number of fields increases in the form and their logic for validation becomes complicated, we see that this is not a good way to proceed. Instead, we want to define these validations on a field and then run all of them in one go before doing any further processing.

Flask WTForm provides this feature as a group of built-in classes that can be added to form field definitions and also supports custom validation via additional code. A full list of built-in validator classes can be found in validator docs for Flask WTForm. The validations per field can be summarized as follows:

Validation per form field

Field name

Requirement

In-built Validator Class

Descriptions

name

the name field is populated

InputRequired

Validates that input was provided for this field

password

password length is between 8-15 characters

Length

Validates the length of a string

password

password has at least one digit in it

Regexp

Validates the field against a user-provided regexp

confirm_password

password and confirm_password have the same value

EqualTo

Compares the values of two fields

In general below is the syntax to define the validation of a field.

class MyFormClass:
    field1 = FieldType(label="field_label", 
                       validators=[ValidatorClass1(message="Failure in validation 1"), 
                                   ValidatorClass2(message="Failure in validation 2")])
    # example: field1 -> name, FieldType -> StringField, label -> "NAME"

With these changes, the LoginForm looks as follows.

from wtforms.validators import InputRequired, EqualTo, Regexp, Length


name_valid_msg = "No valid value provided for name variable"
password_len_msg = "Length of password should be at between 8 and 15"
password_valid_msg = "Password needs to have at least one digit"
confirm_password_msg = "The passwords do not match"

class LoginForm(FlaskForm):
    name = StringField('NAME', validators=[InputRequired(message=name_valid_msg)])
    
    password = PasswordField('PASSWORD', 
               validators=[Length(min=8, message=password_len_msg),
                             Regexp(regex=r'^.*\d+.*$',message=password_valid_msg)])
    
    confirm_password = PasswordField('CONFIRM_PASSWORD',
                       validators=[EqualTo("password", message=confirm_password_msg)])

And this is what the server looks like, we used form.validate_on_submit, which runs all validations for each field, reducing code overhead. The validate_on_submit method checks if the form is submitted and no validation on any field fails.

@app.route('/', methods=['GET', 'POST'])
def handle_form():
    form = LoginForm()
    if request.method == 'POST':
        if form.validate_on_submit():
            pass
        
    return render_template("form.html", form=form, msg='')

Now if the validations fail, like the password and confirm_password fields do not match, we would get the error The passwords do not match.

In the same example if we see, implementing the LoginForm password field, and adding the Regexp Validator we had to include a complicated regular expression r'^.*\d+.*$' . But this logic can be implemented more easily by supplying the verification logic ourselves. This is what a custom validator class does, let us see an example of it.

The custom validation can be implemented in two ways,

  • A class function in the LoginForm class: For a field called x, its validating method should be called validate_x by convention, and it should be a static method, accepting a form and a field argument. So for the password field, we will add the validate_password method.

  • A custom validator class: like built-in validator classes like InputRequired, we can create another custom validator class. The class has an __init__ method that accepts a message to display on validation failure and a __call__ method that is invoked for validation. It should return True if the validation succeeds or should raise a ValidationError in case it fails.

Let us first create a utility method that will validate the password field from a form:

def check_password(form, field, message):
    password = field.data
    if any(x in "0123456789" for x in password): return True
    raise ValidationError(message)

This is how the custom validation with class function on the field in form class would like:


class LoginForm(FlaskForm):
   
    # ... Field definitions here ....

    @staticmethod
    def validate_password(form, field):
        return check_password(form, field, 'Password failed validation, it must have 1 digit') 

And this is how the custom validator class would look like:

class CustomPasswordMatcher:
    def __init__(self, message):
        self.message = message

    def __call__(self, form, field):
        # returns True if validation succeeds else raises a ValidationError exception
        return check_password(form, field, self.message)

Finally bringing the custom validator class in the form would look as follows:

custom_msg = "Password failed validation, it must have 1 digit"
password_len_msg = "Length of password should be at between 8 and 15"

class LoginForm(FlaskForm):
    # ... other fields have definitions unchanged
    password = PasswordField('PASSWORD', validators=[
        Length(min=8, message=password_len_msg),
        # CUSTOM VALIDATOR CLASS defined in previous code snippet
        CustomPasswordMatcher(message=custom_msg) 
    ])

    

As seen, the check_password code is simple to follow. This is one way to custom-validate a field, we can add multiple validating functions for a field and call them one by one inside the validate_password. It is possible to add validations for many other scenarios.

Conclusion

In this topic, we saw some of the advanced use of Flask WTForm with validators, file upload, and CSRF. Flask WTForms is extremely extensible in terms of creating custom features like a custom field type or the custom rendering of a field. Also, Flask WTForm can be used for some other advanced checks like internationalization, translation, re-captcha, etc. A good form design should consider what form fields will be required in the UI and how can it be validated in the server most effectively, for which Flask WTForm provides all the tools of the trade.

6 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo