JSON Web Token¶
JSON Web Token (JWT) is built on top of JSON Web Signature or JSON Web Encryption 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 token¶
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 token¶
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"}
Validate claims¶
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.
Missing essential claims¶
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)
Allow empty essential claims¶
claims_requests = JWTClaimsRegistry(aud={"essential": True, "allow_blank": True})
# this will NOT raise MissingClaimError
claims = {"iss": ""}
claims_requests.validate(claims)
Invalid claims values¶
claims = {"iss": "https://authlib.org"}
claims_requests = JWTClaimsRegistry(iss={"value": "https://jose.authlib.org"})
claims_requests.validate(claims) # this will raise InvalidClaimError
Default validators¶
The JWTClaimsRegistry
has built-in validators for timing related fields:
exp
: expiration timenbf
: not beforeiat
: issued at
JWS & JWE¶
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 JSON Web Signature, 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 JSON Web Encryption, 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.
The key parameter¶
In the above example, we’re using OctKey only for simplicity. There are other types of keys in JSON Web Key.
Key types¶
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:
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.
Use key 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)
Callable key¶
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)
Algorithms & Registry¶
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:
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.