JWT Authentication in Flask APIs Setting up JWTs

Learning objective: By the end of this lesson, students will be able to explain how JSON Web Tokens use signing and verifying to authenticate users in a Flask application.

JSON Web Tokens (JWT) in Flask

Before creating routes to handle signing up and signing in users, let’s look at how we can use JSON Web Tokens (JWT) in an Flask application.

We’ll start by installing the pyjwt package to handle all JSON Web Token functionality. You can find the docs here

pipenv install pyjwt

JWTs are a way to securely transmit information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

💡 No need to know what HMAC, RSA, or ECDSA are. Just know that they are different algorithms used to sign JWTs.

We don’t have to know exactly how JWTs are created and verified, but we still need to know how to use them in our application.

Using JWTs breaks down into two actions:

In this lesson, we will create routes to handle these two actions, as a demonstration of how to use JWTs in Flask. These routes will not appear in the final application, and should only be used for improving your conceptual understanding of JWTs in Flask.

Signing a JWT

Let’s add a new route to our app.py file with the following characteristics:

# add an import for jsonify
from flask import Flask, jsonify

@app.route('/sign-token', methods=['GET'])
def sign_token():
    return jsonify({ "message": "You are authorized!"})

Open up Postman and make a GET request to http://localhost:5000/sign-token. The response should show the message You are authorized!. Make sure you can see the message before you move on.

Now that we have a route that sends a test message, let’s dive into the JWT signing process. The pyjwt package has a method called encode that we can use to create a JWT. We will use this method to create a JWT and send it to the client.

The encode method takes three arguments:

  1. The payload to be included in the JWT.
  2. The secret key to sign the JWT with.
  3. A list of algorithms to use when encoding (default is algorithms=[“HS256”])

The payload is the information we want to include in the JWT. This can be any JSON object. We want to specify that this token belongs to a specific user and no one else. For now, we will include a mock user object with a username, password, and id field.

The secret key is a string used to sign the JWT. This key is unique to our application and should be kept secret.

Now that we’ve seen the encode method, it’s time to use it in our /sign-token route!

Remove the res.json method from the route and replace it with a mock user object:

@app.route('/sign-token', methods=['GET'])
def sign_token():
# Mock user object added
    user = {
        "id": 1,
        "username": "test",
        "password": "test"
    }
    return jsonify(user)

This mock user object will be a stand-in for a user object that we would have in our database; we’ll use it as the payload for our JWT.

We have our payload; now we must create the secret key.

First, create a .env file:

touch .env

And inside of it, add a new key-value pair for the secret key:

JWT_SECRET=supersecret

Before moving on, let’s talk about the secret key. This key can be anything as long as it is a string. This could be as simple as the word supersecret or catsarecool; or as complex as 4inK]I+X+;n2+g"*>AMQc{p99)9DB@a@. As the name implies, the key to the secret key should be secret and not shared or left in your code where someone would have access to view it. This is why we’re storing it in the .env file, which you should always be sure to include in your .gitignore file to prevent it from surfacing on GitHub.

🧠 If you want a more secure secret key, you can use a password generator to create a random string of characters. Or, simply bash on your keyboard for a few seconds and use that as your secret key. Just remember to remove spaces.

Then, to access these environment variable, we’ll need to import the python-dotenv package:

# app.py

from dotenv import load_dotenv
import os
load_dotenv()

This will give us access to environment variables using the os.getenv('variable_name') method.

Next, let’s make use of the pyjwt package and import jwt:

# app.py

# Add an import for the jwt library:
import jwt

We now have everything we need for the encode method.

Pass both the mock user object and the secret key to the sign method and assign the result to a variable called token, and send that variable as the response:

# app.py
@app.route('/sign-token')
def sign_token():
    user = {
        "id": 1,
        "username": "test",
        "password": "test"
    }
    token = jwt.encode(user, os.getenv('JWT_SECRET'), algorithm="HS256")
    # return token
    return jsonify({"token": token})

Send the token back to the client in the response:

Using Postman, make a GET request to http://localhost:5000/sign-token. You should see a response with a token in it. Make sure you can see the token before you move on.

Token

We have successfully created a JWT and sent it to the client! We will use this token in the next lesson to verify the user.

Verifying a JWT

Just like with the signing route, our verifying route will not be a part of our finished application and will just be used for testing and learning purposes.

Inside the app.py file create a new route to receive a POST request. This route should follow the structure below:

@app.route('/verify-token', methods=['POST'])
def verify_token():
    return jsonify({"message": "Token is valid."})

Using Postman, make a POST request to http://localhost:5000/verify-token.

In Postman, you should see a response with the following message:

{
  "message": "Token is valid."
}

Make sure you can see the message before you move on.

Next, we’ll use the decode method to verify the token. The decode method takes two arguments:

We will use the decode method to decode the token we created in the previous route. We will also use the secret key created in the .env file.

To send the token to the server, we’ll once again use Postman. Open Postman and copy the token you were sent in the previous route. If you have lost that token, just send another request to the same test route and get another token.

Still in Postman, navigate to the request you created to test the verify-token route. Under the URL bar, select the Authorization tab.

Auth tab

Once in the Authorization tab, select Bearer Token from the dropdown. Paste the token you copied from the previous route in the input field that appears.

Bearer token

When we send a request to the verify-token route, the token will be included in the request’s headers. We can access this token in the route handler using request.headers.get('Authorization').

Returning to our code, in app.py, update the existing import below with the request package:

# Add import for request
from flask import Flask, jsonify, request

Next, let’s return to our verify-token route.

Remove the placeholder message from res.json and replace it with a new token variable from request.headers.get('Authorization'):

@app.route('/verify-token', methods=['POST'])
def verify_token():
    token = request.headers.get('Authorization')
    return jsonify({"token": token})

Test it out with Postman to ensure you can see the token in the response.

You should see something like:

{
  "token": "Bearer <token here>"
}

Notice the word Bearer is in front of the token. This is a convention that is used to specify the type of token that is being sent. We will need to remove this word before we can verify the token, because the decode method expects only the token with nothing else prepended.

We’ll use the split method to remove the word Bearer from the token. This method will split the string into an array of strings. We can then grab the second item in the array, which will be the token.

@app.route('/verify-token', methods=['POST'])
def verify_token():
    token = request.headers.get('Authorization').split(' ')[1]
    return jsonify({"token": token})

Testing this with Postman again, we can see that we just get back the token without the word Bearer in front of it.

Now that we have the token, let’s verify it’s correct with the decode method from the jwt library. This method will intake the token and the secret key and return the payload used to create the token.

If the token is invalid, the method will throw an error, so let’s wrap it all up in a try/except block:

@app.route('/verify-token', methods=['POST'])
def verify_token():
    try:
        token = request.headers.get('Authorization').split(' ')[1]
        decoded_token = jwt.decode(token, os.getenv('JWT_SECRET'), algorithms=["HS256"])
        return jsonify({"user": decoded_token})
    except Exception as err:
       return jsonify({"err": err.message})

Using Postman, make a POST request to http://localhost:5000/verify-token.

You should see a response with the payload used to create the token:

{
    "user": {
        "id": 1,
        "password": "test",
        "username": "test"
    }
}

Notice that the payload is the mock user we created in the previous route! This will come in handy when we start to use JWTs for authentication. In the next section, we’ll take these ideas of signing and verifying JWTs and apply them to our application.