How to Add API Key Authentication to a Flask app
An API key is similar to a password, and is usually given to non-human users of your API. Whenever they make a request to your API they'll send the API key, and that authenticates and identifies them.
In this post, let me show you how to add API key authentication to your Flask app! We will use the same libraries as we do in our REST APIs with Flask and Python course:
- Flask
- SQLAlchemy
- Flask-RESTful
- Flask-JWT
If you'd like to use Flask-RESTX and Flask-JWT-Extended instead, the changes required are minimal!
How to generate and store API keys in your database
Creating the DeviceModel
Since we're using SQLAlchemy, the first step should be to decide how we want to store data about our "non-human users" and the API keys that we've given them.
Let's start by creating a model, which I'll call DeviceModel
, to store said data. In the application, I'll refer to "non-human users" as "devices", so it's simpler.
This model will have:
id
, which is a unique auto-incrementing identifier, for internal usedevice_name
, a descriptive string for each device.device_key
, the API key that each device will be given. They can use this to make requests to some of our API endpoints.user_id
, a one-to-many relationship with users, so we know which devices are owned by which users.
I'll also add a few helper methods to the model so it's easier to interact with from our views later on:
from db import db
import uuid
class DeviceModel(db.Model):
__tablename__ = 'devices'
id = db.Column(db.Integer, primary_key=True)
device_name = db.Column(db.String(80))
device_key = db.Column(db.String(80))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
user = db.relationship('UserModel', back_populates="devices")
def __init__(self, device_name, user_id, device_key=None):
self.device_name = device_name
self.user_id = user_id
self.device_key = device_key or uuid.uuid4().hex
def json(self):
return {
'device_name': self.device_name,
'device_key': self.device_key,
'user_id': self.user_id
}
@classmethod
def find_by_name(cls, device_name):
return cls.query.filter_by(device_name=device_name).first()
@classmethod
def find_by_device_key(cls, device_key):
return cls.query.filter_by(device_key=device_key).first()
def save_to_db(self):
db.session.add(self)
db.session.commit()
def delete_from_db(self):
db.session.delete(self)
db.session.commit()
Linking devices to users
We added a relationship
to our DeviceModel
, so now it's time we do the same on the other side of the relationship:
class UserModel(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80))
password = db.Column(db.String(80))
+ devices = db.relationship('DeviceModel', back_populates="user")
An API endpoint to register a new device and API key
The last piece of the puzzle is to allow users to create new devices, each with an API key.
To do so, we'll add a Flask-RESTful Resource
with a post()
method that can be called by the user with a device name. It will also require the user authenticates themselves with a JWT:
from flask_restful import Resource, reqparse
from flask_jwt import jwt_required, current_identity
from models.device import DeviceModel
class AddDevice(Resource):
parser = reqparse.RequestParser()
parser.add_argument(
'device_name',
type=str,
required=True
)
@jwt_required()
def post(self):
data = AddDevice.parser.parse_args()
name = data["device_name"]
if DeviceModel.find_by_name(name):
return {'message': f"A device with name '{name}' already exists."}, 400
new_device = DeviceModel(
device_name=name,
user_id=current_identity.id
)
new_device.save_to_db()
return {"api_key": new_device.device_key}, 201
We will also need to register this Resource
with our Flask app, so that the endpoint is generated and can be accessed. In app.py
:
+from resources.device import AddDevice
...
+api.add_resource(AddDevice, '/user/add-device')
To add a new device, human users will have to make a request to /user/add-device
with a JSON body like the below and a valid JWT Authorization header:
{
"device_name": "New Device Example"
}
And they will get a response like this:
{
"api_key": "ef229daa-d058-4dd4-9c93-24761842aec5"
}
How to require an API key in certain Flask endpoints
Now that authenticated users can create a new device and get an API key, we can create Flask endpoints that allow authentication only with the API key, instead of a JWT (which is reserved for human users).
You could start by adding a decorator like this one in security.py
:
from models.device import DeviceModel
import functools
from hmac import compare_digest
from flask import request
def is_valid(api_key):
device = DeviceModel.find_by_device_key(api_key)
if device and compare_digest(device.device_key, api_key):
return True
def api_required(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
if request.json:
api_key = request.json.get("api_key")
else:
return {"message": "Please provide an API key"}, 400
# Check if API key is correct and valid
if request.method == "POST" and is_valid(api_key):
return func(*args, **kwargs)
else:
return {"message": "The provided API key is not valid"}, 403
return decorator
It checks whether the API key exists, and also whether the request method is "POST". We are assuming that API keys can only be used for POST
requests, but feel free to remove that check if it's not a restriction you want to place in your application.
When an endpoint should require an API key, just decorate it with the @api_required
decorator, just like how we use @jwt_required()
in some endpoints. Then users will have to include a JSON body in their requests like this one:
{
"api_key": "ef229daa-d058-4dd4-9c93-24761842aec5"
}
Conclusion and extras
That's everything for this post! I hope you've enjoyed it and that it helps your REST APIs! If you want to learn more about REST API development with Flask and Python, check out our complete course.
Thanks to Leo Spairani for writing most of the code in this post!