DEV Community

Bruno Oliveira
Bruno Oliveira

Posted on

Flask series part 10: Allowing users to register and login

Introduction

On the last post in the series, we improved the UI/UX by adding a top navigation bar, integrated with a login form.

Now, it's time to capitalize on this recent UI feature and actually expand our application to allow users to register and login into our app.

End-goal of this feature

When users will be able to register and login, we can expand some views in our application that we will only make available to logged in users, namely:

  1. An "Add your recipe" view, where users can add their own personal recipe to the website.

  2. A "Dashboard" view, where users can see the details of the recipes they've added, and update or delete them via a table view.

Additionally, obviously, only logged in users will be able to logout.

This feature will explore lots of Flask extra features, more integrations with external APIs and how to customize Jinja templates to show information about the current user.

Preparing the registration form

In order to allow users to register and login into our application, we will need to create views on the front-end, where users can be redirected to, to perform the corresponding actions.
Then, we'll need to write the corresponding endpoints on the server-side, so that:

  1. For the registration endpoint:

    • A new user registry is rejected in case the user exists;
    • After the form is deemed as valid, a new user is created and added to the database, with the credentials entered in the form and with a special extra attribute, the is_active status, which we will set to false by default;
    • Afterwards, once the registration is completed, user gets redirected to the homepage, and, a flashing message will be shown as a popup, informing the user that an email has been sent. Then:
      • The user will receive an email where he can confirm his account, and upon confirmation, the active status will be set to true and the user will be logged in.
  2. For the activation endpoint:

    • Activation in the case of our app will simply mean to set the active status of a user which clicked on the email link to true, and we will be using that information in order to update the front-end templates with the currently logged in user.
  3. For the login endpoint:

    • Login fails in case the credentials are incorrect or incomplete, on which case we redirect back to the login/main page;
    • If the credentials match, we will set the current user to the one with matching credentials and login him/her to the app.

Application security: always use https and hash your database passwords

Before we delve in to all the code for this new feature, it's important to talk about application security and database security.

Using https versus http

You should always protect all of your websites with https, even if they don’t handle sensitive communications. Aside from providing critical security and data integrity for both your websites and your users' personal information, https is a requirement for many new browser features, particularly those required for progressive web apps. So, it's now being adopted as a modern web standard, and for good reasons:

  • https helps prevent intruders from tampering with the communications between your websites and your users’ browsers. Intruders include intentionally malicious attackers, and legitimate but intrusive companies, such as ISPs or hotels that inject ads into pages.

  • https prevents intruders from being able to passively listen to communications between your websites and your users.

  • https is a key component to the permission workflows for new features and updated APIs for Progressive Web Apps: taking pictures or recording audio with getUserMedia(), enabling offline app experiences with service workers, etc.
    Many older APIs are also being updated to require permission to execute, such as the geolocation API.

In order to use https in Heroku, we need to forcefully configure it via our Flask server code. The idea is to execute a function before each request that will force a permanent redirect of all requests (via the http code 301) that don't have https in its header, after replacing http with https.
The Flask annotation @app.before_request is used to make sure that this function gets called before each request. The code is as follows:

@app.before_request
def enforce_https_in_heroku():
    if request.headers.get('X-Forwarded-Proto') == 'http':
        url = request.url.replace('http://', 'https://', 1)
        code = 301
        return redirect(url, code=code)
Enter fullscreen mode Exit fullscreen mode

The request headers get inspected for the existence of http on which case the headers are replaced, and a redirect to the original (but now secured) URL occurs with code 301.
Since this function is called before each request, all of our communications will be secured by https. We just made our app a little bit more secure.

Hashing database passwords

As seen above, using https is important to prevent intruders from tampering with communications between the website and the users' browser.
However, what if an attacker gets hold of our database by any means? In this scenario, the usage of https is not relevant for protection purposes, because the database sits in the back-end, so, it means that the attackers directly got hold of all the data in the database.
Unfortunately, even nowadays, it's very common for lots of websites and web applications that need databases, to store passwords in plaintext.

