Tehnologii Web/2022-2023/Laborator 10

Flask JWT Authentication

edit

In this lesson, we will talk about an authentication method in Flask, called JSON Web Tokens (JWT).

We will go step by step through this process.

Installing packages

edit

In this chapter, we will specify and install all required project packages. We will build a REST API authentication token for an API project.

We will use the following libraries:

  • Flask,
  • pyjwt,
  • flask-sqlalchemy,
  • datetime,
  • uuid

In general, for all packages that are installed, it is useful to enter the package names and their version in a text file called requirements.txt. It is useful to deploy the application using the Heroku platform.

For example:

flask==1.1.2
pyjwt==2.0.0
datetime
uuid
Flask-SQLAlchemy

To automatically install the libraries, the line in the terminal will be:

pip install -r requirements.txt

Also, to install the libraries one-by-one, the command will be:

pip install flask_sqlalchemy

and others.

Start of the implementation

edit

In the app.py file we import the libraries:

from flask import Flask, jsonify, make_response, request
from werkzeug.security import generate_password_hash,check_password_hash
from flask_sqlalchemy import SQLAlchemy
from functools import wraps
import uuid
import jwt
import datetime
import os

We should observe here the os library whose prominent role is to access the absolute path of a directory where a database is present. First, we should create a file called booksA.db. Next, we configure the application to be compatible with a database and the JWT process.

app = Flask(__name__)
app.config['SECRET_KEY'] = 'introduce_one'
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'booksA.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
 
db = SQLAlchemy(app)

To obtain the secret key, we can auto-generate it by using the secret library. After we introduce the python keyword in the Terminal, we can write the following lines:

import secrets 
secrets.token_hex(16)

Then, we are going to create the table Users, also in the app.py file:

