Using authentication decorators in Flask
Software Engineer
Has your team worked on an API and wanted 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, you will be able to make requests to the Flask API only for authenticated users. I’ll also show you how to test your endpoint security using pytest, and how to automate those tests in a continuous integration pipeline.
Prerequisites
To follow along with this tutorial, you will need:
- Working knowledge of Python, Flask, and virtual environments
- Git installed on your system
- A CircleCI account
- A GitHub account
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 you will be using for this tutorial is a simple book management API. You will use this API to create, read, and delete books.
The focus of this tutorial is not on the process of developing the API endpoints. Instead it is focused on how to use and configure authentication tokens in Flask. We won’t be looking too closely at the structure of the tokens or the various token configurations, such as when they expire or their composition, but you can learn more about the process of creating and configuring the tokens in the PyJWT docs.
Cloning the sample project
To clone the project, run this command in your terminal:
$ git clone https://github.com/CIRCLECI-GWP/authentication-decorators-in-flask.git
$ cd authentication-decorators-in-flask
Understanding Flask decorators
Before continuing on with the rest of the tutorial, it might be helpful to learn more about what a decorator is. 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.
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 you 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 by only 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 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 = 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 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:
- 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.
- Verifies that the username exists; if it does not, the user gets a prompt to sign up for an account.
- 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 theSECRET_KEY
. These are the same ingredients used when decoding the providedx-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).
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.
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
Create new book
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):
""""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":[]})
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):
"""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!')
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.
CircleCI will start executing the tests after building the Python image. Because you already have tests in the tests
directory, your pipeline will pass.
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!