Working with JSON Web Key Sets
Scale up your JWT validation operation.
Recap
In Part 1 we looked at what a JSON Web Token is, how it's created, and how it's validated. In Part 2 we looked at a simple Express server with JWT validation middleware, with all the usual warnings about not reinventing the wheel.
Defining the Terms (mostly review)
As a popular language learning app says... Before we begin, here are some terms you should know:
- JWT - short for JSON Web Token. This is the piece of information that needs to be validated (checked for authenticity) with every request.
-
Algorithm - in the JWT world, the algorithm is the combination of hashing and signing the token's payload. Comes in two flavors:
-
Symmetric - If you see
HSxxx
in the header, you're dealing with HMAC-SHAxxx signing algorithms. A secret key was used to sign the JWT, and that same secret key will be necessary to validate the signature later. -
Asymmetric - If you see
RSxxx
,ESxxx
, orPSxxx
in the header, you're working with asymmetric signing algorithms. Of these,RSxxx
is probably the most common and almost certainly the most performant; it uses RSA as the signing method, which requires a public/private keypair. The private key is used to sign the JWT, but it can be validated with the matching public key.ESxxx
means Elliptic Curve Digital Signature Algorithm (ECDSA) was used;PSxxx
also uses RSA under the hood but uses a newer signature scheme than the olderRSxxx
tokens use. If that doesn't make any sense whatsoever, that's fine - understanding the ins and outs of hashing and signing algorithms isn't strictly necessary to understand the basic workflow.
-
Symmetric - If you see
- JWKS - short for JSON Web Key Set. If you don't know what that is, read on because that's the point of this post.
- Public/Private Keypair - A set of two cryptographic certificates that are tied to each other. In (very) short, a private key can be manipulated to extract a public key, but the operation is one-way; that is, the public key cannot be used to generate the corresponding private key. Wikipedia has more information. Key pairs are commonly used to generate SSL certificates for websites, and for enabling passwordless SSH logins.
Scaling Up
In Part 2 we looked at an example repo that could validate either a symmetric algorithm or an asymmetric algorithm, based on the token's header's alg
claim and either an environment variable or a public key. In Part 3 we'll consider ONLY asymmetric algorithms, specifically RS256
.
One of the advantages of using a private/public keypair is that the verification can be done using a public key without the private key ever needing to leave the signing server. The public key is, after all, meant to be public, and because of the math involved, it's not possible to reverse-engineer the private key from just the public key. This means the public key can be loaded pretty much anywhere, which means the token can be validated pretty much anywhere too. But the downside of using a keypair is that if you rotate the public key every now and then (and you absolutely should do that) there will be some period, however brief, where the certificates won't match: the server will be signing tokens with the new private key, but the validation endpoints will still have the old public key, and the validator will fail. Building infrastructure around the update process can help mitigate the risk of a key mismatch but it's still not completely fail-proof.
JWKS represents a better way to manage your key rotation. When the JWT is signed by the issuing system, a key ID for the private key used to sign the token is stored in the header of the token. The validating system reads both the algorithm used as well as the key ID, and validates the signature based on the public key with the same ID. The JWKS is a serialized array containing one or more public keys with their IDs. The token header will contain just one more claim, the kid
claim:
{
"alg": "RS256",
"typ": "JWT",
"kid": "12345"
}
The kid
claim can be any alphanumeric string - GUID, integer, name, they all work just as well, though care should be taken to ensure that the keys being used are not assigned the same ID.
The JWKS itself is a JSON object with a single keys
array, which contains key objects:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "174EFCA3923B25163F581A0CB71CAD97B036E0B8",
"x5t": "F078o5I7JRY_WBoMtxytl7A24Lg",
"x5c": ["MIIEWzCCA0OgAwIBAgIJAPzw5pQEyIL4MA0GCSqGSIbgZ3..."],
"e": "AQAB",
"n": "n1x3isqbPYjG2dUm5d5N1MBk9zKHt5LujgFXJO1SCnCW...",
"alg": "RS256"
},
{ ... }
]
}
Note that each key has a kid
assigned; tokens should include this same kid
in the header so that the public key can be loaded.
"But wait," you say, "where's the public key? What is this x5t
and x5c
and e
and n
stuff?" You're probably used to seeing public keys that look like this:
-----BEGIN PUBLIC KEY-----
(lots of letters and numbers)
(that go on for lines and lines)
-----END PUBLIC KEY-----
What you see in the JWKS, though, is the raw information necessary to build the public key on the fly. Libraries like jsrsasign (which we used in Part 2) have methods to import the cryptographic information in the JWKS and construct the public key necessary to validate the token. Those libraries also have utilities to do the same thing in reverse - turn the more common certificate format (PEM) into a serialized data object.
Accessing the JWKS
While it's nice to have a serialized format that allows for the storage of multiple keys, it can still be a hassle to update this JWKS if you have to push it to multiple endpoints. A better strategy is to take advantage of autodiscovery and host the key set at a single URI at a well-known location so that consumers can periodically auto-update. For example, Microsoft Azure AD stores its JWKS at https://login.microsoftonline.com/common/discovery/v2.0/keys (you can visit that in your browser and see the JSON structure, though I'd recommend using something that can pretty-print the output as it's minified by default). When Microsoft wants to rotate a key, they simply push the new public key into the set and publish the new JWKS at the same URI. Consuming clients can poll the URI on a regular schedule and get the new public key. After a brief period (long enough that consuming clients have picked up the updated list) they will start signing tokens with the new private key, and any client that gets a new token will have the corresponding public key already loaded into memory.
Special Consideration: Generating the Keypair
Throughout this intro, I've mentioned public/private keypairs. You can generate these yourself, and if you've ever set up remote access to a server using SSH, you probably already have:
$ openssl genrsa -out jwt_signing_cert.pem 2048
$ openssl rsa -in jwt_signing_cert.pem -pubout -out jwt_public_key.pem
This generates a 2048-bit private key using RSA, then extracts the public key and saves it. Once those two commands finish, you'll have two new .pem
files, one the private key and the other the public key. You can absolutely use those files for signing and validation...but you probably shouldn't. The reason is that these keys are considered "self-signed" and using self-signed keys in public is generally considered a poor practice. Some cloud vendors and validation packages will not allow you to validate tokens using self-signed certificates.
When I first started using my former employer's JWKS solution, I was stymied by this restriction for a few days while trying to build a demo I could use in sales calls. Generating signed certificates generally costs money, and while I could have probably gotten a reimbursement for this, it was an expense I didn't want to incur. Ultimately, though, I realized that a PEM certificate is a PEM certificate, regardless of what it's used for, and there exists at least one service that was created for the sole purpose of issuing free certificates: Let's Encrypt. The process for doing that ended up being pretty straightforward, though it took a while to piece all the information together.
Before going down this route, you'll want to make sure that certbot
is installed and up-to-date. Follow the official instructions if you're not sure.
You'll also need to make sure you have the means of generating your own cert with Let's Encrypt, which means you need access to the DNS settings for a domain.
It's important to note that by default, certbot will use ECDSA as the private key algorithm. It does still support using RSA keys (for now), but you will need to request it specifically. By default when creating RSA keys, certbot uses a 2048-bit key. While you can change this, you probably should not, as some cloud providers only support 2048-bit RSA keys.
To generate a signed 2048-bit RSA private key from Let's Encrypt, use the following command, either as root
or using sudo
:
$ certbot certonly \
--manual \
--preferred-challenges dns-01 \
--key-type rsa \
-d jwks01.yourdomain.com
I'm using the manual
mode on certbot to specify my preferred challenge method (dns-01
which requires a DNS TXT record to prove ownership) and, critically, specifying the --key-type rsa
flag to generate an RSA private key.
Certbot will give you a text record to place at _acme-challenge.jwks01.yourdomain.com
(the actual subdomain you use is arbitrary; I chose jwks01
because it's unlikely I would actually host anything there, so I don't have to worry about naming collision, and because when I'm looking at all my _acme-challenge
records I've collected over the years, I want to know what that one was for). Once that DNS record is in place and certbot has validated it, it will place four files in its default location; for me, that was /etc/letsencrypt/live/jwks01.yourdomain.com
. DO NOT MOVE THESE FILES. We really only care about two of them: privkey.pem
(the RSA private key) and fullchain.pem
(the certificate signing chain). You'll want to copy those two files into your project directory.
(The other two files there are more relevant if you're setting up SSL for jwks01.yourdomain.com
, which is the whole point of Let's Encrypt. However, as I mentioned earlier, a signed RSA private key is a signed RSA private key.)
You can use the commands above to generate the public key from the private key:
$ openssl rsa -in privkey.pem -pubout -out pubkey.pem
And that's all there is to it! You're now the proud owner of a signed RSA private key and its corresponding public key.
"But wait," you say once again, "what about the fullchain.pem
file?" Glad you asked! There's one more piece of this puzzle you need to generate an actual JWKS file, but because that's a bit more code-heavy, I'll save that for next week's installment.
Wrapping it Up
In this installment, we looked at what a JSON Web Key Set is and how it helps scale out the adoption of asymmetric RSA signing keys for JSON web tokens. We also took a brief detour into the world of public key cryptography and talked about ways to generate a valid public/private keypair that can be used to sign and validate JWTs.
The final installment will cover how to modify the existing express-jwt
example repo introduced in Part 2 to import those keys, serve the JWKS at a well-known URI, and use the JWKS to validate multiple tokens signed by different private keys.
Further Reading
- Private and Public Keys - a quick primer from ssl.com.
- JWKS Properties - a deeper dive into the properties of a single JSON Web Key from Auth0.
-
Everything you need to know about RSASSA-PSS - a deeper dive into the difference between the signing process for
RSxxx
tokens andPSxxx
tokens.
Top comments (0)