Python REST API Authentication with JSON Web Tokens

In this post we’ll show you how to set up authentication for your Python REST API using JSON Web Tokens.

mike waites
Python Rest API Toolkit
5 min readDec 5, 2017

--

In our first post we introduced you to Arrested — a framework for rapidly building robust REST APIs on top of Flask. In this post we’ll show you how to authenticate all requests to your APIs using JSON Web Tokens.

We will be following on from our first post using Arrested’s middleware system to show how easy it is to extend your Arrested APIs. If you’re not using Arrested don’t worry, a lot of the code outlined in this tutorial is reusable.

Background

We’ve decided that requests to our API should be authenticated using JSON Web Tokens.

Here’s a breakdown of what we’re going to cover in this post.

  1. Create a new Client Model.
  2. Add our first piece of middleware to ensure that all requests are made with a valid ?api_key
  3. Add middleware & utility code for authenticating JWT’s pulled from the Authorization header.

Setting Up

👋 Quick Start. If you want to get started quickly you can use the arrested cookie-cutter template to create a project that includes all of the steps covered in the first post.

$ pip install cookiecutter
$ cookiecutter gh:mikeywaites/arrested-cookiecutter --checkout tutorials/post-1
project_name [Arrested Users API]: Star Wars
project_slug [star-wars]:
package_name [star_wars]:
$ cd star-wars/

We have included a database dump containing a few Characters. Simply copy the database file and name it using the package name you provided above.

$ cp post_1.db star_wars.db

Now test everything is working by starting the container and making a request.

$ docker-compose run --rm api flask db upgrade
$ docker-compose up -d api
$ curl -u admin:secret localhost:8080/v1/characters | python -m json.tool
{
"payload": [
{
"created_at": "2017-12-03T07:15:30.021280",
"id": 1,
"name": "Darth Vader",
"updated_at": "2017-12-03T07:15:30.021288"
},
{
"created_at": "2017-12-03T07:15:50.687444",
"id": 2,
"name": "Obe Wan Kenobe",
"updated_at": "2017-12-03T07:15:50.687458"
},
{
"created_at": "2017-12-03T07:15:59.472073",
"id": 3,
"name": "Hans Solo",
"updated_at": "2017-12-03T07:15:59.472086"
},
{
"created_at": "2017-12-03T07:16:12.972535",
"id": 4,
"name": "Luke Skywalker",
"updated_at": "2017-12-03T07:16:12.972551"
},
{
"created_at": "2017-12-03T07:16:42.004132",
"id": 5,
"name": "Princess Leah",
"updated_at": "2017-12-03T07:16:42.004159"
}
]
}

Let’s get started. 👌

1. Setting up the API Client Model

Add a new files models/client.py and add the code below. This model will store an API clients access credentials. The client_id is used to identify a Client. The secret_key is used to encode and decode JSON Web Tokens.

Now import the Client model in models/__init__.py

Now create and apply a database migration

$ docker-compose run api flask db revision "add client model"
$ docker-compose run api flask db upgrade

2. Identifying the API Client — Using Arrested’s middleware system

Flask comes with a great system for defining request middleware. Arrested builds on top of this system to allow more fine grained control of where and when your middleware is run.

Middleware can be applied at each level of the Arrested stack. You will often want a piece of middleware to be applied across every resource and every endpoint defined in an API. In our case authentication.

For those of you that followed along in the first post you might remember seeing some middleware in action already. The Arrested cookie cutter creates has middleware that authenticates requests using Basic Auth by default.

We now require that all requests are made with an ?api_key that we’ll use to identify the Client making request.

First we’ll add a few functions that will allow us to encode and decode the tokens.

Create a new file apis/v1/utils.py and add the following code.

Open up apis/v1/middleware.py and add the get_api_client_from_request method.

Now add the middleware to the before_all_hooks list on our ArrestedAPI object in apis/v1/__init__.py

Now let’s try making a request to the Characters API.

$ curl -u admin:secret localhost:8080/v1/characters | python -m json.tool{
"message": "Missing required ?api_key param"
}
$ curl -u admin:secret localhost:8080/v1/characters?api_key=foo | python -m json.tool{
"message": "API Client not found"
}

The first request was missing the api_key param. The second request failed because the api_key we provided was not valid. Let’s create some Client objects using the Flask shell command.

