Lesson 4A

A Social Blogging Application — Part 1: User Authentication

Share Tweet

Contents

  1. Resources
  2. Part 1: User Authentication
    1. Authentication Extensions for Flask
    2. Password Security
    3. Create an Authentication Blueprint
    4. User Authentication with Flask-Login
    5. Preparing the User model for login
    6. Protecting routes
    7. Adding a Login Form
    8. Sign users in & out
    9. Running your application
    10. New User Registration
    11. Account Confirmation
  3. Exercises
  4. Further Reading

Resources


Part 1: User Authentication

Authentication is a basic process in order to authenticate a registered user based on his/her identification information which is either email address or username, and a password. For some apllications such as e-banking application, they also require an OTP. In this chapter, we will implement an authentication process for a social Blogging Application.

Authentication Extensions for Flask

To implement the authentication mechanism, in this chapter, we will employ the following new Python packages:

  • Flask-Login: Management of user sessions for logged-in users
  • Werkzeug: Password hashing and verification
  • Authlib: The ultimate Python library in building OAuth and OpenID Connect servers.

Password Security

The key to storing user passwords securely in a database relies on not storing the password itself but a hash of it. A password hashing function takes a password as input, adds a random component to it (the salt), and then applies several one-way cryptographic transformations to it. The result is a new sequence of characters that has no resemblance to the original password, and has no known way to be transformed back into the original password. Password hashes can be verified in place of the real passwords because hashing functions are repeatable: given the same inputs (the password and the salt), the result is always the same.

To generate a hashed password, and check a hashed password with a user's input, we can take advantage of the two functions which are implemented in Werkzeug's security module:

  • generate_password_hash(password, method='pbkdf2:sha256', salt_length=8) — This function takes a plain-text password and returns the password hash as a string that can be stored in the user database. The default values for method and salt_length are sufficient for most use cases.
  • check_password_hash(hash, password) — This function takes a password hash previously stored in the database and the password entered by the user. A return value of True indicates that the user password is correct.

Fig. 1 shows the changes to the User model created in Part 2: Databases to accommodate password hashing.

Fig 1. app/models.py: password hashing in the User model
Fig 1. app/models.py: password hashing in the User model

The method password(self, password) (aka. the setter method) will call Werkzeug's generate_password_hash() function and write the result to the password_hash field. In order to prevent outsiders from reading the password field, the function password(self) will generate and return an error message.

The verify_password() method takes a password and passes it to Werkzeug's check_password_hash() function for verification against the hashed version stored in the User model. If this method returns True, then the password is correct.

🛠 To evaluate the implemented hashed password function above, we can do either a manual test in Flask Shell or create a unit test. In the below figure, we present a tests/test_user_model.py test cases which placed in /tests directory.

Fig 2. tests/test_user_model.py: password hashing tests
Fig 2. tests/test_user_model.py: password hashing tests

To run these new unit tests, you execute the flask test command, and the result may look like below figure.

Fig 3. Sample results when running unit tests
Fig 3. Sample results when running unit tests
Create an Authentication Blueprint

Blueprints is a way to define routes in the global scope, you can refer to section Implementing Application Functionality in a Blueprint in Chapter 7. In this section, the routes related to the user authentication subsystem will be added to a second blueprint directory, named /auth. Using different blueprints for different subsystems of the application is a great way to keep the code neatly organized.

Inside /auth directory, there are two Python files, i.e., /auth/__init__.py and /auth/views.py. The blueprint's package constructor creates the blueprint object and imports routes from a views.py module. The /auth/views.py module imports the blueprint and defines the routes associated with authentication using its route decorator. For now, a /login route is added, which renders a placeholder template for an authentication form.

Fig 4. app/auth/__init__.py: authentication blueprint creation
Fig 4. app/auth/__init__.py: authentication blueprint creation
Fig 5. app/auth/views.py: authentication blueprint routes and view functions
Fig 5. app/auth/views.py: authentication blueprint routes and view functions

Note that the template file login.html is stored inside /app/templates/auth directory, and the two Python files are stored inside /app/auth directory.

The auth blueprint needs to be attached to the application in the create_app() factory function, as shown in below figure.

Fig 6. app/__init__.py: authentication blueprint registration
Fig 6. app/__init__.py: authentication blueprint registration
User Authentication with Flask-Login

When users log in to the application, their authenticated state has to be recorded in the user session, so that it is remembered as they navigate through different pages. Flask-Login is a small but extremely useful extension that specializes in managing this particular aspect of a user authentication system, without being tied to a specific authentication mechanism. The Flask-Login can be installed via pip command, as follows:

$ pip install flask-login

To attach Flask-Login into your application's authentication, you need to implement the following functions:

  1. Preparing the User model for login
  2. Protecting routes
  3. Adding a login form
  4. Signing users in & out

