Let’s learn how to secure a REST API with JSON web tokens to prevent users and third-party applications from abusing it.

We will build a database service using SQLite and allow users to access it via a REST API using HTTP methods such as POST and PUT.

In addition, we will get to know why JSON web tokens is a suitable way to protect rest API instead of digest and basic authentication. Before we proceed, let’s understand the term JSON web tokens, REST API and Flask framework.

JSON Web Tokens

JSON web token, also known as JWT, is the secure way of transferring random tokens between two parties or entities. JSON is usually made up of three parts as the following.

  • Payload
  • Header
  • Signature

JSON uses two types of structure forms when transferring data or information between two parties.

  • Serialized
  • Deserialized

The serialized form is used when transferring data to the network through each request and response whilst the deserialized form is used when reading and writing data to the web token.

In the serialized form, there are three components.

  • Header
  • Payload
  • Signature

The header component defines the cryptographic information about the tokens. For example:

  • Is it signed or unsigned JWT?
  • Define algorithm techniques

The deserialized form, unlike the serialized form, contains two components.

  • Payload
  • Header

REST API

API (application programming interface) allows communication between two applications to retrieve or submit the data. There are two popular types of APIs – web and system API.

In this article, we will only look at the web API. There are two types of web API.

  • Request – Response API:  Rest, GraphQL, Remote Procedure Call (RPC)
  • Event-Driven API:  WebHooks, Web Sockets, HTTP Streaming

REST API falls under the request-response category. It makes use of HTTP methods such as GET, POST, and PUT to perform API operations.

A classic example is when a user sends a GET method to the web service to request for or retrieve a specific resource or a collection of resources. The server then sends back the specific resource or collection of resources back to the user who requested it.

Flask Framework

Flask is a framework based on python. It is a micro-framework used by python developers to build rest API.  It is called a micro framework because it allows developers, for instance, to add custom authentication and any other backend system based on preferences.

Let’s get it started with the implementation. My system setup is as follows.

  • Ubuntu as OS
  • Python 2.7+
  • Postman

Set up a virtual environment using virtualenv

We need to set up a virtual environment to ensure that some packages will not conflict with system packages. Let’s use the virtualenv to set up a new virtual environment.

Assuming you have the pip command available on your system, run the following command via pip to install.

pip install virtualenv

If you don’t have pip on your machine, then follow this documentation to install pip on your system.

Next, let’s create a directory to store or hold our virtual environment. Use the mkdir command shown below to create a directory

mkdir flaskproject

Change into the flaskproject directory using the following command

cd flaskproject

Inside the flaskproject directory, use the virtualenv tool to create a virtual environment as shown below:

virtualenv flaskapi

After you have used the virtualenv tool to create the virtual environment, run the cd command to change into the flaskapi directory as the virtual environment and activate it using the command below.

source bin/activate

Execute all tasks related to this project within the virtual environment.

Install packages using pip

Now it’s time to install packages such as the flask framework and PyJWT which we will use to build the rest API and other necessary packages for our API project.

Create a requirements.txt file with the following packages.

Flask  
datetime 
uuid
Flask-SQLAlchemy
PyJWT

Install them with pip.

pip install -r requirements.txt

Set up a database

Let’s install SQLite.

apt-get install sqlite3

Create a database named the library. Inside this database, we will create two tables, namely the Users and Authors table.

Users table will contain registered users. Only registered users can have access to the Authors table.

Authors table will store authors’ information or details such as the name of the author, country of birth and so on submitted by the registered users.

Create the database using the following command:

sqlite3 library.db

You can check whether you have successfully created the database by using the command below:

.databases

Open a new terminal and execute the following in the virtual environment we created earlier.

touch app.py

Paste the following code inside the file named app.py

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

The first line in the code above imports packages such as request and jsonify. We will make use of request to keep track of the request-level data during a request and use jsonify to output responses in a JSON format.

On the next line, we imported SQLAlchemy  from flask_sqlalchemy in order to integrate SQLAlchemy features into the flask.

From werkzeug.security, we imported generate_password_hash to generate password hash for users and check_password_hash to check the user’s password when comparing password submitted by users with users’ passwords stored in the database.

Finally, we imported uuid also known as universal unique identifiers to generate random id numbers for users.

Still, inside the app.py file, implement the configuration settings for the library API using the code below inside the app.py file.

Place the following code beneath the import statement.

app = Flask(__name__)

app.config['SECRET_KEY']='Th1s1ss3cr3t'
app.config['SQLALCHEMY_DATABASE_URI']='sqlite://///home/michael/geekdemos/geekapp/library.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)

Now create two models for the Users and Authors table as shown below. Copy and paste the code inside the app.py file.

Place the code below right beneath this database setting  db = SQLAlchemy(app)

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)
class Authors(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     name = db.Column(db.String(50), unique=True, nullable=False))
     book = db.Column(db.String(20), unique=True, nullable=False))
     country = db.Column(db.String(50), nullable=False))
     booker_prize = db.Column(db.Boolean)

Generate Users and Authors Tables

On the terminal, type the following code inside the virtual environment to generate or create tables for both the Users and Authors tables as shown below

from app import db
db.create_all()

