Tehnologii Web/2022-2023/Laborator 12

Another Flask Authentication Methods edit

Allowing users to log in to the application is one of the most common features in web applications.

Installation gives packages edit

There are three main packages needed for the application:

  • Flask
  • Flask-Login: To manage user sessions after login
  • Flask-SQLAlchemy: to represent the user model and database interface

So, install the packages using the following line in Terminal:

$ pip install flask flask-sqlalchemy flask-login

We will use SQLite to avoid installing additional database dependencies.

A new project named flask_auth_app will be created. And, inside it, a new folder called project will be created here.

Creating the main file edit

We will create the main file, but also an intermediate one (since a lot of main classes are called here), called __init__.py, in the folder named project. Everything we will introduce here will be stored and put in the project folder.

One plan handles the usual routes, which include the index page and the protected profile page. Another one handles everything related to authentication. In a real application, the functionality can be achieved in another way as well, but the solution presented here will work well for this tutorial.

This file will have the function of creating the application, which will initialize the database and register the data in the database.

SQLAlchemy will need to be initialized, some configuration values set, and data saved.

import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy


db = SQLAlchemy()

def create_app():
    app = Flask(__name__)

    app.config['SECRET_KEY'] = 'secretkey'
    basedir = os.path.abspath(os.path.dirname(__file__))
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'baza.db')

    db.init_app(app)

    # blueprint for auth routes of the application 
    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    # blueprint for non-auth routes of the application 
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app

Adding routes edit

For routes, two Blueprints will be used.

For main_blueprint, there will be a home page (/) and a profile page (/profile).

First, main.py is created:

from flask import Blueprint
from . import db

main = Blueprint('main', __name__)

@main.route('/')
def index():
    return 'Index'

@main.route('/profile')
def profile():
    return 'Profile'

For auth_blueprint there will be routes to fetch both the login page (/login) and the registration page (/signup). Finally, there will also be a logout route (/logout) to log out an active user. Next, a new file that uses Blueprint authentication will be created. The following code lines are added to the auth.py.

auth = Blueprint('auth', __name__)

@auth.route('/login')
def login():
    return 'Login'

@auth.route('/signup')
def signup():
    return 'Signup'

@auth.route('/logout')
def logout():
    return 'Logout'

For now, log in, registration, and log out are defined with text returns. There will also be routes for handling POST requests from login and register. We will review this code later and update it with the desired functionality. In a terminal, we can set the FLASK_APP and FLASK_DEBUG values:

# Unix 
$ export FLASK_APP=project
$ export FLASK_DEBUG=1

# Windows 
$ set FLASK_APP=project
$ set FLASK_DEBUG=1

The FLASK_APP environment variable tells Flask how to load the application. We would want this to indicate where create_app method is. The project folder will be indicated.

The FLASK_DEBUG environment variable is enabled by setting it to 1. This will enable a debugger that will display application errors in the browser.

We will run the application with flask run in Terminal.

After verifying that the routes behave as expected, the templates can be created.

Creating templates (HTML files) edit

This is the first step before we can implement the actual login functionality. The application will use four templates:

  • index.html
  • profile.html
  • login.html
  • signup.html

There will also be a basic template that will have a common code for each of the pages. In this case, the base template will have navigation links and the overall page layout.

First, create a directory called templates in the project folder. Next, the main HTML page, named base.html, will be created.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask Auth Example</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" />
</head>
<body>
    <section class="hero is-primary is-fullheight">
        <div class="hero-head">
            <nav class="navbar">
                <div class="container">
                    <div id="navbarMenuHeroA" class="navbar-menu">
                        <div class="navbar-end">
                            <a href="{{ url_for('main.index') }}" class="navbar-item">
                                Home
                            </a>
                            <a href="{{ url_for('main.profile') }}" class="navbar-item">
                                Profile
                            </a>
                            <a href="{{ url_for('auth.login') }}" class="navbar-item">
                                Login
                            </a>
                            <a href="{{ url_for('auth.signup') }}" class="navbar-item">
                                Sign Up
                            </a>
                            <a href="{{ url_for('auth.logout') }}" class="navbar-item">
                                Logout
                            </a>
                        </div>
                    </div>
                </div>
            </nav>
        </div>
        <div class="hero-body">
            <div class="container has-text-centered">
               {% block content %}
               {% endblock %}
            </div>
        </div>
    </section>