Preparing the User model for login

To enable the Flask-Login functionalities, we need to update the User model to include some properties and methods such as is_authenticated, is_active, is_anonymous, get_id() which are required by Flask-Login. To simply the implementation, Flask-Login provides a UserMixin class that has default implementations that are appropriate for most cases. We thus just inherit our User model from the UserMixin, as shown in Fig. 7.

Fig 7. app/models.py: Update the User model to support the Flask-Login extension
Fig 7. app/models.py: Update the User model to support the Flask-Login extension

We also have to initialize the Flask-Login in the application factory function, as shown in Fig. 8.

Fig 8. app/__init__.py: Flask-Login initialization
Fig 8. app/__init__.py: Flask-Login initialization [2]
Protecting routes

Flask-Login provides a very handy way to protect a route which can be accessed by authenticated users by include a login_required decorator. If a route implemented with login_required decorator is accessed by a user who is not authenticated, Flask-Login will intercept the request and redirect the user to the login page instead. Fig. 9 presents an example of how to declare the login_required decorator.

Fig 9. An example of login_required decorator [2]
Fig 9. An example of login_required decorator [2]
Adding a Login Form

The login form will be rendered on a web page when users visit the login page to authenticate their credential. The login form consists of a text field for email address, a password field, a "Remember Me" check box, and a submit button. In Fig. 10, we show the implementation of the login form.

Fig 10. app/auth/forms.py: login form
Fig 10. app/auth/forms.py: login form

We also update the navigation bar of the base template (base.html) to include the "Log In" and "Log Out" links depending on the logged-in state of the current user. Fig. 11 show how to include these links in the base.html.

Fig 11. app/templates/base.html: Include the Log In and Log Out in the navigation bar
Fig 11. app/templates/base.html: Include the Log In and Log Out in the navigation bar
Sign users in & out

The login() function in Fig. 12 shows the implementation of authenticating users. The login_user() function include two parameters, the first one is a User object and the second one is a Boolean variable to determine the user's session in either long-term or short-term. The value of this second parameter is obtained from the form.remember_me.data where FALSE indicates the application to clear the user's session when the browser window is closed, and thus the user will have to log in again next time.

To render the login form defined in the Adding a Login Form section on web page, we need to create a template file, named app/templates/login.html to include the form object, as defined in login() function in Fig. 12.

The logout() function in Fig. 12 will remove and reset the user session when it is invoked via its route. To inform users that they are successfully logged out from the application, we will flash a message that confirms the action.

Fig 12. app/auth/view.py: Implementation of Log In and Log Out procedures
Fig 12. app/auth/view.py: Implementation of Log In and Log Out procedures
Running your application

At this point, your application can first provide some basic functions to users such as authenticating a user (sign in and sign out) and rendering an index.html whose contents is dynamic based on the information of the current user. In order to test/run this Flask application for the first time, you need to execute the following commands in terminal:

  1. When working on a new project, you should create database migrations with flask db init command, and it will result a new migrations directory placed in the root of the project directory. If this error message Error: No such command 'db'. comes up, you should execute the export FLASK_APP=flasky.py command before executing flask db init.
  2. Before testing the implemented authentication functions, you need to create a new database and a User instance. Since we do not implement the User Registration at this time, we can only create a new User instance from the shell. Executing the below commands one by one to start Flask shell and create a User instance:
    flask db upgrade
    flask shell
    >>> db.create_all()
    >>> u = User(email='huuphucduong@gmail.com', username='kelvin', password='lovecoding')
    >>> db.session.add(u)
    >>> db.session.commit()
    >>> print(u.id)
    1
  3. After executing commands mentioned in step (2), a data-dev.sqlite database file is created in the project directory.
  4. The environment variables defined in config.py need to be instantiated, especially FLASK_APP variable. It is also useful to enable Flask's debug mode by setting FLASK_DEBUG=1. For macOS or Unix-based OS, you execute the below commands. For Microsoft Windows, just changing export keyword to set.
    export FLASK_APP=flasky.py
    export FLASK_DEBUG=1
  5. And finally, run the application as usual be executing flask run command.

It is worth noting that the above-mentioned steps (1, 2, 3) are only necessary for the first run.

New User Registration

A registration form is used to allow new users to enter their information such as email address, username, and password to create an account, and then they can use the created account to log in to your application. This section will present how to create a User Registration Form and implement the corresponding back-end function.

As usual, it is a 3-step process to implement a new web form including (1) define a new class to represent a web form by inheriting FlaskForm class, (2) defining the procedure to handle and process data submitted by users in views.py together with a corresponding URI in @auth.route('/path') decorator, and (3) implementing an HTML file (template) to render the form on web browsers and mapping to views.py through render_template('path/to/html', form=form) function.