What this means is that, if I'd register in a website with credentials:
User: bruno
password: bruno123
these would be the exact values saved in the database. This is a huge security risk from the point of view of a database compromise. Any attacker that got hold of these data, could easily login as being me, request password changes, or change my credentials at will, not to mention, gain access to sensitive information. Many shops today still do this, and, it's a terrible mistake:

Key takeaway: never store your passwords in a database in plaintext. Use hashing at all times.

The trick to prevent against this type of attack, is to use hashing or any other form of one-way encryption. With one way encryption, it means it's very easy to encrypt a certain value, but, it is very hard (read: "impossible") to decrypt the original value from the encrypted one.

Like this, instead of storing plaintext passwords, we can hash them and store the value of the hashed password on the database:
User: bruno
pass: make_hash("bruno123")=<some random string of characters representing the hashed value>
So, if any malicious user gets hold of our database, he won't be able to use the information he retrieved for any bad purposes. It's easy to do and it adds a huge amount of security to the app.
To do it, we will use the following two functions:

def hash_password(password):
    """Hash a password for storing."""
    salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii')
    pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'),
                                  salt, 100000)
    pwdhash = binascii.hexlify(pwdhash)
    return (salt + pwdhash).decode('ascii')
Enter fullscreen mode Exit fullscreen mode

This function hashes a password with salt. Salt is random data that is used as an additional input to a one-way function that "hashes" data, a password or passphrase. Salts are used to safeguard passwords in storage.
We then compute an hash for the password and add the salt to it, and we get our hashed password, completely protected from an attacker.

Then, we need to compare the stored password with the password a user provides (for example, via a login form. Here https matters) and in order to do so, we will use this function:

def verify_password(stored_password, provided_password):
    """Verify a stored password against one provided by user"""
    salt = stored_password[:64]
    stored_password = stored_password[64:]
    pwdhash = hashlib.pbkdf2_hmac('sha512',
                                  provided_password.encode('utf-8'),
                                  salt.encode('ascii'),
                                  100000)
    pwdhash = binascii.hexlify(pwdhash).decode('ascii')
    return pwdhash == stored_password
Enter fullscreen mode Exit fullscreen mode

Here, we retrieve the value of the salt, as well as the stored password, we use the salt to hash the password the user entered and then we compare the hashed password the user entered with the one we retrieved, and only if they match, do the passwords match.
Like this, we have guaranteed our security at a database level as well as during traffic between the client and server.
Now that we've handled security, we are ready to move forward and finally implement our registration form.

How the form will look

This is how our simple registration form will look:

registry form

We have three fields for a user: name, email and password.

The email will be used for authentication purposes, since: it's unique and there can be multiple people with the same name, but different emails, and each email will be unique per registered user.
So, if we see an email already in the database, the registry will be denied.

As always, we build our Jinja template:

<!DOCTYPE html>
<html lang="en">

{% extends "base_template.html" %}

{% block body %}
    <body>
    <h1>Create an account</h1>
    <form action="/userRegistry" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.name.label }}<br>
            {{ form.name(size=32) }}
        </p>
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=32) }}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
    </body>
{% endblock %}
</html>
Enter fullscreen mode Exit fullscreen mode

The template is simple and straightforward: we extend from our base template that we introduced in the previous installment, and simply create a form with the necessary fields that we are going to need from the user to do the registration.

On submission, we see that the form will redirect the entered data to the /userRegistry endpoint, where the registration will take place. This is what we will see how to implement next.

Implementing the user registry endpoint

Now that we have the front-end in place, we can focus on implementing the /userRegistry endpoint.

Here's the code:

@db_session
@app.route('/userRegistry', methods=['GET', 'POST'])
def user_registry():
    form = UserRegistryForm()
    if form.validate_on_submit():
        if request.method == 'POST':
            email = request.form['email']
            password = hash_password(request.form['password'])
            name = request.form['name']
            exist = User.get(login=email)
            if exist:
                flash('The address %s is already in use, choose another one' % email)
                return redirect('/userRegistry')
            curr_user = User(login=email, username=name, password=password, is_active=False)
            commit()
            localhost_url = 'http://0.0.0.0:5000'
            message = Mail(
                from_email='<sender_email>',
                to_emails=To(email),
                subject='Confirm your account',
                html_content='<h2>Hello,<h2> to complete your registration click  <a href="' + (
                        os.environ.get("HEROKU_URL") or localhost_url) + '/activate/' + str(
                    curr_user.id) + '"> here </a>.'
            )
            try:
                sg = SendGridAPIClient(os.environ.get("SENDGRID_API_KEY"))
                response = sg.send(message)
            except Exception as e:
                print e.message
            return redirect('/')
    else:
        return render_template('registry_form.html', title='Register', form=form)
Enter fullscreen mode Exit fullscreen mode

There's a lot going on here, so let's go over it step by step.

Annotations

We had the @db_session annotation before the standard route annotation to indicate to Flask that we will do database manipulations within the endpoint. This is similar to the with db_session context local to a block, but this declares it for the whole endpoint and we can use db operations at will now.

Below this annotation, we have the standard Flask annotation to declare an endpoint and we will support both post and get methods.
The get method will be used when rendering the template, for when the user first accesses the view, no information is being posted to the server, so we use the standard get method to render the template.

Declaring a form to handle our information

In the line form = UserRegistryForm() we are declaring a form for handling the user information for registration.

UserRegistryForm is a Python class, that looks like this:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField
from wtforms.validators import DataRequired


class UserRegistryForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Register')
Enter fullscreen mode Exit fullscreen mode

We are using WTForms to create our user registration form.
This is an extension for Flask that makes handling forms with Flask a very easy task.

In WTForms, a form is represented by a standard Python class.
Forms are the core container of WTForms. Forms represent a collection of fields, which can be accessed on the form dictionary-style or attribute style.

Fields do most of the heavy lifting. Each field represents a data type and the field handles coercing form input to that datatype. For example, IntegerField and StringField represent two different data types. Fields contain a number of useful properties, such as a label, description, and a list of validation errors, in addition to the data the field contains.

Every field has a Widget instance. The widget’s job is rendering an HTML representation of that field. Widget instances can be specified for each field but every field has one by default which makes sense. Some fields are simply conveniences, for example TextAreaField is simply a StringField with the default widget being a TextArea.

In order to specify validation rules, fields contain a list of Validators. For our example, we just enforce that the fields are required to be filled in.

To actually use a form, we can simply instantiate it, and, thanks to the way the data binding to the form works, we can access it via the request object and the form attribute which is simply a dictionary containing all the information entered in the form:

email = request.form['email']

like this, we retrieve the email value from the form.

This has its corresponding value on the Jinja template:

<p>
            {{ form.email.label }}<br>
            {{ form.email(size=32) }}
        </p>
Enter fullscreen mode Exit fullscreen mode

this has a label and the actual value taken by the email field.

Handling user registration

The next step is to handle user registration. For that, we need a User class, which we can design ourselves:

class User(db.Entity, UserMixin):
    login = Required(str, unique=True)
    username = Required(str)
    password = Required(str)
    is_active = Required(bool)
    recipes = Set("UserCreatedRecipe")
Enter fullscreen mode Exit fullscreen mode

As we can see, we have a login field, which will be the email of the user, as it's unique, we have the standard fields, username and password, an is_active status and a recipes field (which we will delve into later).

For now, the most important feature of our User class is the fact that it inherits from the UserMixin class.

In Python, this is how you use class inheritance: you declare a super class as an attribute of your subclass directly. Then our User class has access to all methods and properties of the UserMixin class.

This class is a special class from the flask-login extension, that contains some methods that our user class needs to have, so, by inheriting from it, we get access to lots of functionality that we are going to need in order to manage our users.

With this class defined, we can now add the logic for the actual handling of:

  • Checking if a user exists;
  • Adding a new user to the database;

The code for this is:

exist = User.get(login=email)
            if exist:
                flash('The address %s is already in use, choose another one' % email)
                return redirect('/userRegistry')
            curr_user = User(login=email, username=name, password=password, is_active=False)
            commit()
Enter fullscreen mode Exit fullscreen mode

