- description
How to encode and decode a JSON Web Token (JWT) in python.
joserfc.jwt
JSON Web Token (JWT) is built on top of jws
or jwe
and includes specific payload claims. These claims are required to be in JSON format and follow a predefined set of fields.
Hint
Do you know that JSON Web Token (JWT) is not a part of JOSE. Instead, it was created by the OAuth working group.
encode
is the method for creating a JSON Web Token string. It encodes the payload with the given alg
in header:
from joserfc import jwt
from joserfc.jwk import OctKey
header = {"alg": "HS256"}
claims = {"iss": "https://authlib.org"}
key = OctKey.import_key("secret")
text = jwt.encode(header, claims, key)
The returned value of text
in above example is:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJodHRwczovL2F1dGhsaWIub3JnIn0.
Zm430u0j1wzf5Me5Zoj2h6dTt9IFsb7-G5mUW3BTWbo
Line breaks for display only.
decode
is the method to translate a JSON Web Token string into a token object which contains .header
and .claims
properties:
# reuse the text and key in above example
token = jwt.decode(text, key)
# token.header = {'alg': 'HS256', 'typ': 'JWT'}
# token.claims = {"iss": "https://authlib.org"}
The jwt.decode
method will only verify if the payload is a JSON base64 string.
You can define claims requests JWTClaimsRegistry
for validating the decoded claims. The JWTClaimsRegistry
accepts each claim as an Individual Claims Requests JSON object.
from joserfc.jwt import JWTClaimsRegistry
claims_requests = JWTClaimsRegistry(
iss={"essential": True, "value": "https://authlib.org"},
)
# usually you will use the claims registry after ``.decode``
claims_requests.validate(token.claims)
The Individual Claims Requests JSON object contains:
essential
OPTIONAL. Indicates whether the Claim being requested is an Essential Claim. If the value is true, this indicates that the Claim is an Essential Claim.
value
OPTIONAL. Requests that the Claim be returned with a particular value.
values
OPTIONAL. Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.
And we added one more field:
allow_blank
OPTIONAL. Allow essential claims to be an empty string.
claims_requests = JWTClaimsRegistry(aud={"essential": True})
# this will raise MissingClaimError
claims = {"iss": "https://authlib.org"}
claims_requests.validate(claims)
# this will raise MissingClaimError
claims = {"iss": ""}
claims_requests.validate(claims)
claims_requests = JWTClaimsRegistry(aud={"essential": True, "allow_blank": True})
# this will NOT raise MissingClaimError
claims = {"iss": ""}
claims_requests.validate(claims)
claims = {"iss": "https://authlib.org"}
claims_requests = JWTClaimsRegistry(iss={"value": "https://jose.authlib.org"})
claims_requests.validate(claims) # this will raise InvalidClaimError
The JWTClaimsRegistry
has built-in validators for timing related fields:
exp
: expiration timenbf
: not beforeiat
: issued at
JWT is built on top of JWS and JWE, all of the above examples are in JWS. By default jwt.encode
and jwt.decode
work for JWS. To use JWE, you need to specify a registry
parameter with JWERegistry
. Here is an example of JWE:
from joserfc import jwt, jwe
from joserfc.jwk import OctKey
header = {"alg": "A128KW", "enc": "A128GCM"}
claims = {"iss": "https://authlib.org"}
key = OctKey.generate_key(128) # the algorithm requires key of 128 bit size
registry = jwe.JWERegistry() # YOU MUST USE A JWERegistry
jwt.encode(header, claims, key, registry=registry)
The JWE formatted result contains 5 parts, while JWS only contains 3 parts, a JWE example would be something like this (line breaks for display only):
eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwidHlwIjoiSldUIn0.
F3plSTFE5GPJNs_qGsmoVx4o402URh5G.
57P7XX6C3hJbk-Nl.
dpgaZFi3uI1RiOqI3bmYY3_opkljIwcByf_j6fM.
uv1BZZy5F-ci54BS11EYGg
Another difference is the key used for encode
and decode
.
For jws
, a private key is used for encode
, and a public key is used for decode
. The encode
method will use a private key to sign, and the decode
method will use a public key to verify.
For jwe
, it is the contrary, a public key is used for encode
, and a private key is used for decode
. The encode
method will use a public key to encrypt, and the decode
method will use a private key to decrypt.
In the above example, we're using OctKey
only for simplicity. There are other types of keys in jwk
.
Each algorithm (alg
in header) requires a certain type of key. For example:
HS256
requiresOctKey
RS256
requiresRSAKey
ES256
requiresECKey
orOKPKey
You can find the correct key type for each algorithm at:
JSON Web Signature Algorithms <jws_algorithms>
JSON Web Encryption Algorithms <jwe_algorithms>
Here is an example of a JWT with "alg" of RS256
in JWS type:
from joserfc import jwt
from joserfc.jwk import RSAKey
header = {"alg": "RS256"}
claims = {"iss": "https://authlib.org"}
with open("your-private-rsa-key.pem") as f:
key = RSAKey.import_key(f.read())
# "RS256" is a recommended algorithm, no need to pass a custom ``registry``
text = jwt.encode(header, claims, key)
# ``.encode`` for JWS type use a public key, if using a private key,
# it will automatically extract the public key from private key
jwt.decode(text, key)
In production, jwt.encode
is usually used by the client side, a client normally does not have the access to private keys. The server provider would usually expose the public keys in JWK Set.
You can also pass a JWK Set to the key
parameter of encode
and decode
methods.
import json
from joserfc.jwk import KeySet
from joserfc import jwt
with open("your-private-jwks.json") as f:
data = json.load(f)
key_set = KeySet.import_key_set(data)
header = {"alg": "RS256", "kid": "1"}
claims = {"iss": "https://authlib.org"}
jwt.encode(header, claims, key_set)
The methods will find the correct key according to the kid
you specified. If there is no kid
in header, it will pick on randomly and add the kid
of the key into header.
A client would usually get the public key set from a public URL, normally the decode
code would be something like:
import requests
from joserfc import jwt
from joserfc.jwk import KeySet
resp = requests.get("https://example.com/.well_known/jwks.json")
key_set = KeySet.import_key_set(resp.json())
def parse_token(token_string: str):
jwt.decode(token_string, key_set)
It is also possible to assign a callable function as the key
:
import json
from joserfc import jwk
def load_key(obj):
headers = obj.headers()
alg = headers["alg"]
key_path = f"my-{alg}-key.json"
with open(key_path) as f:
data = json.load(f)
key = jwk.import_key(data["kty"], data)
return key
# jwt.encode(header, claims, load_key)
The encode
and decode
accept an algorithms
parameter for specifying the allowed algorithms. By default, it only allows your to use recommended algorithms.
You can find out the recommended algorithms at:
JSON Web Signature Algorithms <jws_algorithms>
JSON Web Encryption Algorithms <jwe_algorithms>
For instance, HS386
is not a recommended algorithm, and you want to use this algorithm:
>>> from joserfc import jwt, jwk
>>> header = {"alg": "HS384"}
>>> claims = {"iss": "https://authlib.org"}
>>> key = jwk.OctKey.import_key("secret")
>>> jwt.encode(header, claims, key, algorithms=["HS384"])
If not specifying the algorithms
parameter, the encode
method will raise an error.