Skip to content

examples

This is a collection of recipes related to jwskate usage.

JWK

Generate private/public key pairs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from jwskate import Jwk

private_jwk = (
    Jwk.generate(alg="ES256")  # select the signature or encryption alg here
    .with_kid_thumbprint()  # optionally, include a RFC7638 compliant thumbprint as kid
    .with_usage_parameters()  # optionally, include 'use' and 'key_ops'
)

print(private_jwk)
# {
#   "kty": "EC",
#   "crv": "P-256",
#   "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY",
#   "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk",
#   "d": "Lce_08inNOEe6Q9xEGrR9T0CJNQa1o4EhGtDQYAI0N8",
#   "alg": "ES256",
#   "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA",
#   "use": "sig",
#   "key_ops": [
#     "sign"
#   ]
# }

public_jwk = private_jwk.public_jwk()
print(public_jwk.to_json(indent=2))
# {
#   "kty": "EC",
#   "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA",
#   "alg": "ES256",
#   "use": "sig",
#   "key_ops": [
#     "verify"
#   ],
#   "crv": "P-256",
#   "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY",
#   "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk"
# }

# let's expose this public key as a JWKS:
print(public_jwk.as_jwks().to_json(indent=2))
# {
#   "keys": [
#     {
#       "kty": "EC",
#       "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA",
#       "alg": "ES256",
#       "use": "sig",
#       "key_ops": [
#         "verify"
#       ],
#       "crv": "P-256",
#       "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY",
#       "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk"
#     }
#   ]
# }

Fetching a JWKS from a remote endpoint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from jwskate import JwkSet
import requests

raw_jwks = requests.get("https://www.googleapis.com/oauth2/v3/certs").json()
jwkset = JwkSet(raw_jwks)

print(jwkset)
# {
#  "keys": [
#      ...
#  ]
# }

# compared to a raw dict, a JwkSet offers convenience methods like:
if jwkset.is_private:  # returns True if the jwks contains at least one private key
    raise ValueError(
        "JWKS contains private keys!"
    )  # an exposed JWKS should only contain public keys

my_jwk = jwkset.get_jwk_by_kid("my_key_id")  # gets a key by key id (kid)
# select keys that is suitable for signature verification
verification_jwks = jwkset.verification_keys()
# select keys that are suitable for encryption
encryption_jwks = jwkset.encryption_keys()

Converting between PEM, DER, JWK and cryptography keys

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from jwskate import Jwk

# generate a sample JWK, any asymmetric type will do:
private_jwk = (
    Jwk.generate(alg="ES256")  # generates the key
    .with_usage_parameters()  # adds use and key_ops
    .with_kid_thumbprint()  # adds the key thumbprint as kid
)
print(private_jwk.to_json(indent=2))
# {'kty': 'EC',
#  'crv': 'P-256',
#  'x': '8xX1CEhDNNjEySUKLw88YeiVwEOW34BWm0hBkAxqlVU',
#  'y': 'UfZ0JKT7MxdNMyqMKzKcAcYTcuqoXeplcJ3jNfnj3tM',
#  'd': 'T45KDokOKyuhEA92ri5a951c5kjmQfGyh1SrEkonb4s',
#  'alg': 'ES256',
#  'use': 'sig',
#  'key_ops': ['sign'],
#  'kid': '_E8_LoT4QEwctEkGNbiP9dogVDz6Lq9i8G_fj9nnEo0'}

# get the cryptography key that is wrapped in the Jwk:
cryptography_private_key = private_jwk.cryptography_key
print(type(cryptography_private_key))
# <class 'cryptography.hazmat.backends.openssl.ec._EllipticCurvePrivateKey'>

# create the PEM for the private key (encrypted with a password)
private_pem = private_jwk.to_pem("Th1s_P@ssW0rD_iS_5o_5tr0nG!")
print(private_pem.decode())
# -----BEGIN ENCRYPTED PRIVATE KEY-----
# MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhFd4nINf0/8QICCAAw
# DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJNdsyMjSx3d6RqBBTuI5LoEgZD4
# qdPHcTZhKAuzQ9mkM1SlaZfiydWM2KFqPCYPLwoX+3kuCHPanMLlDxwOGN9XMRYl
# hG3eO0Gu4eWdc/2QEcXIyBCbyKnSwhaHUSSfkhyK9eh8diHQw+blOIImIldLPxnp
# +ABOhO6pCjQxM7I5op7RZuxLNWGLyAlfOOvawLfnM/wKLW6GXmlywu7PZ5qk9Bk=
# -----END ENCRYPTED PRIVATE KEY-----

# write this private PEM to a file:
with open("my_private_key.pem", "wb") as foutput:
    foutput.write(private_pem)

# get the matching public key
public_jwk = private_jwk.public_jwk()
print(public_jwk)
# {
#   "kty": "EC",
#   "kid": "m-oFw9zA2YPFyqm265jbHnzXRa3SQ1ESdCE1AtAqO1U",
#   "alg": "ES256",
#   "use": "sig",
#   "key_ops": [
#     "verify"
#   ],
#   "crv": "P-256",
#   "x": "VVbLOXwIgIFsYQSpnbLm5hr-ibfnIK0EeWYj2HXWvks",
#   "y": "7f24WIqwHGr-jU9dH8GHpPEHMtAuXiwsedFnS6xayhk"
# }