$ docker-compose run api flask shell>>> from star_wars.models import db, Client
>>> c1 = Client(name='My Client')
>>> c2 = Client(name='Other Client')
>>> db.session.add_all([c1, c2])
>>> db.session.commit()
>>> (c1.client_id, str(c1.secret_key))
('PeiBvLfjUZ5R9dZYiq62pe', 'e74b9d12-5464-4213-8e77-61cb384de1fa')
>>> (c2.client_id, str(c2.secret_key))
('jZXSc6kDxgfC5RWTLJ28GL', '8145a286-4b63-4ad6-abaa-a31a6e92d085')

Now make the request again using a valid api_key. Remember to replace the api_key value with the client_id of one of the Clients created above.

$ curl localhost:8080/v1/characters?api_key=YOUR_CLIENT_ID | python -m json.tool{
"payload": [
{
"created_at": "2017-12-03T07:15:30.021280",
"id": 1,
"name": "Darth Vader",
"updated_at": "2017-12-03T07:15:30.021288"
},
{
"created_at": "2017-12-03T07:15:50.687444",
"id": 2,
"name": "Obe Wan Kenobe",
"updated_at": "2017-12-03T07:15:50.687458"
},
{
"created_at": "2017-12-03T07:15:59.472073",
"id": 3,
"name": "Hans Solo",
"updated_at": "2017-12-03T07:15:59.472086"
},
{
"created_at": "2017-12-03T07:16:12.972535",
"id": 4,
"name": "Luke Skywalker",
"updated_at": "2017-12-03T07:16:12.972551"
},
{
"created_at": "2017-12-03T07:16:42.004132",
"id": 5,
"name": "Princess Leah",
"updated_at": "2017-12-03T07:16:42.004159"
}
]
}

3. Authenticating requests using JWTs.

In this section we’ll implement the middleware to authenticate requests using JSON Web Tokens.

Add the get_client_token function to apis/v1/middleware.py

Now add the middleware to our v1 ArrestedAPI object just like we did in step 2. We also removed the basic_auth middleware.

Now make a request to the API.

$ curl localhost:8080/v1/characters?api_key=YOUR_CLIENT_ID | python -m json.tool{
"message": "Request does not contain token"
}

We’ve got an error telling us that the request does not contain a token. At this point your API users would produce a token using one of the many libraries available for their programming language. For the purposes of illustration, we’ll use the encode_client_token util we added above.

$ docker-compose run api flask shell
$ from star_wars.models import Client
$ from star_wars.apis.v1.utils import encode_client_token
$ c1 = Client.query.get(1)
$ encode_client_token(c1)
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MTQ5MTAxMzEsImlhdCI6MTUxMjMxODEzMSwibmJmIjoxNTEyMzE4MTMxLCJhdWQiOiIxIn0.9eijEvvWU90SzNfplI4wziXDy1UglxRfWR1zPuu1yJk'

Now let’s try the request again this time providing our token in the Authorization header.

curl -H "Authorization: Bearer YOUR_TOKEN" localhost:8080/v1/characters?api_key=YOUR_CLIENT_ID | python -m json.tool{
"payload": [
{
"created_at": "2017-12-03T07:15:30.021280",
"id": 1,
"name": "Darth Vader",
"updated_at": "2017-12-03T07:15:30.021288"
},
{
"created_at": "2017-12-03T07:15:50.687444",
"id": 2,
"name": "Obe Wan Kenobe",
"updated_at": "2017-12-03T07:15:50.687458"
},
{
"created_at": "2017-12-03T07:15:59.472073",
"id": 3,
"name": "Hans Solo",
"updated_at": "2017-12-03T07:15:59.472086"
},
{
"created_at": "2017-12-03T07:16:12.972535",
"id": 4,
"name": "Luke Skywalker",
"updated_at": "2017-12-03T07:16:12.972551"
},
{
"created_at": "2017-12-03T07:16:42.004132",
"id": 5,
"name": "Princess Leah",
"updated_at": "2017-12-03T07:16:42.004159"
}
]
}

Conclusion

Thanks for sticking with me until the end 😅. Using Arrested’s middleware we switched out authentication across the API with ease. We demonstrated how simple it is to progressively validate requests to your API by applying middleware. You can read all the options Arrested has to offer in depth here.

I’d encourage you to to try and expand on what’s been covered here. How would you securely make requests on behalf of users or how would you limit actions against an API to an admin user?

I hope you’ve enjoyed the post and as always we’re really keen on hearing any feedback you have. Don’t forget to 👏

Until next week 👋

--

--

mike waites
Python Rest API Toolkit

Head of Engineering @oldstlabs , Drummer for @SKETSLONDON, cycling NUTTER,