TutorialsLast Updated Apr 23, 202511 min read

Using authentication decorators in Flask

Waweru Mwaura

Software Engineer

Developer C sits at a desk working on an intermediate-level project.

In this tutorial, I’ll guide you step-by-step through creating API endpoints secured with authentication tokens. With these endpoints, only authenticated users will be able to make requests to your Flask API. You’ll also learn how to test your endpoint security using pytest and automate those tests in a continuous integration pipeline.

Prerequisites

Before diving in, make sure you have the following:

Using Flask API sample project

For this tutorial, you’ll work with a simple book management API. This API allows you to create, read, and delete books.

While the focus here isn’t on building the API endpoints themselves, you’ll learn how to configure and use authentication tokens in Flask. We won’t dive deeply into token structures or configurations like expiration times, but you can explore more in the PyJWT docs.

Cloning the sample project

To get started, clone the project by running the following commands in your terminal:

$ git clone https://github.com/CIRCLECI-GWP/authentication-decorators-in-flask.git

$ cd authentication-decorators-in-flask

Understanding Flask decorators

Before moving forward, let’s take a moment to understand decorators. A decorator is a function that takes another function as input and returns a new function. This is possible because Python treats functions as first-class objects, meaning they can be passed as arguments, returned from other functions, and assigned to variables. In essence, decorators extend the behavior of a function without altering its core functionality.

A common example of a Flask decorator is @app.route('/'), which defines routes. This decorator transforms a function into a route that can be accessed via a browser without explicitly calling the function in your code.

Authentication decorator call

The diagram above illustrates how a decorator function operates, enforcing specific requirements before a request proceeds or a response is returned.

Setting up authentication decorators in a Flask API

Now, let’s explore how to use decorators for authentication.

Endpoints in your application must be authenticated before processing requests. Authentication ensures that the endpoint is tied to a unique user session.

You can also assign roles to authenticated users, such as an admin role with elevated privileges. For instance, a decorator can restrict access to certain resources based on user roles. Think of it like a bank: while all employees can enter the building, only a select few can authorize transactions.

For example, the @login_required decorator is triggered every time a route is accessed. Here’s how it works:

# 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 your 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 your endpoint only by registered and logged-in users.

This might seem complicated, but you 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 = request.headers.get('x-access-token')
        if not token:  # Throw error if no token provided
            return make_response(jsonify({"message": "A valid token is missing!"}), 401)
        try:
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            current_user = User.query.filter_by(public_id=data.get('public_id')).first()
            if not current_user:  # Ensure the user exists
                return make_response(jsonify({"message": "User not found!"}), 404)
        except jwt.ExpiredSignatureError:
            return make_response(jsonify({"message": "Token has expired!"}), 401)
        except jwt.InvalidTokenError:
            return make_response(jsonify({"message": "Invalid token!"}), 401)

        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 you have a token with the name x-access-token in your 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 your 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 you need to work on using the decorators in your endpoints. There are two questions you 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 log-in 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 your 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 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: You can compare password hashes only 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

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 verify this by making a CURL request to the API endpoint to create and obtain created books.

Add token

Add token to the header

Create new book

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

Development is never complete without testing the code. Your next step is to create tests for the endpoints that are using the authentication decorator. You will be using pytest to create the tests.

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

Start by defining a method that logs in a user every time you execute your 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, you are using pytest’s app.client() method to make a request to your API to create a user and log in with the same user to obtain the authentication token. Once you have the token, you can now use it to make requests in your tests.

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

Once you 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) -> None:
        """Test fetching books with a logged-in user."""
        login_token = get_login_token(self.client, self.user_details)
        headers = {
            'Content-Type': "application/json",
            'x-access-token': login_token
        }
        fetch_books = self.client.get('/bookapi/books', headers=headers)
        response = json.loads(fetch_books.data.decode())

        self.assertEqual(fetch_books.status_code, 200)
        self.assertEqual(response, {"Books": []})

This code first gets a login token and then makes a request to the /bookapi/books endpoint. It then asserts that the response code is 200 and that the response is an empty list of the books. The list is empty because you have not created any books yet.

At the same time, you can also test that a user without a valid token cannot access the books endpoint. Use this code snippet:

# 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) -> None:
        """Test fetching books with an invalid token."""
        headers = {
            'Content-Type': "application/json",
            'x-access-token': 'invalid-token'
        }
        fetch_books = self.client.get('/bookapi/books', headers=headers)
        response = json.loads(fetch_books.data.decode())

        self.assertEqual(fetch_books.status_code, 401)
        self.assertEqual(response['message'], 'Invalid token!')

Rather than pass in a valid token to x-access-token in the headers to be passed on to /bookapi/books endpoint, the code passes in an invalid token. This results in a 401 error.

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

pipenv run pytest

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

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

CircleCI gives you the confidence that your tests work and raises the level of trust, especially when you are have a continuous integration and continous deployment practice in place.

Writing the CI pipeline

To set up CircleCI, 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@2.1.1

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 this configuration, CircleCI uses a Python version of 3.9 and the pipenv package manager to install the dependencies. You are using the pytest package to run your 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, commit your work and push your changes to GitHub.

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 authentication-decorators-in-flask.

On the Projects dashboard, select the option to set up the project you want. Choose the option to use the configuration file in your repository. Then click Set Up Project to start the building process.

Start building prompt

CircleCI will start executing the tests after building the Python image. Because you already have tests in the tests directory, your pipeline will pass.

Successful test run

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

Conclusion

Congratulations to you on getting this far. In this tutorial, you have learned how to create custom authentication decorators and use them in your API to receive and decode JWT tokens from registered users in the login process. You 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, you learned how you 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 continuous integration and continuous delivery (CI/CD) for your applications, this is a great time to get started.

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

Copy to clipboard