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!

If you want to look at all the code needed to add API authentication in one place, you can do so here.

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 use
  • device_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!

Photo by NeONBRAND on Unsplash