JWTs: More Than You Think

JSON Web Tokens, or JWTs, are a very important aspect of internet technology, be it Internet of Things, or Internet of People. Knowing this makes it very confusing as to why there’s so little discussion about it, and so much misinformation about it. So I’m writing this blog post to share some information, and hopefully clear away the mists that surround this subject.

There are already many libraries in most major languages for handling JWTs, but for the code examples in this article, I’ll be using the jose gem for Ruby.

The Misconception

Most of the times that you look up JWTs, you’ll see something similar to the item below.

{
  alg: RS256,
  typ: JWT
},
{
  iss: "Chris Larsen",
  sub: 'JWT',
  aud: 'Silicon Halton',
  iat: Time.now
},
2xauJJstlosXp40c2BAwJhueGUqTqq50pu5ICcJuQQA

While this is a JWT, it’s not the only kind of JWT. This is what’s called a JSON Web Signature(JWS). There is another type of JWT called a JSON Web Encryption(JWE). In common usage, most developers use JWT as a synonym for JWS. Even many of the JWT libraries ignore the JWE functionality, only providing signature and verification features.

JWS vs. JWE

The JWS and JWE specifications both provide useful, yet different functions. A JWS adds a digital signature to a set of claims, allowing the receiver to ensure that none of the data has been altered in transmission. A JWE on the other hand, encrypts the data sent, to ensure that it cannot be read by a third party. Both formats make use of Base64URL encoding in order to provide a URL-safe token that can be sent as the bearer token of an HTTP request, or as part of an HTTP response.

It’s also possible to sign AND encrypt data, by nesting one JWT within another. That is functionality that I won’t be covering in this article, but it’s possible to read about it in the JWT spec.

A number of algorithms are included in the specs for both signing and encrypting JWTs. The most popular algorithms are HMAC, which uses a symmetric key, and RSA, which uses private/public key pairs.

JWS

The typical use case for a JWS is for client authentication. The steps for this usage would be:

  1. Client signs in using credentials (most typically a combination of username/email and password)
  2. Server checks credentials against user database, and upon success, will generate and return the encoded JWS, with any required information included in the payload.
  3. Subsequent requests from the client to the server will include the encoded JWT for authentication.

The process of building a JWS is as follows:

  1. Encode JWS Protected Header as BASE64URL(UTF8(JWS Protected Header))
  2. Encode JWS Payload as BASE64URL(UTF8(JWS Payload))
  3. Encrypt ASCII(BASE64URL(UTF8(JWS Protected Header)) || ‘.’ || BASE64URL(JWS Payload)) using the algorithm specified in the header and the key
  4. Concatenate these values in the order Header.Payload.Signature
  5. This provides the URL safe JWS Compact Serialization

Note: Base64URL encoding is NOT a substitute for encryption. Only the signature of a JWS is encrypted. If a third party is able to get your JWS, then they will be able to easily decode it and read the contents of the header and payload. If you’re transmitting sensitive information in the JWS, you should also encrypt it as a JWE. Also, as always, you should be securing your connection using TLS, even when sending a JWE.

Here’s an example of creating and verifying a JWS using the HS256 symmetric algorithm.

require 'jose'

# HS256 Symmetric Signature
secret = 'some128bitsecret'
jwk = JOSE::JWK.from_oct(secret)
header = { "alg" => "HS256" }
payload = { "iss" => "Chris Larsen", 
                     "sub" => "JWTs", 
                     "aud" => "The blog reading public", 
                     "iat" => 1540121863 
}

signed_hs256 = JOSE::JWT.sign(jwk, header, payload).compact
puts "Signed and Encoded JWS: #{signed_hs256}"
#=> Signed and Encoded JWS: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJTaWxpY29uIEhhbHRvbiBTb2Z0d2FyZSBQMlAiLCJpYXQiOjE1NDAxMjE4NjMsImlzcyI6IkNocmlzIExhcnNlbiIsInN1YiI6IkpXVHMifQ.2xauJJstlosXp40c2BAwJhueGUqTqq50pu5ICcJuQQA

verification = JOSE::JWT.verify_strict(jwk, ["HS256"], signed_hs256)
puts "Verification of JWS: #{verification.first}"
#=> Verification of JWS: true

 

Here’s an example of creating and verifying a JWS using RS256 encryption. You’ll note that the encrypted signature is a lot larger using this algorithm, resulting in a larger token.

# RS256 Assymetric Signature
private_key = JOSE::JWK.generate_key([:rsa, 4096])
public_key = JOSE::JWK.to_public(private_key)
header = { "alg" => "RS256" }

