This tutorial covers:

  1. Understanding Flask decorators
  2. Writing an authentication decorator
  3. Setting up and testing endpoints

Has your team worked on an API and wanted (somehow) to implement more powerful security features? If you are dissatisfied with the level of security in an API, there are solutions for improving it! In this tutorial, I will lead you through the process of creating API endpoints that are secured with authentication tokens. Using these endpoints, we will be able to make requests to the Flask API only for authenticated users.

Prerequisites

To follow along with this tutorial, you will need:

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

Using the Flask API sample project

The application we will be using for this tutorial is a simple book management API. We will use this API to create, read, and delete books. In this tutorial we will not focus on the process of developing the API endpoints but on the process of ensuring that the endpoints are secured by enforcing use of authentication tokens. It is important to note that this tutorial is focused on how to use and configure authentication tokens in Flask and not on the structure of the tokens or the various token configurations, such as when they expire or their composition. For additional details on the process of creating and configuring the tokens, you can read more in the PyJWT docs.

Cloning the sample project

To clone the project, run this command in your terminal:

$ git clone https://github.com/mwaz/flask-authentication-decorators.git

$ cd flask-authentication-decorators

Now you have access to the codebase I will be referring to in the rest of the tutorial. Before we get started though, let me explain what decorators are.

Understanding Flask decorators

A decorator is a function that takes in another function as a parameter and then returns a function. This is possible because Python gives functions special status. A function can be used as a parameter and a return value, while also being assigned to a variable. In other words, a decorator will always extend the behavior of a function without modifying the behavior of that function.

An example of a Flask decorator that you have probably used is the @app.route(‘/’) for defining routes. When displaying the output to a browser, this decorator converts a function into a route that can be accessed by the browser without having to explicitly invoke the function in the program.

Authentication Decorator call

This workflow diagram shows how a decorator function is executed and how it enforces a requirement before the request can proceed or a response is returned.

Setting up authentication decorators on a Flask API

Now we can explore how to use decorators for authentication.

Endpoints must be authenticated before they are allowed to make requests in an application. Authentication means that the endpoint has an existing session and is unique to a specific user.

You can have additional roles for authenticated users, like an admin role with elevated privileges. This way, a decorator can lock certain resources that should be accessed only by one type of user (an admin, for example). Think of it like a bank. All employees are allowed into the bank, but not all of them have the privileges to authorize a transaction.

For example, the @login_required decorator will execute every time the route is called. Here is a code snippet that shows how it is done:

# Decorator usage on an endpoint
@app.route("/bookapi/books/<int:book_id>") 
@login_required
def add_book():
      # Adding a book requires that the endpoint
      # is authenticated
	return book[book_id]

The decorator @login_required is used to secure the bookapi/books/:bookid route in a the Flask application. It enforces the rule that the client has to either authenticate with a valid logged-in user or have an existing token. For the add_book() method to be executed, the request will first have to go through the defined decorator for verification that it has the required access permissions.

Note: Decorators can assist in the implementation of DRY code principles, such as accepting input from a query, JSON, or form request with type checking. In this case, an error is provided if the input to the decorator is incorrect, or if it is missing required inputs.

Writing an authentication decorator

Using the API you cloned earlier, you can generate our own custom decorator. This decorator will enforce authorization by requiring users to provide an authentication token, provided by PyJWT. The token generated will allow access for the creation and viewing of books in our endpoint only by registered and logged-in users.

This might seem complicated, but we can take it step by step. Start by creating your authentication decorator using this code snippet:

# Authentication decorator
def token_required(f):
    @wraps(f)
    def decorator(*args, **kwargs):
        token = None
        # ensure the jwt-token is passed with the headers
        if 'x-access-token' in request.headers:
            token = request.headers['x-access-token']
        if not token: # throw error if no token provided
            return make_response(jsonify({"message": "A valid token is missing!"}), 401)
        try:
           # decode the token to obtain user public_id
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            current_user = User.query.filter_by(public_id=data['public_id']).first()
        except:
            return make_response(jsonify({"message": "Invalid token!"}), 401)
         # Return the user information attached to the token
        return f(current_user, *args, **kwargs)
    return decorator

Note: This code snippet is located in library/models.py in the cloned repository.

This code snippet first checks that we have a token with the name x-access-token in our request header. If not, it sends a response that a valid token is missing from the request.

When a valid token header is present in the request, it is decoded using our application SECRET_KEY. Then, the public_id of the user associated with the decoded data is fetched. This is what is returned by the decorator.

Creating authentication tokens