</body>
</html>

This code will create a series of menu links to each page of the application. It also sets a block for content that can be overridden by a child template. In the index.html page that will be created, add the following code sequence:

{% extends "base.html" %}

{% block content %}
<h1 class="title">
  Login Example
</h1>
<h2 class="subtitle">
  Authentication and authorization in Flask.
</h2>
{% endblock %}

The code will display a title and a subtitle. Next, a login.html page will be created.

{% extends "base.html" %}

{% block content %}
<div class="column is-4 is-offset-4">
    <h3 class="title">Login</h3>
    <div class="box">
        <form method="POST" action="/login">
            <div class="field">
                <div class="control">
                    <input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus="">
                </div>
            </div>

            <div class="field">
                <div class="control">
                    <input class="input is-large" type="password" name="password" placeholder="Your Password">
                </div>
            </div>
            <div class="field">
                <label class="checkbox">
                    <input type="checkbox" name="remember">
                    Remember me
                </label>
            </div>
            <button class="button is-block is-info is-large is-fullwidth">Login</button>
        </form>
    </div>
</div>
{% endblock %}

This code generates a login page with email and password fields. There is also a checkbox to remember a connected session. Add the following code to create a page for registration, signup.html, with email, name, and password fields:

{% extends "base.html" %}

{% block content %}
<div class="column is-4 is-offset-4">
    <h3 class="title">Sign Up</h3>
    <div class="box">
        <form method="POST" action="/signup">
            <div class="field">
                <div class="control">
                    <input class="input is-large" type="email" name="email" placeholder="Email" autofocus="">
                </div>
            </div>

            <div class="field">
                <div class="control">
                    <input class="input is-large" type="text" name="name" placeholder="Name" autofocus="">
                </div>
            </div>

            <div class="field">
                <div class="control">
                    <input class="input is-large" type="password" name="password" placeholder="Password">
                </div>
            </div>

            <button class="button is-block is-info is-large is-fullwidth">Sign Up</button>
        </form>
    </div>
</div>
{% endblock %}

The code for the profile.html page is the following one:

{% extends "base.html" %}

{% block content %}
<h1 class="title">
  Welcome, Users! 
</h1>
{% endblock %}

We will revisit this code later to dynamically greet any user.

Once we've added the templates, we can update the return statements in each of the routes to return templates instead of text.

Update main.py by changing the import line and routes for index and profiles:

from flask import Blueprint, render_template

@main.route('/')
def index():
    return render_template('index.html')

@main.route('/profile')
def profile():
    return render_template('profile.html')

Also we update auth.py.

from flask import Blueprint, render_template

@auth.route('/login')
def login():
    return render_template('login.html')

@auth.route('/signup')
def signup():
    return render_template('signup.html')

Nothing will be done to /logout, for now.

Creating user models edit

This user model represents will be important for storing users. There will be the required email address fields, a password, and a name. In future applications, we can decide whether we want additional information to be stored for each user. Remarks such as birthdays, profile pictures, locations, or any preferences can be added.

Models created in Flask-SQLAlchemy are represented by classes that translate into tables of a database. The attributes of those classes then become columns for those tables.

Create the user model within the models.py file:

from . import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
    email = db.Column(db.String(100), unique=True)
    password = db.Column(db.String(100))
    name = db.Column(db.String(1000))

This code defines a user with columns for an id, email, password, and name.

Now that a user model has been created, we can move on to configuring the database.

Database configuration edit

An SQLite database will be used. One can create an SQLite database on their own, but it will need to be integrated using Flask-SQLAlchemy. There is already the database path specified in the __init__.py file, so Flask-SQLAlchemy will need to be specified to create the database, by running the code or by using the Python REPL. we will use here the procedure of running the code for creating the database.

We should make sure that we are in the folder of the project.