signed_rs256 = JOSE::JWT.sign(private_key, header, payload).compact
puts "Signed and Encoded JWS: #{signed_rs256}"
#=> Signed and Encoded JWS: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJTaWxpY29uIEhhbHRvbiBTb2Z0d2FyZSBQMlAiLCJpYXQiOjE1NDAxMjE4NjMsImlzcyI6IkNocmlzIExhcnNlbiIsInN1YiI6IkpXVHMifQ.fzvQ4VPoA1DimHrP_b4kJY5k9vOgaR5rCZ85sKx1jYduuA0wLuu65M5hrvL3AeTs8xrkgqfK-joyU0PKBogJ76Dj5hb8yUmjxuvaG9kGxKM0VoJIS6V87N6wiQHu9SWCcIkYqovPnc01nDn7vAfsdHt6DkVRw2C3uWOR3kmwQT0iXWSjGMtm_uotqMexBNLghAf3LO131B8qltSguVeH-mtNFO18FNlMA6XlYB56dL-QYTWXk4VH5ocNiveUwmzlH8HmCq4Ms2tahXxHkHJRdQVmQU6EOodpldnx1CeZA7M-T7XBuB4byaYkpazrhotG8t4Z-SNuonWuE02D4N4vLpI2K2eVY4gjbqBmX5R8NHS8gY7gjmpbq3Xu2cgZIt0mgXMSUyrv-mYbVoTWBqx3Fs7lsXKCmBy6AGKxhMqFJPVFiW3WgjptEBhHmg0wRZxOW9RxtYqKO3h-2ytWlpZ06Wj_e4RuXeh6DkKwgBw1a9I1fbl-IyVfJt6cOLGEn7L9pmgZn1zcGtDcTvQdBqjwuX3DL3YDfRSb-Oxe5I-7tWRS_sMUuilULw4ITo0owo1z_rljtF5HvFyt8qU7_b6Be-kZcBAOSGVAoX7EyZznREpRpNM2taZB6XDrm7ZPkzPrHlHsaB7d1IfF-RwRk3X0tRGT1l-E-S3_1w6ZbwCh__k

verification = JOSE::JWT.verify_strict(public_key, ["RS256"], signed_rs256)
puts "Verification of JWS: #{verification.first}"
#=> Verification of JWS: true

While the first example used the same key for both signing and verifying, this example uses assymetric key pairs. In the case of signatures, the private key is used to sign an item, and the public key is used to verify it. This provides a more secure method than the symmetric key, as the private key, used to generate the signature doesn’t have to be shared. While sharing a symmetric key to the receiver of the token, it’s possible for it to be intercepted by a third party, and thus used to alter the data in the payload, and then provide a signature that matches the altered data. The public key doesn’t provide that ability, and thus is safe to share openl.

JWE

The process for building a JWE is a bit more complicated than that of the JWS. One of the key things to note about a JWE, is that there are two algorithms listed in the header. One is used to encrypt the plaintext of the message, and is included with the name “enc.” The other algorithm is listed in the header as “alg,” and is used to encrypt the key that encrypts the message.

  1. Encode JWE Protected header as BASE64URL(UTF8(JWS Protected Header))
  2. Generate a random Content Encryption Key(CEK)
  3. Encrypt the CEK with the recipient’s public key using the algorithm specified by “alg” in the header
  4. Base64url-encode the JWE Encrypted Key
  5. Generate a random Initialization Vector, and Base64URL encode it
  6. Generate the Additional Authenticated Data as a Base64URL encoding of the Protected Header
  7. Encrypt the plaintext with the algorithm specified by “enc” in the header, using the CEK as the encryption key, the Initialization Vector, and the Additional Authenticated Data value
  8. Concatenate these values in the order ProtectedHeader.EncryptedKey.InitializationVector.Ciphertext.AuthenticationTag
  9. This provides the URL safe JWE Compact Serialization

Here’s an example of encrypting a JWE using a symmetric algorithm:

require 'jose'

message = "Jodie Whittaker is the greatest Doctor ever!"