Now you have a decorator to ensure that your restricted endpoints have a valid access token before making the request. Next we need to work on utilizing the created decorators in our endpoints. There are two questions we need to answer to complete this task:

  • How is a token generated?
  • How is a decorator called on a request method?

How to create access tokens

The login process is one of the most important considerations when determining whether a token is valid. The token generated on the login process is the same one that will be received when our decorator method decodes it.

Here is what you need to create the token:

# Login method responsible for generating authentication tokens
@app.route('/login', methods=['POST'])
def login():
    auth = request.get_json()
    if not auth or not auth.get('username') or not auth.get('password'):
        return make_response('Could not verify!', 401, {'WWW-Authenticate': 'Basic-realm= "Login required!"'})

    user = User.query.filter_by(username=auth['username']).first()
    if not user:
        return make_response('Could not verify user, Please signup!', 401, {'WWW-Authenticate': 'Basic-realm= "No user found!"'})

    if check_password_hash(user.password, auth.get('password')):
        token = jwt.encode({'public_id': user.public_id}, app.config['SECRET_KEY'], 'HS256')
        return make_response(jsonify({'token': token}), 201)

    return make_response('Could not verify password!', 403, {'WWW-Authenticate': 'Basic-realm= "Wrong Password!"'})

What this code snippet does:

  1. Verifies that both the username and the password have been passed on with the request. If they have not, an error is returned to the user.
  2. Verifies that the username exists; if it does not, the user gets a prompt to sign up for an account.
  3. Checks that the hashed password stored in the database is the same as the hash of the password that has been provided. If this is the case, a token is created by encoding the public_id of the user and the SECRET_KEY. These are the same ingredients that used when decoding the provided x-access-token from the request.

If the username and password match values already stored in the database, the code generates an access token you can use in the next steps to access the API resources.

Note: We can only compare password hashes because with the same hashing algorithms, two hashed texts will always have the same hash result. On the other hand, it would require too much compute power to decode the hashed password text to revert it back to a plain password text and compare it with an actual password (which with good algorithms is close to impossible).

Authentication Flow

This flowchart shows that once the authentication process is successful, a token is created and sent back to the user. This is the token that is used to access the restricted endpoints.

You can verify this flow by making CURL requests to both the signup and login endpoints. Then make sure you can create a new user and use the details to log the user in and get a token.

Creating a user

Authenticating a user

Hurrah, you have a token! Now you can put it into use.

Using the authentication decorator in API endpoints

In the previous steps, you have created a token and also decoded the token to get user information for the authentication decorator. Now you can start the process of integrating the authentication decorator into your API endpoints.

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

This code snippet makes the API request to the bookapi/books/:bookid endpoint and calls the @token_required decorator. Once the decorator is called, it will verify that the request has a valid x-access-token token in the request header. If the passed token in the request is valid, a response of a successful request for a deleted book is sent.

Just like that, you have an authenticated endpoint that can only be accessed by a user with a valid token! You can again verify this by making a CURL request to the API endpoint to create and obtain created books.

Creating and fetching books

Note: Most tokens are created in the JWT (JSON Web Tokens) format. JWT is an open standard used to share security information between two parties, like between client and server.

Testing endpoints that require authentication tokens

As the saying goes, development is never complete without testing the code. Our next step is to create tests for the endpoints that are using the authentication decorator. We will use pytest to create the tests.

Note: The tests can be found in the root directory of the cloned project under tests/test_book_api.py file.

Start by defining a method that logs in a user every time we execute our tests to ensure there is a valid token. This code snippet shows how:

# Method in test file to generate a user token
def getLoginToken(self):
        """"Method to get a login token
        """
        user_register = self.client().post('/signup', data=self.user_details, content_type="application/json")
        self.assertEqual(user_register.status_code, 201)
        user_login = self.client().post('/login', data=self.user_details, content_type="application/json")
        self.assertEqual(user_login.status_code, 201)
        token = ast.literal_eval(user_login.data.decode())
        return token['token']

In this code snippet, we are using pytest’s app.client() method to make a request to our API to create a user and log in with the same user to obtain the authentication token. Once we have the token, we can now use it to make requests in our tests.

Note: To ensure that data is not corrupted, we always drop the test DB and add a new user with every test run. This is handled by the test_db as it would be an anti-practice to use the test database as the staging/development database.

Once we have the token, making API requests in the tests is simple. Just make the request via your test client and pass the token in the header request. This code snippet shows how:

# Test to ensure that a user can get all books from the app
def test_user_logged_in_user_can_get_books(self):
      """"Method to test fetching books with logged in user
      """
      logintoken = getLoginToken(self) # Get a login token from our method
      headers = {
          'content-type': "application/json",
          'x-access-token': logintoken # pass the token as a header
      }
      fetch_books = self.client().get('/bookapi/books', data=self.user_details, content_type="application/json", headers=headers)
      response = fetch_books.data.decode()
      self.assertEqual(fetch_books.status_code, 200)
      self.assertEqual(ast.literal_eval(response), {"Books":[]})

In the above code snippet, we first get a login token and then make a request to the /bookapi/books endpoint. We then assert that the response code is 200 and that the response is an empty list of the books since we have not created any books yet.

At the same time, we can also test that a user without a valid token cannot access the books endpoint. Let’s see this in action in our code snippet below.

# Test to ensure that a user without a valid token cannot access the books endpoint
def test_user_without_valid_token_cannot_get_books(self):
        """Method to check errors with invalid login
        """
        headers = {
            'content-type': "application/json",
            'x-access-token': 'invalid-token'
        }
        fetch_books = self.client().get('/bookapi/books', data=self.user_details, content_type="application/json", headers=headers)
        response = fetch_books.data.decode()
        self.assertEqual(fetch_books.status_code, 401)
        self.assertEqual(ast.literal_eval(response)['message'], 'Invalid token!')

In the code snippet below, rather than pass in a valid token to x-access-token in the headers to be passed on to /bookapi/books endpoint, we pass in an invalid token which then results to a 401 error due to the token being invalid.

To verify that the above tests are working, we can execute the tests by running this command in the terminal:

pipenv run pytest 

To learn and get more examples of our tests for the book management API, you can check the full implementation on the tests/test_book_api.py file in our cloned repository.

Now that we know how to create authentication tokens, decode them with decorators and also use them in our API endpoints, we need a proper way to run our tests in the cloud. An easier way is to run them locally using the pytest command, but we want to show them to the world. To achieve this, we will need to integrate our tests with a CI/CD tool, which in our case will be CircleCI.

CircleCI gives us the confidence that our tests work and this raises the level of trust, especially when we are doing continuous integration and continous deployment.

Setting up CircleCI

To set up CircleCI, commit your work and push your changes to GitHub.

Note: If you have already cloned the project repository, you can skip this part of the tutorial. I have added the steps here if you want to learn how to set up your own project.

Log in to CircleCI and go to the Projects dashboard. You can choose the repository that you want to set up from a list of all the GitHub repositories associated with your GitHub username or your organization. The project for this tutorial is named flask-authentication-decorators. On the Projects dashboard, select the option to set up the project you want. Choose the option to use an existing configuration. Then select the option to start the building process on the prompt.

Start building prompt

Note: After initiating the build, expect your pipeline to fail. You still need to add the customized .circleci/config.yml configuration file to GitHub for the project to build properly.

Writing the CI pipeline

Create a .circleci directory in your root directory and then add a config.yml file. The config file holds the CircleCI configuration for every project. Use this code:

version: 2.1
orbs:
  python: circleci/python@1.2

workflows:
  build-app-with-test:
    jobs:
      - build-and-test
jobs:
  build-and-test:
    docker:
      - image: cimg/python:3.9
    steps:
      - checkout
      - python/install-packages:
          pkg-manager: pipenv
      - run:
          name: Run tests
          command: pipenv run pytest

In the configuration above, CircleCI uses a python version of 3.9. We are using the pipenv package manager to install the dependencies. We are also using the pytest package to run our tests. Once all the dependencies have been installed, and the environment is set up, execute using the pipenv run pytest command.

After adding this configuration, push your changes to GitHub. CircleCI will start executing the tests after building the Python image. Because you already have tests in the tests directory, the last command will execute those tests, and your pipeline will pass.

Successful test run

Voila! You have successfully set up your CircleCI pipeline to run tests in the cloud. This brings our authentication decorators journey with Flask almost to an end, at least for now.

Conclusion

Congratulations to you on getting this far! In this tutorial, we have learned how to create custom authentication decorators and use them in our API to receive and decode JWT tokens from registered users in the login process. We also learned how to pass authentication tokens to the API using headers, expecting that the request received by the application has the same headers. Finally, we learned how we can enforce authentication by ensuring that endpoints are protected with an authentication decorator. As a bonus, you can show your team how to set up CircleCI and execute automated tests. If you have yet to adopt continous integration and continous deployment for your applications, this is a great time to do so.

I hope you enjoyed this tutorial as much as I did writing it. Until next time!


Waweru Mwaura is a software engineer and a life-long learner who specializes in quality engineering. He is an author at Packt and enjoys reading about engineering, finance, and technology. You can read more about him on his web profile.