JWT creation and validation in Python using Authlib

Scott Brady
Scott Brady
Python

Authlib is a Python library that provides various OAuth, OpenID Connect, and JWT functionality. Authlib is my preferred library for JWT functionality, as it is one of the better Python implementations for JWT best practices, designed with OAuth and OpenID Connect in mind.

In this article, you’ll see how to use the Authlib library to create and validate JWTs following modern best practices and claims validation.

To learn more about JWT best practices, check out my Pluralsight course JWT Fundamentals.

Installing Authlib

You can install Authlib from the Python Package Index, or you can find installation instructions in the Authlib documentation.

pip install Authlib

Authlib signing keys

To create a JWT, you will need a private key to sign it with. This should use an asymmetric signing algorithm, such as RS256, rather than something symmetric like HS256.

In this example, I’ll load in a signing key from a JSON Web Key (JWK) that contains a key suitable for ES256. Hopefully, it’s obvious that this key is no longer private and only suitable for demos. If you want to quickly generate your own key for testing & documentation, check out the JWK generation feature of my JWT tool.

I’ll be using a JWK directly, but you might consider loading your key from a file or vault.

jwk = {
  "crv": "P-256",
  "kty": "EC",
  "alg": "ES256",
  "use": "sig",
  "kid": "a32fdd4b146677719ab2372861bded89",
  "d": "5nYhggWQzfPFMkXb7cX2Qv-Kwpyxot1KFwUJeHsLG_o",
  "x": "-uTmTQCbfm2jcQjwEa4cO7cunz5xmWZWIlzHZODEbwk",
  "y": "MwetqNLq70yDUnw-QxirIYqrL-Bpyfh4Z0vWVs_hWCM"
}

Creating a JWT using Authlib

To create a JWT, you’ll need two dictionaries, one for the JWT header and one for the payload.

header = {"alg": "ES256"}

The only required header is the algorithm (alg) header; the type (typ) and key ID (kid) headers will automatically be set for you. At the time of writing (version 1.0.1), Authlib will always override the type (typ) header to “JWT”.

import time

payload = {
    "iss": "https://idp.example.com",
    "aud": "api1",
    "sub": "9377717bef5a48c289baa2d242367ca5",
    "exp": int(time.time()) + 300,
    "iat": int(time.time())
}

For the payload, the library does not enforce any required claims; however, following JWT best practices, you should include the issuer, audience, subject, expires at, and issued at claims. Authlib will automatically convert the expires at, issued at, not before claims from a datetime to a Unix timestamp for you.

from authlib.jose import jwt

token = jwt.encode(header, payload, jwk)
print(token.decode("utf-8"))

Despite its name, encode will both encode and sign your JWT using the provided header, payload, and key, returning the bytes of the token.

By default, Authlib will also perform a sense check for sensitive values such as private keys and passwords in the token payload. It’s not the most comprehensive check, but I imagine it comes from a place of experience, and it’s certainly better than nothing!

Example Python JWT

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImEzMmZkZDRiMTQ2Njc3NzE5YWIyMzcyODYxYmRlZDg5In0.eyJpc3MiOiJodHRwczovL2lkcC5leGFtcGxlLmNvbSIsImF1ZCI6ImFwaTEiLCJzdWIiOiI5Mzc3NzE3YmVmNWE0OGMyODliYWEyZDI0MjM2N2NhNSIsImV4cCI6MTY2MDczMTUwOSwiaWF0IjoxNjYwNzMxMjA5fQ.msRnYGq5dS_Ua2hizn0DWBkJczkXTgXSPFcgZACb8f1ezzxoeJFxA4om2cYo2Yay9XzzL_HbxMXEIDGkKoo5Dg

Which, when decoded, shows your payload in the same order that you provided and standard datetime claims converted to Unix timestamps.

{
  "alg": "ES256",
  "typ": "JWT",
  "kid": "a32fdd4b146677719ab2372861bded89"
}.
{
  "iss": "https://idp.example.com",
  "aud": "api1",
  "sub": "9377717bef5a48c289baa2d242367ca5",
  "exp": 1660731509,
  "iat": 1660731209
}

Validating a JWT using Authlib

Now that you have a token let’s validate it as if you were the token recipient.

For this example, I will use the corresponding public key from the creation sample. This key is again a JWK but without the private key parameters.

jwk = {
  "crv": "P-256",
  "kty": "EC",
  "alg": "ES256",
  "use": "sig",
  "kid": "a32fdd4b146677719ab2372861bded89",
  "x": "-uTmTQCbfm2jcQjwEa4cO7cunz5xmWZWIlzHZODEbwk",
  "y": "MwetqNLq70yDUnw-QxirIYqrL-Bpyfh4Z0vWVs_hWCM"
}

Decoding JWTs in Python

To decode the JWT, you can use Authlib’s decode method; however, this alone is not enough for secure validation.

from authlib.jose import jwt

claims = jwt.decode(token, jwk)

Like the encode method, decode does more than just base64url decoding; it also validates the token’s signature. If the signature is invalid, you’ll get a BadSignatureError.

However, decode does not validate any of the payload claims, meaning that you could be accepting expired tokens or tokens intended for a different recipient. This brings us to an important point: always validate tokens, never decode.

JWT validation in Python

Thankfully, the decode method returns some JWTClaims, containing the parsed header and payload values and a validate method for you to call. This validate method handles basic claims validation out of the box (the expires at and not before claims).

claims = jwt.decode(token, jwk)
claims.validate()

To use JWTs effectively, you will want to use their standard claims, such as the issuer (do you trust who created this token?) and audience (is this token intended for you?).

Authlib allows you to both require the presence of these claims and validate their values by passing in some claims_options.

claims_options = {
    "iss": { "essential": True, "value": "https://idp.example.com" },
    "aud": { "essential": True, "value": "api1" }
}

claims = jwt.decode(token, jwk, claims_options=claims_options)
claims.validate()

If a claim check fails, you’ll get an InvalidClaimError.

Algorithm validation

For the decode method, it can be useful to pass in a JSON Web Key Set (JWKS) that you downloaded from a 3rd party. Authlib will parse this set for you and find the correct key based on the token’s key ID (kid) header.

Unfortunately, this library does not automatically handle algorithm validation for you. It does not ask, “does this key make sense for this algorithm?”. This means you are vulnerable to attacks against JWT’s cryptographic agility, such as using an RSA public key as a symmetric key with an HMAC.

You can tighten things by restricting what algorithms you accept. As a validator, it’s likely you’ll only be supporting one algorithm at a time per token issuer.

from authlib.jose import JsonWebToken

jwt = JsonWebToken(jwk["alg"])

claims = jwt.decode(token, jwk)
claims.validate()

Source Code

Hopefully, you found that helpful as a quickstart to using Authlib’s JWT functionality. You can find the source code in my samples repository on GitHub.