# A128GCM Symmetric Key Encryption/Decryption
secret = 'some128bitsecret'
jwk = JOSE::JWK.from_oct(secret)
encrypted_a128gcmkw = JOSE::JWE.block_encrypt(jwk, message, { "alg" => "A128GCMKW", "enc" => "A128GCM" }).compact
puts "encrypted JWE: #{encrypted_a128gcmkw}"
#=> encrypted JWE: eyJhbGciOiJBMTI4R0NNS1ciLCJlbmMiOiJBMTI4R0NNIiwiaXYiOiJnMW5Ld20xTlpjWExlMnpMIiwidGFnIjoiUkpSRll4cEVqY0NkMXN3a25MNnR2USJ9.rk77No8yWkIwH0TJpo60Nw.aJOp6rNyPnbivzcD.XVV3p1pzXH68wZvkRxU1Z3w8qHpvMuPudhqfsyaDP-556nII_MksS48rmx0.0jIODwJGT8emRqRexbkHWg

decrypted = JOSE::JWE.block_decrypt(jwk, encrypted_a128gcmkw).first
puts "Decrypted message: #{decrypted}"
#=>Decrypted message: Jodie Whittaker is the greatest Doctor ever!

Now, here is an example of encrypting the same message using an assymetric algorithm:

#RSA Assymetric Key Encryption/Decryption
puts "-----Assymmetric Key Encryption/Decryption-----"
private_key = JOSE::JWK.generate_key([:rsa, 4096])
public_key = JOSE::JWK.to_public(private_key)

encrypted_rsaoaep = JOSE::JWE.block_encrypt(public_key, message, { "alg" => "RSA-OAEP", "enc" => "A128GCM" }).compact
puts "encrypted JWE: #{encrypted_rsaoaep}"
#=> encrypted JWE: eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.G0XdCld3CSlX1ukuLX9wMiQKmBLDqWN4Af2Ow7a2zHiKKjFoEZ45DpNrvIki0wqDxojCX2iPcFZfWgOyS1vtutgSlUtFlrKwpUbD5pYt0R6iBGPIUWCa9fp-qaEiMOm0TVh00Z75R23Ziqr_SZ1ZlCzFPn5tbNfqrVIfxwMZN1hwXvBkF80DDQ-wqYQEUPKsshC65tckxM-xyqinYnmGGUuX3tjfPWlhG25i0AqEEhXWJ_uoy6BZrRFpdTQbtmRUerRQjFAIvloWRrl0a0cLMYcZAcSOSlpMVYRHWYmxhZzrB2GUKGlXp3LPtpBrMGf-o-5yL-ZmfHJXX4PsECp7wwg-3nPtdMSzBiPDZF2UPVFvbKGAqsliwfzZisGXJVZ3MsSrPjf0eMJibYyojRVCxEzfWhFx_ksGIEGWOc9rFhVGP6ruBO7VcYaEAitFx4vqvZ5tlEcJ7j_mjkiSu6LaU7xwD1lwZrGEGuOex9AknexNwSFTmPGTJW0_KxyZ73AJ7Ii8wLh8ckHUujOOvH3PyPmKqJNz_L-3-tMk5j9GoRaNS2LAUDhuCITC4WQoY64UPBSmkx8hqGHYdtzKmM_V8_F0qDih-WGxs4PvAV24y_ZESL-5NCBJgyJimWA8SWyP5T8uW9NkOPqMMxkXJDm0wBu7MRIxyEsHa8TDQl-iNiw.lYqyW-AXLSd-AEEV.cweZr5AUIY7vh1AnteK10lmdytJCWDXrBKLY0Trlj-kMp8Nyzb8_IAlIaZo.ZmTXFeamO5WlyifXxt2dJQ

decrypted = JOSE::JWE.block_decrypt(private_key, encrypted_rsaoaep).first
puts "Decrypted message: #{decrypted}"
#=> Decrypted message: Jodie Whittaker is the greatest Doctor ever!

Similar to the JWS examples, the first example using a symmetric key, while the second example here uses a public/private key pair. The risks inherent to the symmetric key are just as bad with a JWE. Any third party who intercepts a symmetric key would be able to decode the JWE and read the private data within. With a public/private key pair, the information is more secure. The receiver can send their public key to the sender, who then uses that key to encrypt the information, secure in the knowledge that the token can only be decrypted using the holder of the receiver’s private key.

Conclusion

Libraries for working with JWTs already exist in all major programming languages. Some of them only implement some functionality, and not all, so it’s best to do your research when choosing a library to use. JWT.io provides an extensive (although not complete) list of libraries for creating and using JWTs.

If you have any questions, please feel free to ask below, or reach out to me through Twitter.

Additional Resources

RFC7519 – JSON Web Token

RFC7165 – Use Cases and Requirements for JSON Object Signing and Encryption (JOSE)

JWT.io – An information website created and maintained by security company Auth0. While it does provide a great deal of information, it also ignores the JWE aspects of the JWT spec.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s