We can create the database using the create_all method on the db object, in the __init__.py file, after the db.init_app(app) code line:

    from . import models 
    with app.app_context():
        db.create_all()

Being in this position, we can run the application and create the database directly.

Configuring the authorization function edit

For the register function, the data that the user submits in the form will be retrieved and added to the database. We will need to ensure that a user with the same email address does not already exist in the database. If it doesn't exist, then we need to make sure we've hashed the password before entering it into the database.

It starts by adding a second function to handle the POST form data. The data transmitted from the user is collected.

Update auth.py by changing the import line and implement signup_post:

from flask import Blueprint, render_template, redirect, url_for, request
from werkzeug.security import generate_password_hash, check_password_hash
from .models import User
from . import db 

# The next code will be created and added after the previous version of this file 

@auth.route('/signup', methods=['POST'])
def signup_post():
    # code to validate and add a user to the database goes here
    email = request.form.get('email')
    name = request.form.get('name')
    password = request.form.get('password')

    user = User.query.filter_by(email=email).first() # if this returns a user, then the email already exists in database

    if user: # if a user is found, we want to redirect back to signup page so user can try again
        return redirect(url_for('auth.signup'))

    # create a new user with the form data. Hash the password so the plaintext version isn't saved.
    new_user = User(email=email, name=name, password=generate_password_hash(password, method='sha256'))

    # add the new user to the database
    db.session.add(new_user)
    db.session.commit()

    return redirect(url_for('auth.login'))

Testing the registration method edit

Now that the register method is complete, a new user can be created. Let's test the form to create a user.

There are two ways for checking if the registration was successful:

  1. a database viewer can be used to see the row that was added to the table or
  2. we can try to sign up again with the same email address and if we get an error we will know the first email has been saved correctly.

The added code will notify the user that the email already exists and will need to be redirected, to go to the login page. By calling the flash function, it will be possible to send a message to the next request, which in this case is the redirect. The page that the user is redirected to will then have access to that template message.

First, the flash is added before redirecting to the registration page.

The auth.py file will be updated.

# The next code will be created and added after the previous version of this file 

from flask import redirect, url_for, request, flash

@auth.route('/signup', methods=['POST'])
def signup_post():
    if user: # if a user is found, we want to redirect back to the signup page so the user can try again
        flash('Email address already exists')
        return redirect(url_for('auth.signup'))

To receive the flash message in the template, we can add this code before the form in the signup.html file:

{% with messages = get_flashed_messages() %}
{% if messages %}
    <div class="notification is-danger">
        {{ messages[0] }}. Go to <a href="{{ url_for('auth.login') }}">login page</a>.
    </div>
{% endif %}
{% endwith %}
<form method="POST" action="/signup">

This code will display the message "Email address already exists. Go to login page".

Adding the login method edit

The login method is similar to the registration function. In this case, we will compare the entered email address, to see if it is in the database. If so, the user-supplied password will be tested by hashing the password that the user submits and comparing it to the hashed password in the database. We'll know the user entered the correct password when both hashed passwords match.

Once the users introduce the password check, they will know they have the correct credentials and can log in using Flask-Login. By calling login_user, Flask-Login will create a session for that user that will persist as long as the user remains logged in, allowing the user to view protected pages.

One can start with a new route for handling POST data. And redirect to the profile page when the user successfully login.

In the auth.py file, add:

@auth.route('/login')
def login():
    return render_template('login.html')

@auth.route('/login', methods=['POST'])
def login_post():
    # login code goes here
    return redirect(url_for('main.profile'))

Credentials must be verified:

@auth.route('/login', methods=['POST'])
def login_post():
    # login code goes here
    email = request.form.get('email')
    password = request.form.get('password')
    remember = True if request.form.get('remember') else False

    user = User.query.filter_by(email=email).first()

    # check if the user actually exists
    # take the user-supplied password, hash it, and compare it to the hashed password in the database
    if not user or not check_password_hash(user.password, password):
        flash('Please check your login details and try again.')
        return redirect(url_for('auth.login')) # if the user doesn't exist or password is wrong, reload the page

    # if the above check passes, then we know the user has the right credentials
    return redirect(url_for('main.profile'))