# get the cryptography public key
cryptography_public_key = public_jwk.cryptography_key
print(type(cryptography_public_key))
# <class 'cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey'>

# get the public PEM
public_pem = public_jwk.to_pem()
print(public_pem.decode())
# -----BEGIN PUBLIC KEY-----
# MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8xX1CEhDNNjEySUKLw88YeiVwEOW
# 34BWm0hBkAxqlVVR9nQkpPszF00zKowrMpwBxhNy6qhd6mVwneM1+ePe0w==
# -----END PUBLIC KEY-----

# write this public PEM to a file:
with open("my_public_key.pem", "wb") as foutput:
    foutput.write(public_pem)

# read the private PEM from file and load it as a Jwk:
with open("my_private_key.pem", "rb") as finput:
    private_pem_from_file = finput.read()
private_jwk_from_file = (
    Jwk.from_pem(private_pem_from_file, password="Th1s_P@ssW0rD_iS_5o_5tr0nG!")
    .with_usage_parameters(alg="ES256")  # adds back the alg, use and key_ops parameters
    .with_kid_thumbprint()  # adds back the thumbprint as kid
)
assert private_jwk_from_file == private_jwk

# read the public PEM from file and load it as a Jwk:
with open("my_public_key.pem", "rb") as finput:
    public_pem_from_file = finput.read()
public_jwk_from_pem_file = (
    Jwk.from_pem(public_pem_from_file)
    .with_usage_parameters(
        alg="ES256"
    )  # alg is not part of the PEM, so you can add it back
    .with_kid_thumbprint()  # adds back the thumbprint as kid
)
assert public_jwk_from_pem_file == public_jwk

# get the public DER
public_der = public_jwk.to_der()
print(public_der.hex())
# 3059301306072a8648ce3d020106082a8648ce3d03010703420004f315f508484334d8c4c9250a2f0f3c61e895c04396df80569b4841900c6a955551f67424a4fb33174d332a8c2b329c01c61372eaa85dea65709de335f9e3ded3
# write DER to a file
with open("my_public_key.der", "wb") as foutput:
    foutput.write(public_der)

# read a DER and load it as a Jwk:
with open("my_public_key.pem", "rb") as finput:
    public_pem_from_file = finput.read()
public_jwk_from_der_file = (
    Jwk.from_pem(public_pem_from_file)
    .with_usage_parameters(
        alg="ES256"
    )  # alg is not part of the DER, so you can add it back
    .with_kid_thumbprint()  # adds back the thumbprint as kid
)
assert public_jwk_from_der_file == public_jwk

JWT

Parsing a JWT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from jwskate import Jwt
from datetime import datetime, timezone

# you may recognize the default JWT value from https://jwt.io
jwt = Jwt(
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
    "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
    "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
)
# access the parsed header, as a dict
print(jwt.headers)
# {'alg': 'HS256', 'typ': 'JWT'}
# access the parsed claims, as a dict
print(jwt.claims)
# {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022}
# access the signature, as bytes
print(jwt.signature.hex())
# 49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397

# alg and typ from the headers are accessible as attributes
assert jwt.alg == "HS256"
assert jwt.typ == "JWT"

# some registered claims are accessible, pre-parsed and validated according to RFC7519
assert jwt.issuer is None
assert jwt.subject == "1234567890"
assert jwt.audiences == []
# this would be a datetime if token had a valid 'exp' claim
assert jwt.expires_at is None
# this would be a datetime if token had a valid 'nbf' claim
assert jwt.not_before is None
assert jwt.issued_at == datetime(2018, 1, 18, 1, 30, 22, tzinfo=timezone.utc)
assert jwt.jwt_token_id is None

# checking the signature is as easy as
if jwt.verify_signature(b"your-256-bit-secret", alg="HS256"):  # this returns a bool
    print("signature is verified")

# or, if you prefer an exception
jwt.verify(
    b"your-256-bit-secret", alg="HS256"
)  # this raises an exception if the signature is invalid

Signing an arbitrary JWT

To have full control over the claims that are signed, use the low-level Jwt.sign() method. You provide the full set of claims to sign and those will be signed exactly as-is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from jwskate import Jwk, Jwt

sign_key = Jwk.generate(
    alg="ES256"
)  # here we generate a key, but you usually want to load your own key

claims = {"iss": "my_issuer", "iat": 1695715510, "sub": "my_sub"}
jwt = Jwt.sign(claims, sign_key)

print(jwt)
# eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteV9pc3N1ZXIiLCJpYXQiOjE2OTU3MTUyOTgsInN1YiI6Im15X3N1YiJ9.OBfoh-9zRhHkAEQQbbopUrJ_eENTRgSmllC8r-sCCVrQ73a_F9QAoAX-ye1RUUqLDRoaiEkhJI2VmLVmEEqdaQ

