REST API (RESTful) running on an Enterprise class system

REST API (RESTful) running on an Enterprise system part 2 – user authentication

In the previous article in this series, we showed how to build a simple server using a web framework like flask. Today we’ll show how to extend that project and improve it with user authentication using cookies.

In the previous article in this series, we showed how to build a simple server using a web framework like flask. Today we’ll show how to extend that project and improve it with user authentication using cookies.

For those who haven’t read the previous article, I strongly encourage you to do so – REST API (RESTful) running on an Enterprise class system part 1. It serves as an introduction to the series and is directly related to the steps we will take today.

The structure we have created so far is as follows:

.
├── app
│   ├── __init__.py
│   └── app.py
├── httpd.conf
├── run.py
└── wsgi.py

The project directory may also contain files and directories such as bin, lib, __pycache__ and others. These are files left by the running application and files that create the virtual python environment. For our operations we will need flask-sqlalchemy and flask-login, which we install with:

pip install flask flask-sqlalchemy flask-login

Flask-login provides us with tools that allow us to simply handle user authentication by login and password with automatic handling of cookes.

1. User Model

Pierwszym krokiem będzie stworzenie modelu użytkownika. W tym celu w pliku UserModel.py tworzymy bazę danych przy pomocy metody SQLAlchemy() wraz z klasą UserModel dziedziczącą po UserMixin oraz db.Model. UserMixin jest klasą zawierającą metody podstawowej obsługi użytkownika, takie jak:

  • is_authenticated() – we will use it to distinguish logged-in users
  • get_id() – returns the ID of the logged in user
  • is_active() – it can be used to verify an email address or block users
  • is_anonymous() – is used to identify a non-logged-in or anonymous user.

db.Model deals with the handling of a database table containing user information. In the body of the class, we define what data the user will accept:

class UserModel(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(80), unique=True)
    username = db.Column(db.String(20))
    password_hash = db.Column(db.String())

Next, we create methods to process and save password. To do this, we will use python’s built-in modules and methods: hashlib.sha256 and secrets.compare_digest. The file created in this way should look as follows:

from hashlib import sha256
from secrets import compare_digest
from flask_login import UserMixin
from flask_sqlalchemy import SQLAlchemy

 
db = SQLAlchemy()
 
class UserModel(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(80), unique=True)
    username = db.Column(db.String(20), unique=True)
    password_hash = db.Column(db.String())
 
    def set_password(self, password):
        self.password_hash = sha256(password.encode()).digest()
     
    def check_password(self, password):
        return compare_digest(self.password_hash, sha256(password.encode()).digest())

2. User session handling

Flask comes with simple tools to provide session handling. In this project we will use the LoginManager imported from the flask_login module. In the file named login.py, we create an object of class LoginManager responsible for handling user sessions:

from .UserModel import UserModel
from flask_login import LoginManager

lm = LoginManager()

@lm.user_loader
def load_user(id):
    return UserModel.query.get(int(id))

Using the user_loader decorator, we define a function responsible for loading a user with a given ID from the database.

3. Initializing the database and instances of the LoginManager class

Now initialize the database and the instance we created in the earlier section.

To the app.py file prepared in the previous article, which looks like this:

#!/usr/bin/env python

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello world!"

we add database configuration:

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
app.config['SECRET_KEY'] = 'secret-key-goes-here'

and initialize an empty table before receiving the first request to the service:

db.init_app(app)
@app.before_first_request
def create_table():
    db.create_all()

We also initialize the login object imported from the file we created:

lm.init_app(app)
lm.login_view = 'login'

Using lm.login_view, we define the address of the page to which a non-logged-in user will be redirected if they try to open a site they do not have access to. After these operations, the app.py file should look as follows:

from flask import Flask
from .UserModel import db
from .login import lm

def create_app():
    app = Flask(__name__)
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
    app.config['SECRET_KEY'] = 'secret-key-goes-here'

    db.init_app(app)
    @app.before_first_request
    def create_table():
        db.create_all()
    lm.init_app(app)
    lm.login_view = 'login'
    return app

app = create_app()

@app.route("/")
def hello():
    return "Hello world!"

4. Request handling