In Fig. 13, we present the implementation of the user registration form in app/auth/forms.py. The registration form consists of five pieces of information, i.e., email, username, password, comfirm_password, and submit. The password field is entered twice to make sure that the entered passwords are identical. The WTForms provide the EqualTo validator to compare the values of two fields. When a user registers for an account, the application must check whether an input email and/or username has been registered before. To implement a validator like this, we can take advantage of the prefix validate_ followed by the name of the field method such as validate_email(), validate_username(). And, don't worry, Flask understands and is able to handle the methods like those. Let's take validate_email(self, field) function as an example, we first check whether an input email (field) is in User table, if True, we just raise a ValidationError exception with the error message as an argument.

Fig 13. app/auth/forms.py: user registration form
Fig 13. app/auth/forms.py: user registration form

The template (HTML) represents RegistrationForm form is defined in /templates/auth/register.html. Like the login template, the register.html render the RegistrationForm with wtf.quick_form() function. We should modify the /templates/auth/login.html to include a link to the registration form by embedding a hyperlink, for example, <a href="{{ url_for('auth.register') }}">Click here to register</a>.

In Fig. 14, we present the implementation of register() function in app/auth/views.py. When the registration form is submitted and validated, we create a new User instance in database based on the information provided by the user. We recall that the RegistrationForm in forms.py did implement the procedure to check whether a provided email or username existed in the database. We note that both login and register functions employ the GET and POST HTTP methods.

Fig 14. app/auth/views.py: user registration route
Fig 14. app/auth/views.py: user registration route
Account Confirmation

For certain types of applications, it is important to ensure that the user information provided during registration is valid. A common requirement is to ensure that the user can be reached through the provided email address.

To validate the email address, applications send a confirmation email to users immediately after they register. The new account is initially marked as unconfirmed until the instructions in the email are followed, which proves that the user has received the email. The account confirmation procedure usually involves clicking a specially crafted URL link that includes a confirmation token.

To implement Account Confirmation mechanism, you can employ itsdangerous package to generate and verify a token. The generated token is sent to user's email address, and the application will use that token to verify the user. We can employ the TimedJSONWebSignatureSerializer provided by itsdangerous to generate and verify a confirmation token. However, from itsdangerous 2.0.0 (released on 2021-05-11), JWS support (JSONWebSignatureSerializer, TimedJSONWebSignatureSerializer) is deprecated. Therefore, I will present a dedicated JWS/JWT library such as authlib instead.

You first need to install authlib by executing pip install Authlib in your current Python environment. The procedure to implement the Account Confirmation function is as follows:

  • Implementing generate_confirmation_token() and confirm() functions in app/models.py in which the generate_confirmation_token() function employs the authlib.jose.JsonWebSignature to generate a confirmation token and the confirm() function will then verify a submitted token.
  • def generate_confirmation_token(self) — We first declare the jws as an instance of JsonWebSignature() class. The protected variable is a dict of protected header such as alg (algorithm), kid (key id). The payload variable is a bytes/string of payload (a data to be serialized). In this program, we define payload as an JSON data including comfirm and timestamp keys where confirm is used to determine the id of user to verify and timestamp is used to make sure that the content of the genarated token will be different even if the user request a confirmation token many times. Another objective of timestamp is to check whether a token is expired by comparing with the pre-defined expiration. The secret variable is a private key used to generate signature, we can use the SECRET_KEY as defined in environment variable or declare a new one. authlib also supports a private PEM key to sign the JWS if we use RSA Signature such as RS256, RS384, RS512. The token will store the serialized data by employing jws.serialize_compact function from authlib.jose.JsonWebSignature package. This function takes in three arguments, i.e., protected, payload, key to generate a JWS Compact Serialization which represents digitally signed or MACed content as a compact, URL-safe string.
  • def confirm(self, token, expiration=3600) — This function takes in two arguments, i.e., token and expiration with default value is 3600. There are two main steps to be performed, first, we need to deserialize a token, and then, validate the timestamp and token. We first employ the jws.deserialize_compact funtion from authlib.jose.JsonWebSignature package to deserialize a token by inputting the token and the secret_key. We can see that one (secret) key is shared between the two parties to serialize and deserialize tokens. Since the jws.deserialize_compact() will raise the authlib.jose.errors.BadSignatureError error if the token data is compromised. Therefore, we put the deserialization code inside the try-except to avoid crashing our program. And then, we use json.loads() which is a Python's built-in function to convert payload data to JSON format, and we thus can get the confirm and timestamp data that were embedded in generate_confirmation_token(). We note that the timestamp must be in %Y-%m-%d %H:%M:%S.%f format (including the milliseconds element). We perform a substraction between datetime.now() and embedded timestamp, and store the result in seconds to duration_in_second variable. The confirm data (aka. user's id) can be easily extracted from payload by executing decoded_payload.get('confirm'). And finally, we validate the duration_in_second and user_id, and if they are valid, we change the state of confirmed property (in database) and save the update.
  • The implemetation of the above functions is presented in Fig. 15.