Signing a "proper" JWT

To have jwskate help you with the "standardized" claims such as iat, exp, iss, use a JwtSigner. You pre-configure it with an issuer, a signing key and alg, and a default lifetime. You can then use it to sign your claims:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from jwskate import JwtSigner, Jwk

sign_key = Jwk.generate(alg="ES256")

signer = JwtSigner(
    issuer="my_issuer",
    key=sign_key,
    default_lifetime=600,
)

jwt = signer.sign(
    subject="my_sub",
    audience="my_audience",
    extra_claims={"my_custom_claim1": "my_custom_value1"},
)

print(jwt)
# eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJteV9jdXN0b21fY2xhaW0xIjoibXlfY3VzdG9tX3ZhbHVlMSIsImlzcyI6Im15X2lzc3VlciIsImF1ZCI6Im15X2F1ZGllbmNlIiwic3ViIjoibXlfc3ViIiwiaWF0IjoxNjk1NzE4NTQ3LCJleHAiOjE2OTU3MTkxNDcsImp0aSI6IjMxMzQ2NGI5LWMwOGMtNGE0ZS04NGE3LTlmMmVlYmE4ZjdkNCJ9.TBr-tjZd9m6Kyaa4OYiv9K6V_n5MAr1iMTOpZvl255TbN4Mk2XD6rd-9_UQdsViGHqeBPSzYFM-4nILPP2Tgyw
print(jwt.claims)
# {'my_custom_claim1': 'my_custom_value1',
# 'iss': 'my_issuer',
# 'aud': 'my_audience',
# 'sub': 'my_sub',
# 'iat': 1695718547, # iat and exp are auto-generated based on current time
# 'exp': 1695719147,
# 'jti': '313464b9-c08c-4a4e-84a7-9f2eeba8f7d4'} # a jti is autogenerated

Signing and encrypting a nested JWT

It is trivial to sign then encrypt a JWT. You just need a signing key, an encryption key, and the claims you need to sign. jwskate handles everything else for you!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from jwskate import Jwk, Jwt

enc_key = Jwk.generate(alg="ECDH-ES+A128KW")  # choose your own key management alg!
sig_key = Jwk.generate(alg="ES256")  # choose your own signature alg!

claims = {"iss": "my_issuer", "iat": 1695715510, "sub": "my_sub"}

signed_and_encryted_jwt = Jwt.sign(claims, key=sig_key).encrypt(  # this signs a JWT
    key=enc_key.public_jwk(), enc="A256GCM"
)  # this encrypts the signed JWT into a JWE. Choose your own encryption alg!

print(signed_and_encryted_jwt)
# eyJjdHkiOiJKV1QiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJSOS05eExHb052M3JhVGp6bUJndXFoZUU0NlBCbVB3VnVSc2lGVkllQnVZIiwieSI6Il8tS2txYXNtR0E1M2g5RTB4MTF2QmlEQ1g1M3M4Qi1TZTdvR0M4OW5yR0EifSwiYWxnIjoiRUNESC1FUytBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIn0.KJSBiz5MkCy-YojozYgs5wn14T4llyI-MQBtkkzQpeHx7NPEhxUieA.x-flJmO6j5fX1ayE.LgpTlnZ6t01rO_ojhpep8RVxvI0ijdV_PbR6JcolTp7e327QpSsLgEA6_gllMO6_oxSPiqFwNgU-FkQhInT1FGZWKc04EA1coAsvmkDykshKWxiSC4LimYkqi-YOSQW7K9IBuMdMyEY6gYEj-znsY1HbYrh3nhS8_03QRbK56fJYIYcLUhkbcZBe3nHdD5kqNNigCuMW8ENo7M6jT53ZC__lYUyVIc-_JLZGUCGCEbJB--1ctFYDA4Iwyvxhk-wi.esupByQ5sWTq2lvzyNLi4w

print(
    signed_and_encryted_jwt.headers
)  # note that this header is generated automatically, including the ephemeral public key (epk)
# {'cty': 'JWT', # the inner content-type is JWT
# 'epk': {'kty': 'EC', 'crv': 'P-256', 'x': 'R9-9xLGoNv3raTjzmBguqheE46PBmPwVuRsiFVIeBuY', 'y': '_-KkqasmGA53h9E0x11vBiDCX53s8B-Se7oGC89nrGA'},
# 'alg': 'ECDH-ES+A128KW',
# 'enc': 'A256GCM'}

# to decode and verify the inner signed JWT:
inner_signed_jwt = signed_and_encryted_jwt.decrypt_jwt(enc_key).verify(
    sig_key.public_jwk()
)

print(inner_signed_jwt)
# eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteV9pc3N1ZXIiLCJpYXQiOjE2OTU3MTU1MTAsInN1YiI6Im15X3N1YiJ9.smfmqDYveE4TQboXhzxb7qddrITvU7JpWNwSfLj4XDOt8-tHe-pAuZ5EYJD0p4cynS1OwhT8LGSWnVpi7bPBCw