Now all that’s left is to define the requests and the corresponding html pages that will be displayed to the user.

index/hello

We’ll start by editing the hello function, which displays hello world to the user and is our start page. We will place buttons on it that redirect to the login and registration page. First, let’s add another route decorator with the address “/hello” and change return "Hello world!" to return render_template('hello.html').

The request now looks like this:

@app.route("/")
@app.route("/hello")
def hello():
    return render_template('hello.html')

The render_template function provided by the flask module allows us to render an html page from a template, which we place in the templates directory located in the folder with the application. Next, we create a hello.html file, in which we place redirects to the login and registration page (we will create them in the following steps). The file looks like this:

<h3>Hello world!</h3>
 
<form action = "{{url_for(login')}}" method = "GET">
    <input type = "submit" value = "Login">
</form>

<form action = "{{url_for(register')}}" method = "GET">
    <input type = "submit" value = "Register">
</form>

Warning. Defining redirects to a non-existent endpoint will cause an internal application error. Keep this in mind when testing the operation of add-ons to it.

hi

The /hi request will be the page displayed for the logged-in user. We define it with the addition of the login_required decorator as follows:

@app.route("/hi")
@login_required
def hi():
    return render_template('hi.html')

html template for hi.html:

<h1>Hello {{ current_user.username }}!</h1>
 
<form action = "{{url_for('logout')}}" method = "GET">
    <input type = "submit" value = "Logout">
</form>

As you can see, we can use macros such as current_user.username in the templates. By using flask_login, we can display any information associated with our user.

login

We define the /login request including the GET and POST methods. The GET request will be responsible for displaying the form, and the POST will be responsible for sending the information in it back to the service. We start with a decorator including the methods:

`@app.route('/login', methods = ['POST', 'GET'])`

Then we check if the user does not have an active session. If so, we redirect it to the user’s site (in our case it will be /hi).

if current_user.is_authenticated:
        return redirect('/hi')

Now, depending on whether the received request is a POST or a GET, we collect the information from the form and check whether the user and the password they provided match those in the database:

username = request.form['username']
user = UserModel.query.filter_by(username = username).first()
if user is not None and user.check_password(request.form['password']):
    login_user(user)
    return redirect('/hi')
else:
    return "wrong password\n" + render_template('login.html')

or we render the page login.html.

If the login and password are correct, we create a session for the user using the login_user(user) command, after which the user is redirected to the /hi site. If the login or password is incorrect, the user will be redirected back to the login.html site with a wrong password! note.

The entire login function presents itself as follows:

@app.route('/login', methods = ['POST', 'GET'])
def login():
    if current_user.is_authenticated:
        return redirect('/hi')
    if request.method == 'POST':
        username = request.form['username']
        user = UserModel.query.filter_by(username = username).first()
        if user is not None and user.check_password(request.form['password']):
            login_user(user)
            return redirect('/hi')
        else:
            return "wrong password\n" + render_template('login.html')
    return render_template('login.html')

The template for the login.html page looks like this:

<form action = "" method = "POST">
    <label for = "username">Username:</label><br>
    <input type = "text" id = "username" name = "username"><br>
    <label for = "password">Password:</label><br>
    <input type = "password" id = "password" name = "password"><br>
    <input type = "submit" value = "Login">
</form>

<form action = "{{url_for('register') }}" method = "GET">
    <input type = "submit" value = "Register">
</form>

In addition to the login form, there is also a button that redirects to the registration site.

register

The register function is analogous to the login function, with minor differences. Instead of verifying that the user exists and that the password is correct, it verifies that the email or username specified is not registered:

if UserModel.query.filter_by(email=new_email).all():
    return ('Email already registered' + render_template('register.html'))
if UserModel.query.filter_by(username=new_username).all():
    return ('Username already registered' + render_template('register.html'))

If the data entered is available, a new user is created and added to the database:

 user = UserModel(email = new_email, username = new_username)
        user.set_password(new_password)
        db.session.add(user)
        db.session.commit()

The user is then redirected to the login page. The register function presents itself as follows:

@app.route('/register', methods=['POST', 'GET'])
def register():
    if current_user.is_authenticated:
        return redirect('/hi')
     
    if request.method == 'POST':
        new_email = request.form['email']
        new_username = request.form['username']
        new_password = request.form['password']
        if UserModel.query.filter_by(email=new_email).all():
            return ('Email already registered' + render_template('register.html'))
        if UserModel.query.filter_by(username=new_username).all():
            return ('Username already registered' + render_template('register.html'))
        user = UserModel(email = new_email, username = new_username)
        user.set_password(new_password)
        db.session.add(user)
        db.session.commit()
        return redirect('/login')
    return render_template('register.html')

The html template for the register site is also analogous to the login.html template.

<form action = "" method = "POST">
    <label for = "email">Email:</label><br>
    <input type = "email" id = "email" name = "email"><br>
    <label for = "username">Username:</label><br>
    <input type = "text" id = "username" name = "username"><br>
    <label for = "password">Password:</label><br>
    <input type = "password" id = "password" name = "password"><br>
    <input type = "submit" value = "Register">
</form>

<form action = "{{url_for('login')}}" method = "GET">
    <input type = "submit" value = "Login">
</form>

logout

The logout request is responsible for logging out the user and is a trivial function to write. All you need to do is use the command in it: logout_user() provided by the flask_login module and redirect the user to the start page or login page:

@app.route('/logout')
def logout():
    logout_user()
    return redirect('/hello')

Summary

The final version of the app.py file looks like this:

#!/usr/bin/env python
from flask import Flask, request, render_template, redirect
from flask_login import current_user, login_user, login_required, logout_user
from .UserModel import UserModel, db 
from .login import lm

def create_app():
    app = Flask(__name__)
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
    app.config['SECRET_KEY'] = 'secret-key-goes-here'

    db.init_app(app)
    @app.before_first_request
    def create_table():
        db.create_all()
    lm.init_app(app)
    lm.login_view = 'login'
    return app

app = create_app()

@app.route("/")
@app.route("/hello")
def hello():
    return render_template('hello.html')

@app.route("/hi")
@login_required
def hi():
    return render_template('hi.html')

@app.route('/login', methods = ['POST', 'GET'])
def login():
    if current_user.is_authenticated:
        return redirect('/hi')
    if request.method == 'POST':
        username = request.form['username']
        user = UserModel.query.filter_by(username = username).first()
        if user is not None and user.check_password(request.form['password']):
            login_user(user)
            return redirect('/hi')
        else:
            return "wrong password\n" + render_template('login.html')
    return render_template('login.html')

@app.route('/register', methods=['POST', 'GET'])
def register():
    if current_user.is_authenticated:
        return redirect('/hi')
     
    if request.method == 'POST':
        new_email = request.form['email']
        new_username = request.form['username']
        new_password = request.form['password']
        if UserModel.query.filter_by(email=new_email).all():
            return ('Email already registered' + render_template('register.html'))
        if UserModel.query.filter_by(username=new_username).all():
            return ('Username already registered' + render_template('register.html'))
        user = UserModel(email = new_email, username = new_username)
        user.set_password(new_password)
        db.session.add(user)
        db.session.commit()
        return redirect('/login')
    return render_template('register.html')

@app.route('/logout')
def logout():
    logout_user()
    return redirect('/hello')

On the other hand, the file structure of the finished application looks like this:

.
├── app
│   ├── app.py
│   ├── db.sqlite
│   ├── __init__.py
│   ├── login.py
│   ├── templates
│   │   ├── hello.html
│   │   ├── hi.html
│   │   ├── login.html
│   │   └── register.html
│   └── UserModel.py
├── httpd.conf
├── run.py
└── wsgi.py

The above shows a simple way to do cookie-based user authentication using the flask framework using the flask_login module. Of course, it is possible to further extend the functionality of templates, clean up the application or move http request definitions to other files using the Blueprint class provided by flask. However, this is a topic for a separate article.

Authors

The blog articles are written by people from the EuroLinux team. We owe 80% of the content to our developers, the rest is prepared by the sales or marketing department. We make every effort to ensure that the content is the best in terms of content and language, but we are not infallible. If you see anything that needs to be corrected or clarified, we'd love to hear from you.