Chapter 7. Improving and Adding Authentication to an API with Flask
In this chapter, we will improve the RESTful API that we started in the previous chapter and we will add authentication related security to it. We will:
Improve unique constraints in the models
Update fields for a resource with the PATCH
method
Code a generic pagination class
Add pagination features to the API
Understand the steps to add authentication and permissions
Add a user model
Create a schema to validate, serialize and deserialize users
Add authentication to resources
Create resource classes to handle users
Run migrations to generate the user table
Compose requests with the necessary authentication
Improving unique constraints in the models
When we created the Category
model, we specified the True
value for the unique argument when we created the db.Column
instance named name
. As a result, the migrations generated the necessary unique constraint to make sure that the name
field has unique values in the category
table. This way, the database won't allow us to insert duplicate values for category.name
. However, the error message generated when we try to do so is not clear.
Run the following command to create a category with a duplicate name. There is already an existing category with the name equal to 'Information'
:
http POST :5000/api/categories/ name='Information'
The following is the equivalent curl
command:
curl -iX POST -H "Content-Type: application/json" -d '{"name":"Information"}'
:5000/api/categories/
The previous command will compose and send a POST
HTTP request with the specified JSON key-value pair. The unique constraint in the category.name
field won't allow the database...
Updating fields for a resource with the PATCH method
As we explained in Chapter 6
, Working with Models, SQLAlchemy, and Hyperlinked APIs in Flask, our API is able to update a single field for an existing resource, and therefore, we provide an implementation for the PATCH
method. For example, we can use the PATCH
method to update an existing message and set the value for its printed_once
and printed_times
fields to true
and 1
. We don't want to use the PUT
method because this method is meant to replace an entire message. The PATCH
method is meant to apply a delta to an existing message, and therefore, it is the appropriate method to just change the value of those two fields.
Now, we will compose and send an HTTP request to update an existing message, specifically, to update the value of the printed_once
and printed_times
fields. Because we just want to update two fields, we will use the PATCH
method instead of PUT
. Make sure you replace 1
with the id or primary key of an existing message in...
Coding a generic pagination class
Our database has a few rows for each of the tables that persist the models we have defined. However, after we start working with our API in a real-life production environment, we will have hundreds of messages, and therefore, we will have to deal with large result sets. Thus, we will create a generic pagination class and we will use it to easily specify how we want large results sets to be split into individual pages of data.
First, we will compose and send HTTP requests to create 9
messages that belong to one of the categories we have created: Information
. This way, we will have a total of 12 messages persisted in the database. We had 3 messages and we add 9 more.
http POST :5000/api/messages/ message='Initializing light controller' duration=25 category="Information"
http POST :5000/api/messages/ message='Initializing light sensor' duration=20 category="Information"
http POST :5000/api/messages/ message='Checking pressure sensor' duration=18 category="Information...
Adding pagination features
Open the api/views.py
file and replace the code for the MessageListResource.get
method with the highlighted lines in the next listing. In addition, make sure that you add the import statement. The code file for the sample is included in the restful_python_chapter_07_01
folder:
from helpers import PaginationHelper
class MessageListResource(Resource):
def get(self):
pagination_helper = PaginationHelper(
request,
query=Message.query,
resource_for_url='api.messagelistresource',
key_name='results',
schema=message_schema)
result = pagination_helper.paginate_query()
return result
The new code for the get
method creates an instance of the previously explained PaginationHelper
class named pagination_helper
with the request
object as the first argument. The named arguments specify the query
, resource_for_url
, key_name
, and schema
that the PaginationHelper
instance has to...
Understanding the steps to add authentication and permissions
Our current version of the API processes all the incoming requests without requiring any kind of authentication. We will use a Flask extension and other packages to use an HTTP authentication scheme to identify the user that originated the request or the token that signed the request. Then, we will use these credentials to apply the permissions that will determine whether the request must be permitted or not. Unluckily, neither Flask nor Flask-RESTful provides an authentication framework that we can easily plug and configure. Thus, we will have to write code to perform many tasks related to authentication and permissions.
We want to be able to create a new user without any authentication. However, all the other API calls are only going to be available for authenticated users.
First, we will install a Flask extension to make it easier for us to work with HTTP authentication, Flask-HTTPAuth
, and a package to allow us to hash a password...
Now, we will create the model that we will use to represent and persist the user. Open the api/models.py
file and add the following lines after the declaration of the AddUpdateDelete
class. Make sure that you add the import statements. The code file for the sample is included in the restful_python_chapter_07_02
folder:
from passlib.apps import custom_app_context as password_context
import re
class User(db.Model, AddUpdateDelete):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
# I save the hashed password
hashed_password = db.Column(db.String(120), nullable=False)
creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False)
def verify_password(self, password):
return password_context.verify(password, self.hashed_password)
def check_password_strength_and_hash_if_ok(self, password):
if len(password) < 8:
...
Creating a schemas to validate, serialize, and deserialize users
Now, we will create the Flask-Marshmallow schema that we will use to validate, serialize and deserialize the previously declared User
model. Open the api/models.py
file and add the following code after the existing lines. The code file for the sample is included in the restful_python_chapter_07_02
folder:
class UserSchema(ma.Schema):
id = fields.Integer(dump_only=True)
name = fields.String(required=True, validate=validate.Length(3))
url = ma.URLFor('api.userresource', id='<id>', _external=True)
The code declares the UserSchema
schema, specifically a subclass of the ma.Schema
class. Remember that the previous code we wrote for the api/models.py
file created a flask_marshmallow.Mashmallow
instance named ma
.
We declare the attributes that represent fields as instances of the appropriate class declared in the marshmallow.fields
module. The UserSchema
class declares the name
attribute as an instance of fields...
Adding authentication to resources
We will configure the Flask-HTTPAuth
extension to work with our User
model to verify passwords and set the authenticated user associated with a request. We will declare a custom function that this extension will use as a callback to verify a password. We will create a new base class for our resources that will require authentication. Open the api/views.py
file and add the following code after the last line that uses the import
statement and before the lines that declares the Blueprint
instance . The code file for the sample is included in the restful_python_chapter_07_02
folder:
from flask_httpauth import HTTPBasicAuth
from flask import g
from models import User, UserSchema
auth = HTTPBasicAuth()
@auth.verify_password
def verify_user_password(name, password):
user = User.query.filter_by(name=name).first()
if not user or not user.verify_password(password):
return False
g.user = user
return True
class AuthRequiredResource...
Creating resource classes to handle users
We just want to be able to create users and use them to authenticate requests. Thus, we will just focus on creating resource classes with just a few methods. We won't create a complete user management system.
We will create the resource classes that represent the user and the collection of users. First, we will create a UserResource
class that we will use to represent a user resource. Open the api/views.py
file and add the following lines after the line that creates the Api
instance. The code file for the sample is included in the restful_python_chapter_07_02
folder:
class UserResource(AuthRequiredResource):
def get(self, id):
user = User.query.get_or_404(id)
result = user_schema.dump(user).data
return result
The UserResource
class is a subclass of the previously coded AuthRequiredResource
and declares a get
methods that will be called when the HTTP
method with the same name arrives as a request on the represented resource...
Running migrations to generate the user table
Now, we will run many scripts to run migrations and generate the necessary table in the PostgreSQL database. Make sure you run the scripts in the terminal or the Command Prompt window in which you have activated the virtual environment and that you are located in the api
folder.
Run the first script that populates the migration script with the detected changes in the models. In this case, it is the second time we populate the migration script, and therefore, the migration script will generate the new table that will persist our new User
model: model
:
python migrate.py db migrate
The following lines show the sample output generated after running the previous script. Your output will be different according to the base folder in which you have created the virtual environment.
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added...
Composing requests with the necessary authentication
Now, we will compose and send an HTTP request to retrieve the first page of the messages without authentication credentials:
http POST ':5000/api/messages/?page=1'
The following is the equivalent curl
command:
curl -iX GET ':5000/api/messages/?page=1'
We will receive a 401 Unauthorized
status code in the response header. The following lines show a sample response:
HTTP/1.0 401 UNAUTHORIZED
Content-Length: 19
Content-Type: text/html; charset=utf-8
Date: Mon, 15 Aug 2016 01:16:36 GMT
Server: Werkzeug/0.11.10 Python/3.5.1
WWW-Authenticate: Basic realm="Authentication Required"
If we want to retrieve messages, that is, to make a GET
request to /api/messages/
, we need to provide authentication credentials using HTTP authentication. However, before we can do this, it is necessary to create a new user. We will use the new user to test our new resource classes related to users and our changes in the permissions policies.
http POST :5000/api/users...
In this chapter, we improved the RESTful API in many ways. We added user friendly error messages when resources aren't unique. We tested how to update single or multiple fields with the PATCH
method and we created our own generic pagination class.
Then, we started working with authentication and permissions. We added a user model and we updated the database. We made many changes in the different pieces of code to achieve a specific security goal and we took advantage of Flask-HTTPAuth and passlib to use HTTP authentication in our API.
Now that we have built an improved a complex API that uses pagination and authentication, we will use additional abstractions included in the framework and we will code, execute, and improve unit test, which is what we are going to discuss in the next chapter.