Let's add the block to the template so that the user can see the flash message:

{% with messages = get_flashed_messages() %}
{% if messages %}
    <div class="notification is-danger">
        {{ messages[0] }}
    </div>
{% endif %}
{% endwith %}
<form method="POST" action="/login">

The previous code sequence is added to the login.html file. Add the following code sequence to the models.py file:

from flask_login import UserMixin
from . import db

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True) # primary keys are required by SQLAlchemy
    email = db.Column(db.String(100), unique=True)
    password = db.Column(db.String(100))
    name = db.Column(db.String(1000))

Next, the user loader must be specified. A load user tells Flask-Login how to find a particular user from the ID that is stored in the session cookie. Add this sequence to the create_app function along with the init code for Flask-Login from the __init__.py file. It is necessary to import the following libraries:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager

Afterward, the following code will be introduced after the previous version lines in the create_app function.

def create_app():
    
    # add code after the previously added lines in this function 
    
    db.init_app(app)

    login_manager = LoginManager()
    login_manager.login_view = 'auth.login'
    login_manager.init_app(app)

    from .models import User

    @login_manager.user_loader
    def load_user(user_id):
        # since the user_id is just the primary key of our user table, use it in the query for the user
        return User.query.get(int(user_id))

Finally, add the login_user function before redirecting to the profile page to create the session, in the auth.py file.

from flask_login import login_user
from .models import User
from . import db

# Previous code from the file 

@auth.route('/login', methods=['POST'])
def login_post():

    # Previous code of the function 
    
    # if the above check passes, then we know the user has the right credentials
    login_user(user, remember=remember)
    return redirect(url_for('main.profile'))

With the Flask-Login configuration, the /login route is used. When everything is typed correctly, we will see the profile page.

At this point, the application can be run and user login to the application can be attempted.

Protecting pages edit

If the username is not Users, or if we want to make an introduction for a specific user, then the code will need to be changed. The scope of the profile is to display the name in the database. We will need to protect the page and then access the user data to get the name.

To protect a page when using Flask-Login, add the @login_required decorator between the route and the function. This will prevent an unauthenticated user from seeing the route. If the user is not authenticated, the user will be redirected to the login page, according to the Flask-Login configuration.

With routes that are decorated with the @login_required decorator, we can use the current_user object inside the function. This current_user represents the user in the database and provides access to all attributes of that user in dot notation. For example, current_user.email, current_user.password and current_user.name and current_user.id will return the actual values ​​stored in the database for the logged in user.

So, we will use the name current_user and send it to the template:

In the main.py file:

from flask import Blueprint, render_template
from flask_login import login_required, current_user
from . import db

# The previous code 

@main.route('/profile')
@login_required
def profile():
    return render_template('profile.html', name=current_user.name)

In the profile.html file:

<h1 class="title">
  Hello, {{ name }}!
</h1>

Once a user visits his profile page, he will be greeted by his name. Now, to update the logout view, the logout_user function is called in a logout route, in the auth.py file.

from flask_login import login_user, login_required, logout_user

# The previous code 

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('main.index'))

The @login_required decorator is used because it doesn't make sense to log out a user who isn't logged in to. One last thing to do is to put if statements in the templates to show only the links relevant to the user, in the base.html file:

<div class="navbar-end">
    <a href="{{ url_for('main.index') }}" class="navbar-item">
        Home
    </a>
    {% if current_user.is_authenticated %}
    <a href="{{ url_for('main.profile') }}" class="navbar-item">
        Profile
    </a>
    {% endif %}
    {% if not current_user.is_authenticated %}
    <a href="{{ url_for('auth.login') }}" class="navbar-item">
        Login
    </a>
    <a href="{{ url_for('auth.signup') }}" class="navbar-item">
        Sign Up
    </a>
    {% endif %}
    {% if current_user.is_authenticated %}
    <a href="{{ url_for('auth.logout') }}" class="navbar-item">
        Logout
    </a>
    {% endif %}
</div>

Before the user connects to the application, he will have the option to log in or register. After he's logged in, he can go to his own profile or log out of the aplication.