class Users(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   public_id = db.Column(db.Integer)
   name = db.Column(db.String(50))
   password = db.Column(db.String(50))
   admin = db.Column(db.Boolean)

Also, we will specify all the columns' properties. Connection of columns from different tables within a database. Now, to use JWT for the purpose of connecting the columns in the tables, a new table called Books will be created.

class Books(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
   name = db.Column(db.String(50), unique=True, nullable=False)
   Author = db.Column(db.String(50), unique=True, nullable=False)
   Publisher = db.Column(db.String(50), nullable=False)
   book_prize = db.Column(db.Integer)

To actually create the tables with Flask-JWT, we use the following line:

with app.app_context():
   db.create_all()
   db.session.commit()

After we introduced the previous lines, we can run the program.

Next, in app.py, we integrate the token_required(f) function. This will generate tokens that allow only registered users to access and manipulate a set of API operations.

Using the model for the user that will be stored in the database, the following code will be entered:

def token_required(f):
   @wraps(f)
   def decorator(*args, **kwargs):
       token = None
       if 'x-access-tokens' in request.headers:
           token = request.headers['x-access-tokens']
       if not token:
           return jsonify({'message': 'a valid token is missing'})
       try:
           data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
           current_user = Users.query.filter_by(public_id=data['public_id']).first()
       except:
           return jsonify({'message': 'token is invalid'})
       return f(current_user, *args, **kwargs)
   return decorator

Creating the register route for the application's Users table. We indicate a post method. After the function token_required(f), the following code sequence will be inserted:

@app.route('/register', methods=['POST'])
def signup_user(): 
   data = request.get_json() 
   hashed_password = generate_password_hash(data['password'], method='sha256')
   new_user = Users(public_id=str(uuid.uuid4()), name=data['name'], password=hashed_password, admin=False)
   db.session.add(new_user) 
   db.session.commit()   
   return jsonify({'message': 'registered successfully'})

Creating the login route. Afterward, we create a route for the login part. We should be careful at the timedelta() set minutes because the token will expire and we should reactivate it.

@app.route('/login', methods=['POST']) 
def login_user():
   auth = request.authorization  
   if not auth or not auth.username or not auth.password: 
       return make_response('could not verify', 401, {'Authentication': 'login required"'})   
   user = Users.query.filter_by(name=auth.username).first()  
   if check_password_hash(user.password, auth.password):
       token = jwt.encode({'public_id' : user.public_id, 'exp' : datetime.datetime.utcnow() + datetime.timedelta(minutes=50)}, app.config['SECRET_KEY'], "HS256")
       return jsonify({'token' : token})
   return make_response('could not verify',  401, {'Authentication': '"login required"'})

Next, a route that displays all users registered in the application will be created. The route will check the registered users from the Users table and provide an output in JSON format. The following sequence will be used here:

@app.route('/users', methods=['GET'])
def get_all_users(): 
   users = Users.query.all()
   result = []  
   for user in users:  
       user_data = {}  
       user_data['public_id'] = user.public_id 
       user_data['name'] = user.name
       user_data['password'] = user.password
       user_data['admin'] = user.admin
       result.append(user_data)  
   return jsonify({'users': result})

Creating routes for the Books table. These routes allow users to pull books from the table, database, and delete them as needed.

A mandatory check will also be implemented to verify that the tokens are valid.

@app.route('/book', methods=['POST'])
@token_required
def create_book(current_user):
 
   data = request.get_json()
 
   new_books = Books(name=data['name'], Author=data['Author'], Publisher=data['Publisher'], book_prize=data['book_prize'], user_id=current_user.id) 
   db.session.add(new_books)  
   db.session.commit() 
   return jsonify({'message' : 'new books created'})

The next route is for getting all the books in the Books column.

@app.route('/books', methods=['GET'])
@token_required
def get_books(current_user):
 
   books = Books.query.filter_by(user_id=current_user.id).all()
   output = []
   for book in books:
       book_data = {}
       book_data['id'] = book.id
       book_data['name'] = book.name
       book_data['Author'] = book.Author
       book_data['Publisher'] = book.Publisher
       book_data['book_prize'] = book.book_prize
       output.append(book_data)
 
   return jsonify({'list_of_books' : output})

To delete a book from the database, the following sequence and route are used:

@app.route('/books/<book_id>', methods=['DELETE'])
@token_required
def delete_book(current_user, book_id): 
   book = Books.query.filter_by(id=book_id, user_id=current_user.id).first()  
   if not book:  
       return jsonify({'message': 'book does not exist'})  
   db.session.delete(book) 
   db.session.commit()  
   return jsonify({'message': 'Book deleted'})

Main function and running the application:

 if  __name__ == '__main__': 
    app.run(debug=True)

Steps in the Postman API Platform desktop application

edit

To get started, we will need to download the desktop application called Postman API Platform, found at the following address: https://www.postman.com/downloads/. After the download is finished, open the application and from the Workspaces section create a new workspace, named FirstAPILocal. After that, add different routes to the localhost address, such as: /register, /login, /users, /books, /book. All routes other than /book will have the POST method. The /book tab will be GET.

Example: localhost/users

The x-access-tokens key has to be added to the table for localhost/book in the Headers section, but the value field is unknown. We will go to the Body section of localhost/book and add the typical JSON file lines to create a new book and add it to the Books table:

{
    "name" : "Lisbon",
    "Author" : "Rodrigues",
    "Publisher" : "NOVP",
    "book_prize" : 54
}

Note. For each JSON code, we choose the raw option. Afterwards, we will go to the localhost/register tab, in the Headers section, to check that there is no key entered. Then, we go to the Body section and enter the lines of code specific to a JSON file, related to the database table called Users:

{
    "name" : "User verifs",
    "password" : "parola"
}

The Send button will be clicked and a registration request will be sent to the system. If the process was carried out correctly, it appears as a response:

{
    "message": "registration successfully"
}

and the created user will appear as an observation in the database that can be accessed from the IDE.

Then, we will go to the tab for login, and select the Authorization section. The Basic Auth variant will be selected from the Combobox. The user's registration data is entered there. After pressing Send, a token will be obtained as a response. That token is valid, as specified, for 50 minutes. We will copy the token and put it in the value field from the Headers section, from localhost/book and we will give Send. A JSON message will be returned as a response, indicating that the book has been added to the table. This can also be seen in the table in the IDE.

Afterward, we go to the /books path to display the list of existing books. The same x-access-tokens will be entered as key, and that login token as value.

A similar step is applicable to the part of deleting a book from the list, entering only the ID of that book as a continuation of the route.

Statements and notes

edit

Some of the advantages of using Flask are:

  • Provides a well-structured development server
  • Greater compatibility with modern technologies
  • Routing URL is simple
  • Minimal and powerful framework
  • Smaller code base size.

Another solution, and method, for the authentication process, is using Blueprint, Flask forms, and Flask-SQLAlchemy library in the project.