The first line uses the Pony ORM to attempt a retrieval of a User by it's id (which is the email on our case). If we are successful, it means that a user registered with this email already exists, on which case we flash a message to the end-user that shows this information.

A quick introduction to flash messages with Flask

A flashing message is like a pop-up message that gets displayed on top of the screen to showcase some important information, usually in the form of user feedback.

To incorporate these flashing messages in the UI, we need to make changes to our Jinja template:

<div>
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                {% for category, message in messages %}
                    <div id="alertMsg" class="alert alert-{{ category }}" role="alert"> {{ message }}</div>
                {% endfor %}
            {% endif %}
        {% endwith %}
        <h3 id="title">
            Type ingredients
        </h3>
    </div>
Enter fullscreen mode Exit fullscreen mode

The built-in Flask method, get_flashed_messages works by pulling all flashed messages from the session and returning them.
Further calls in the same request to the function will return the same messages. By default just the messages are returned, but when with_categories is set to True, the return value will be a list of tuples in the form (category, message) instead.

Filter the flashed messages to one or more categories by providing those
categories in category_filter. This allows rendering categories in
separate html blocks. The with_categories and category_filter
arguments are distinct:

  • with_categories controls whether categories are returned with message text (True gives a tuple, where False gives just the message text).
  • category_filter filters the messages down to only those matching the provided categories.

Handling user registration (cont.)

After the quick introduction to how to work with flashing messages, we can proceed with our analysis of the code.

So, after we add the flashing message, we redirect the user back to the same /userRegistry endpoint, which will perform a new GET request, which will result in the form being cleared so the user can register with the correct/new email address.

Once the registration is successful, we write the new user to the database:

curr_user = User(login=email, username=name, password=password, is_active=False)
commit()
Enter fullscreen mode Exit fullscreen mode

Note how we set the is_active flag to false. This will indicate that the user hasn't yet confirmed his/her registration by clicking on the link which will be sent to the email used for registration.
Until the is_active flag is set to true, the user will not be able to login and a flashing message will be displayed indicating that the account hasn't been confirmed yet.

This allows us to move forward with our implementation of this large feature, where we will add capabilities to our app of sending an email to users as to enabling its account.

Integrating with the SendGrid API - sending emails from our app

There are many APIs for working with transactional email, MailGun and SendGrid are two of the most popular ones, and their free tier plans are quite suitable for both development as well as deployment of small-scale web applications.

In order to integrate with any API, we should start by reading their documentation. Usually, API providers will offer several ways of sending emails: you can use cURL directly to send it/test it from the command line, and, depending on your programming language or environment, there will be specific instructions to follow to integrate it with your existing application code.

After creating an account with SendGrid, we can go to their setup guide, under our username, as a dropdown, and we will see this:

Sendgrid dashboard

So, if we follow the instructions for Python, we find this snippet:

# using SendGrid's Python Library
# https://github.com/sendgrid/sendgrid-python
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

message = Mail(
    from_email='from_email@example.com',
    to_emails='to@example.com',
    subject='Sending with Twilio SendGrid is Fun',
    html_content='<strong>and easy to do anywhere, even with Python</strong>')
try:
    sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
    response = sg.send(message)
    print(response.status_code)
    print(response.body)
    print(response.headers)
except Exception as e:
    print(e.message)
Enter fullscreen mode Exit fullscreen mode

So, by adding this example snippet, we can add the functionality of sending emails to our users via Python for the SendGrid API. (please, note that the API can and will undergo changes over time, so syntax might differ.)

Noteworthy here is the usage of os.environ.get('SENDGRID_API_KEY') that encapsulates the API key in a way that's referenced by the system where the code is being executed. In this way, your credentials are safely stored and can't be seen by anyone reading the code.

Confirming user accounts and logging in the user

After the user receives and clicks on the link in the email, the idea is to redirect the code flow to a new endpoint, where the user will be logged in and redirected to the homepage:

@app.route('/activate/<id>', methods=['GET', 'POST'])
def activate(id):
    with db_session:
        user = User.get(id=id)
        if user:
            user.is_active = True
            commit()
            print "Logged in"
            login_user(user)
            return redirect('/')

