Welcome to Tony's Notebook

Write your own JWT generator in Python

I have written about JWTs before. If you are not familiar with them you might start by taking a look at my article on Understanding JWTs and also my piece on Python keywords, as I revisited JWTs there too. So, assuming you are familiar with those articles, I will dive straight into it.

The problem

I do a lot of generating JWTs on the command line and it was a pain specifying things like Application IDs, user names and ACLs on the command line. I wanted a simple file in my project directory where I could set the information once, and then use that file to generate JWTs as required. That's it!

Here's what a sample file, .jwt, might look like for a typical project:

APP_ID="7ffb050a-121e-4a67-94b8-8301a7e4163d"
PRIVATE_KEY_FILE="private.key"

# 24 hrs
EXPIRY=86400

# For Client SDK tokens to authenticate users only
SUB=<username>
ACL='{"paths": {"/*/users/**": {},"/*/conversations/**": {},"/*/sessions/**": {},"/*/devices/**": {},"/*/image/**": {},"/*/media/**": {},"/*/applications/**": {},"/*/push/**": {},"/*/knocking/**": {}}}'

Generally it's the APP_ID that you need to keep copying and pasting on the command line, so that's first in the file. By convention I store my private key in private.key. The other entries are optional, but I usually set a long expiry for testing. If you don't set this the default of 15 minutes is used.

Env file format

The .jwt file is slightly special in that it is actually an environment file. Typically your environment file is .env. I don't use that name as many of my projects already have a .env and typically I want to keep the JWT info separate. The great thing about environment files is Python has a library that makes dealing with them really easy. You pull in the library with from dotenv import load_dotenv. You then use calls like os.getenv("PRIVATE_KEY_FILE") to fetch the environment variable values.

Claims

I talked before about optional "claims" that a JWT might have. In the above example SUB and ACL are optional claims - they only make sense for me when testing Nexmo Client SDK apps. You could however happily add your own optional claims and they would be taking into account by the JWT generator automatically. The only exception to that is if a claim needed some additional processing or logic before being used that would need to be added in the code. An example of this is ACLs. If you look at valid ACLs for working with Nexmo Client SDK in jwt.io, you will see they are a JSON object. For this reason I have to convert this from a string in the .jwt file to a Python object with json.loads(). You then have a Python dictionary (print(type(payload['acl']))) and jwt.encode does the right thing. If your claims don't need special handling you can just add your claims to the .jwt file and away you go - check the code comment for the correct place to pass in your optional claims.

The code

Given what you already know about JWTs from my other articles you already have a head start in understanding the code. So I will go "full monty" and display it here:

#!/usr/bin/env python3
import os
import jwt
import time
from uuid import uuid4
from dotenv import load_dotenv
import json

def read_file(filename):
    f = open (filename, mode='r', encoding='utf-8')
    source = f.read()
    f.close()
    return source

load_dotenv('.jwt')
app_id = os.getenv("APP_ID")
private_key_file = os.getenv("PRIVATE_KEY_FILE")
private_key = read_file(private_key_file)
exp = os.getenv("EXPIRY")
sub = os.getenv("SUB")
acl = os.getenv("ACL")

def build_payload (application_id,  **kwargs):
    payload = {}
    payload['application_id'] = application_id
    payload['iat'] = int(time.time())
    payload['jti'] = str(uuid4())
    if "exp" in kwargs:
        exp = kwargs.pop('exp')
        if exp:
            payload['exp'] = int(time.time()) + int(exp)
        else:
            payload['exp'] = int(time.time()) + (15*60) # default to 15 minutes
    for k in kwargs:
        if kwargs[k]:
            if k == 'acl':
                payload[k] = json.loads(kwargs[k]) # In jwt.io acl is JSON object in the valid JWT.
            else:
                payload[k] = kwargs[k]
    return payload

payload = build_payload(app_id, exp=exp, sub=sub, acl=acl) # Add optional custom claims as required
token = jwt.encode(payload, private_key, algorithm='RS256')
j = token.decode(encoding='ascii') # Convert byte string to printable string
print(j)

Much of the code I have looked at in my previous articles, so it should be fairly self-explanatory with the comments.

Note that when printing out the JWT it's actually a byte string after encoding, so you need to decode that for the real world using str.decode(). I use the ASCII encoding because ASCII chars should cover the range of chars in a JWT, as base64url encoded data is a string of characters that only contains a-z, A-Z, 0-9, - and _.

Usage

You can add the generator to your path. You can then run it in your project directory to generate JWTs based on the .jwt file in that project directory. If you require a custom version of the generator for a project you can just copy the program file to that project directory, customize the code, and run there. It's open source (MIT license) so you can do what you want with it.

You can also generate JWTs from a shell script, and potentially set an environment variable to contain the JWT. This can make testing quite convenient.

Things to do with the code

GitHub repo

The code is also available in my GitHub repo. I'm not completely happy with the code as it is, although things have been working quite nicely, so I expect the code in the repo will be improved when I get time.

Further information