Fig 15. Implementing the generation and validation of a confirmation token
Fig 15. Implementing the generation and validation of a confirmation token

We now update the register() function in app/auth/views.py to include the new implemented generate_confirmation_token() function and the sendEmail() function. Fig. 16 shows the new version of app/auth/views.py. You can refer to the source code repository to checkout the confirmation email template, and place them (a plain text and an HTML versions) to app/templates/auth/email/ directory.

Fig 16. app/auth/views.py: Update the register() function
Fig 16. app/auth/views.py: Update the register() function

The view function that confirms accounts in implemented in confirm(token) function and is mapped with @auth.route('/confirm/<token>'), as shown in Fig. 16. Since this route is protected with the login_required decorator, the user need to log in before accessing this view when clicking on the confirmation link in email. If the user has been already confirmed (current_user.confirmed is True), the function redirects the user to the homepage. Otherwise, it will validate the submitted token by employing the confirm() function in app/models.py.

In order to deal with an unconfirmed user, we should let them log in to the application, but only show them a webpage that asks them to confirm their account before using the application. To do that, we can use the Flask's before_request hook. It is worth to note that the before_request hook can be applied to the blueprint where it is declared. If you want to apply a hook for all application requests, you must use before_app_request instead, as shown in before_request() function in Fig. 10. We will redirect all unconfirmed users' requests to the auth.unconfirmed view. Therefore, we need to implement an unconfirmed() function with @auth.route('/unconfirmed') decorator, and an HTML template in app/templates/auth/unconfirmed.html.

The unconfirmed.html will show a message to announce that the current user is unconfirmed, and also show them a link to the resend_confirmation() function to resend a new confirmation link, as shown in Fig. 10. We can see that the content of resend_confirmation() is similar to the register(), but using the current_user variable instead.


Exercises

  1. There is an issue with the current application that users can access the /auth/login and /auth/register webpage even though they have been already authenticated (aka. logged in to the application). To address this issue, you need to update the templates/auth/login.html and templates/auth/register.html in order to validate whether a user is authenticated. If a user has been already authenticated, the application will redirect the user to the homepage; otherwise, the application will render the corresponding webpages. Hint: you should take advantage of the current_user.is_authenticated property to determine whether the user is logged in.
  2. In case of a security-conscious, the application should provide a function to allow users to change their password. This function is presented as a web form including three text fields which are an old password, a new password and a verified one. You should note that users need to sign in to the application before performing this function. As part of this change, the navigation bar need to be refactored to include "Change Password" and "Log Out" links.
  3. If a user forgot his/her password, he/she can request a password reset to avoid locking out of the application. To implement the reset password function in a secure way, it is necessary to use authorized token mechanism as used in registering new account. In particular, when a user requests a password reset, an email with a reset token is sent to his/her registered email address, and then, the user can use this token to reset his/her password. As part of this change, the log in page does include a link to "Reset Password" function.
  4. The application also supports users to change their verified email address. To implement this function, an authorized user first enters a new email address, and then, an confirmation email will be sent to this new address. The idea of this function is similar to either register new account or reset password function in which the application generate a token to verify the change. While the server waits to receive the token, it can temporarily store the new address along with the user's id.
  5. To enhance the user experience (UX) on the application, you should improve the authentication mechanism that allows users to authenticate their credential via email. In particular, instead of entering username and password, users can enter their registered email address, and the application will send an confirmation link to their email. And then, users just click on the link to log in to the application.
  6. Update the login.html and register.html to include a recaptcha field, as shown in Fig. 17. Hint: you can employ the RecaptchaField in Flask-WTF to embed a Google reCAPTCHA (V2).
  7. Fig 17. Embed Google reCAPTCHA (V2) to login.html and register.html
    Fig 17. Embed Google reCAPTCHA (V2) to login.html and register.html

Further Reading

  1. ItsDangerous - General Concepts
  2. Flask-Login
  3. Werkzeug (password hashing and verification)
  4. [DigitalOcean] How To Add Authentication to Your App with Flask-Login
  5. Critical vulnerabilities in JSON Web Token libraries
  6. Exercises' Solutions

Next Lesson

In the next lesson, we will enhance the user authentication mechanism with third-party services such as Microsoft and Google. After completing the next lesson, users will be able to sign in to your application via their Microsoft or Google account, instead of using the one created via your application.

Single-sign on with Microsoft and Google


Relaxing 🧘