Afterward, open the app.py file inside the virtual environment and create another function.

This function will generate tokens in order to allow only registered users to access and perform a set of  API operations against the Authors table.

Place this code beneath the database model for the Authors table

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])
         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

Create routes for the users table

Now let’s create a route to allow users to register for the Authors API via a username and password as shown below.

Again open the app.py file inside the virtual environment and paste the following code beneath the function token_required(f)

@app.route('/register', methods=['GET', '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'})

Inside the virtual environment, create another route in the app.py file to allow registered users to login.

When a user logs in, a random token is generated for the user to access the library API.

Paste the code below beneath the previous route we created.

@app.route('/login', methods=['GET', '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, {'WWW.Authentication': 'Basic realm: "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=30)}, app.config['SECRET_KEY'])  
     return jsonify({'token' : token.decode('UTF-8')}) 

  return make_response('could not verify',  401, {'WWW.Authentication': 'Basic realm: "login required"'})

Still, within the virtual environment, create another route in the app.py file to get or retrieve all registered users.

This code checks for all registered users in the Users table and returns the final result in a JSON format.

Paste the code below beneath the login route

@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})

Create routes for the authors table 

Let’s create routes for the Authors table to allow users to retrieve all authors in the database, as well as delete authors.

Only users with valid tokens can perform these API operations.

Inside the app.py file, create a route for registered users to create new authors.

Paste this code beneath the route which allows a user to retrieve all registered users.

@app.route('/author', methods=['POST', 'GET'])
@token_required
def create_author(current_user):
   
   data = request.get_json() 

   new_authors = Authors(name=data['name'], country=data['country'], book=data['book'], booker_prize=True, user_id=current_user.id)  
   db.session.add(new_authors)   
   db.session.commit()   

   return jsonify({'message' : 'new author created'})

Next, create another route to allow a registered user with a valid token to retrieve all authors in the Authors table as shown below.

Paste this code below the route which allows a user to create a new author.

@app.route('/authors', methods=['POST', 'GET'])
@token_required
def get_authors(current_user):

    authors = Authors.query.filter_by(user_id=current_user.id).all()

    output = []
    for author in authors:

           author_data = {}
           author_data['name'] = author.name
           author_data['book'] = author.book
           author_data['country'] = author.country
           author_data['booker_prize'] = author.booker_prize
           output.append(author_data)

     return jsonify({'list_of_authors' : output})

Finally, still inside the app.py file, create a route to delete a specified author as shown below.

Paste this code beneath the route which allows a user to retrieve a list of authors.

@app.route('/authors/<author_id>', methods=['DELETE'])
@token_required
def delete_author(current_user, author_id):  
    author = Author.query.filter_by(id=author_id, user_id=current_user.id).first()   
    if not author:   
       return jsonify({'message': 'author does not exist'})   


    db.session.delete(author)  
    db.session.commit()   

    return jsonify({'message': 'Author deleted'})


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

Afterward, save and close the app.py file inside the virtual environment.

Testing the library API with Postman

In this section, we will make use of a postman tool to send a request to the database services. If you don’t have a postman on your machine, you can find out how to download and install it here.

Apart from the postman, we can make use of other tools such as Curl to send requests to the server.

Open a new terminal and type the following:

postman

The command  postman will cause your web browser to display the page below:

postman_signup

You can decide to sign up and create  a free account but we will skip and get direct access to the app to test the library API as shown below:

Test the library api

 

In this section, we will allow a user to register for the library API by providing a username and a unique password in a JSON format using the POST method using the steps below:

  • Click on the tab labeled Body
  • Then select the raw button and choose the JSON format
  • enter a username and password to register as shown in the screenshot
  • Beside the send button, insert the following URL http://127.0.0.1/register
  • Finally, change the method to POST and press the send button.

a user registers for an api

It will display the following output as shown below:

Now we have successfully registered a user. Let’s go-ahead to allow the user who just registered to login in order to generate a temporary random token to access the Authors table using the following steps:

  •  Click on the authorization tab.
  • Under the type section, select basic authentication.
  • Then fill the username and password form with the username and password you registered with previously.
  • Finally, press the send button to login and generate a random token.

Once the user login successfully, a random token is generated for the user as shown in the screenshot.

We will make use of the generated random token to access the Authors table.

In this section, we will add an author’s information to the Authors table via the POST method using the following steps:

  • Click on the headers tab
  • Include the following HTTP headers shown in the screenshot

  • Next, click on the body tab and enter the details of the new author
  • Then press the send button to add the author’s details to the Author’s table

You can also retrieve authors’ information in the Authors table via the following:

  • Make sure your generated token is in the headers section. if it is not there, you need to fill it with your token.
  • Beside the send button, enter this URL http://127.0.0.1/authors
  • Then change the HTTP method to GET and press the send button to retrieve the authors details.

Finally, you can delete the author(s) in the Authors table via the DELETE method using the following steps:

  • Make sure your token is still in the headers section. You can check the headers tab to ensure the necessary information is in place.
  • Beside the send button, enter this URL http://127.0.0.1/sam
  • Then press the send button to delete the user you specified.

You can find the complete source code on Github.  You can clone it and check it out on your machine.