Enter fullscreen mode Exit fullscreen mode

We simply retrieve the user by id, and, if it exists, we set its status to active and log the user in.

Once the user is logged in, the UI on the front-end will look as follows:

loggedin

So, we greet the logged in user by using its name, so, the same name that was used in the registration form, as it is more user friendly to display the name instead of the email.

Let's inspect the Jinja template code to see what needs to change to add this functionality to the UI:

<div class="topnav">
    <a class="active" href="/">Home</a>
    <a href="userRegistry"> Register </a>
    {% if not (current_user.is_authenticated and current_user.is_active)%}
    <div class="login-container">
        <form method="POST" action="login">
            <input type="text" placeholder="Email" name="email">
            <input type="password" placeholder="Password" name="password">
            <button type="submit">Login</button>
        </form>
    </div>
    {% else %}
    <div class="dropdown">
        <button onclick="myFunction()" class="dropbtn">  Hi, {{current_user.username}}</button>
        <div id="myDropdown" class="dropdown-content">
            <a class="dropdown-item" href="dashboard">Dashboard</a>
            <a class="dropdown-item" href="userRecipe">Add recipe</a>
            <a onclick="fixNav()" class="dropdown-item" href="logout">Logout</a>
        </div>
    </div>
    {% endif %}
</div>
Enter fullscreen mode Exit fullscreen mode

So, we see that we are referencing the current user via the current_user object from flask-login that returns us the user for the current session.
We do multiple validations in order to validate the login process:

  • we see if the current_user is authenticated and simultaneously, we check for the validity of the is_active status and, only if these two conditions are valid, we will configure the Jinja template to display the name of the logged in user;
  • if one of those conditions is not valid, we keep showing the login page;

These validations will occur on a different endpoint: an endpoint for logging a user in. Currently, we were logged in directly after clicking on the link in the email, but, afterwards, on a normal application use case, we will need to login by typing in our credentials. This flow and its validation will take place in the login endpoint, which we will analyze now.

Logging in via the login endpoint

When using the app in a normal usage scenario, the user will have to login via the login form available on the topnav bar. In order to enable that, we need to submit the contents of the small login form component via a POST request to the server, where the login will be handled:

To recap, here is the form on the Jinja side:

<div class="login-container">
        <form method="POST" action="login">
            <input type="text" placeholder="Email" name="email">
            <input type="password" placeholder="Password" name="password">
            <button type="submit">Login</button>
        </form>
    </div>
Enter fullscreen mode Exit fullscreen mode

So, on submit, we send the form contents to the login endpoint, for which we have the following code:

@db_session
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        email = request.form['email']
        password = request.form['password']
        if email == '' or password == '':
            flash("Enter a name and password")
            return redirect('/')
        possible_user = User.get(login=email)
        if not possible_user:
            flash('Wrong username')
            return redirect('/')
        if verify_password(possible_user.password, password) and possible_user.is_active is True:
            print "Logged in"
            set_current_user(possible_user)
            login_user(possible_user)
            return redirect('/')
        flash('Wrong password or account not confirmed')
        return redirect('/')
    else:
        return render_template('base_template.html')
Enter fullscreen mode Exit fullscreen mode

Here, we do validation to ensure both fields are filled in in order to proceed with the login action after which we follow the same flow that has been showcased earlier:

  • We attempt to retrieve the user by the entered email on the login form, failing if we can't find it, by flashing an error message and redirecting to the home page;

  • If the user exists, we perform credentials validation via the verify_password and is_active flag, logging the user in if we succeed, or, again redirecting and flashing an error message on failure;

Conclusion

This concludes the implementation of the feature of registration and authentication of users in our application.
This covered a lot of new concepts, introduced us to new flask extensions and also showed us how some aspects of Python work in more detail, like class inheritance.
This will allow us to build up the remaining features of the application: a dashboard where a user can see its own added recipes in a table view and also build the components for adding new recipes to the app.

Top comments (1)

Collapse
 
sm0ke profile image
Sm0ke

Nice & simple. Thank you!