A Journey to offline JWT Authentication

Rhythm Chopra
6 min readFeb 25, 2023

--

JWT is a pretty common way of securely transmitting information between two parties. This post is not a deep dive into JWT tokens and their specifications, so I’ll spare the details. Jwt.io would be a great place to start if you wish to understand JWT.

Basically the idea is that sender will sign a json payload (along with appropriate header) with a secret or RSA private key and receiver can then verify the data against the same secret (in case of HMAC) or sender’s RSA public key (in case of asymmetric key pair).

In this post, we’ll mostly be looking into the tokens signed by asymmetric RSA key pair and how we can strengthen the verification process on receiver side.

For validation tokens on the receiver’s side in most common scenarios:

  1. We get the token and RSA public key from a trusted site over a secure channel.
  2. Use an out of the box library in any programming language to validate the token signature against the public key.

For illustrations in this post, we’ll use bash scripts because they’re readily usable and don’t actually need any prior setup. To simplify the process of JWT tampering and validation I have created this repo (jwt-offline-validation) with useful scripts that will come handy during the process.

NOTE: The commands and scripts used in this post are only for demonstration purposes and are not intended to be used in production systems.

As a prerequisite, let’s install a couple of dependencies that will come handy during the process

sudo apt install jq basez
git clone https://github.com/rhythmize/jwt-offline-validation

Let’s get a fresh RS256 encoded token and corresponding Public Key from https://jwt.io/ and validate it locally

fresh JWT from jwt.io
source /path/to/repo/scripts/token_helper.sh

# Create directory structure
mkdir -p valid_token
cd valid_token

# echo -n to drop newline at eof for encoded token
echo -n <encoded token> > token
echo <RSA public key> > public.pem

validate_token `cat token` public.pem

In the console you’ll see that token is valid.

vaild token

For JWT authentication to be effective, we need to establish a root of trust on the receiving side, which in the above case is provided by underlying https connection to https://jwt.io/. And since we are fetching the token and public key directly from the server, we can safely assume that there was no tampering of the JWT or RSA Public Key.

Things get little more interesting when some constraints are added in the above scenario. Specifically when the receiving side has no direct access to the sender, it’s an offline device which relies on some intermediary to fetch the token and the public key from remote server and pass it on to the device.

In this case, we would have no way to establish root of trust on receiving side because token and public key are received over an insecure channel. So there’s a high chance that they were tampered.

To illustrate this, let’s take an expired token from the URI below and try to play around with it.

https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiVGVzdCBVc2VyIiwiaXNzIjoidGVzdF9pc3N1ZXIiLCJpYXQiOjE2NDA5NzU0MDAsImV4cCI6MTY3MjU5NzgwMCwic2NvcGUiOiJ0ZXN0In0.ldru_GBr2Smpzk9ofc8fQ0-3H4xpgTmpDxSYjb01qoKbREGFnB7gs1aXGNcXUK4gj5XmuoFckPhOQ0CEazEoq9kpFVqMUD-pLCdzLGQiK7foCJAY4uADcKjcLm3BFxNMBG6bkNl0RqZRiq773gxTD0RNpORkk7UwJflHgBrRB5gwpyQ3c4GP0yrgj-FQS-aMzuFq7Fs3y5ID88d_9RL-lasYBSsGJYkXkf1PUr-aA8nyD6zxdP3SreRvYbEGkvd_U6pLFfDTYB61MX96bldVsFBdf60fqzkYEdUfPwpEzyeL0_LbsnrP35PF61gNA4L-A10_sDIvtFCH8p5XEJ61vw&publicKey=-----BEGIN%20PUBLIC%20KEY-----%0AMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo%0A4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0%2FIzW7yWR7QkrmBL7jTKEn5u%0A%2BqKhbwKfBstIs%2BbMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh%0Akd3qqGElvW%2FVDL5AaWTg0nLVkjRo9z%2B40RQzuVaE8AkAFmxZzow3x%2BVJYKdjykkJ%0A0iT9wCS0DRTXu269V264Vf%2F3jvredZiKRkgwlL9xNAwxXFg0x%2FXFw005UWVRIkdg%0AcKWTjpBP2dPwVZ4WWC%2B9aGVd%2BGyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc%0AmwIDAQAB%0A-----END%20PUBLIC%20KEY-----

Please copy and paste the link into the browser, creating a hyperlink was modifying the HTML entities in the URI, rendering the token invalid.

mkdir -p ../original_token
cd ../original_token

# echo -n to drop newline at eof for encoded token
echo -n <encoded token> token
echo <RSA public key> public.pem

validate_token `cat token` public.pem

For now this token has valid signature but it is expired (as exptected)

expired token

Currently, the token signature is valid i.e. it’s untampered but the token is expired, which is expected. Now, the intermediary can always modify this expired token, sign it with it’s own RSA Key Pair and provide the receiver with modified token and it’s own public part of the RSA Key (instead of the one received from the actual server).

mkdir -p ../rsa_keypair
cd ../rsa_keypair

# Create new RSA key pair
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -outform PEM -pubout -out public.pem

# Modify original token (update exp claim) and sign with own private key
modify_token `cat ../original_token/token` private.pem > updated_token

# Validate updated token against public.pem
validate_token `cat updated_token` public.pem

NOTE: In case you see the expired token error with validate_token command, feel free to re-run modify_token because tokens are set to expire after 2 mins.

valid modified token

And the token is valid now, we converted an invalid token into a valid one. In the real world scenario, the intermediary will have all the opportunities to tamper the token, sign it with it’s own private key and send the updated token and it’s own public key to the receiver. And since receiver has no way to validate the authenticity of public key, it’ll treat the token as valid and behave accordingly.

Now it gets more important to validate the authenticity of RSA public key itself before validating the token.

One way to ensure the authenticity of RSA public key is that we provide it to the receiver beforehand over a secure channel, like embed it into the firmware of the system.

NOTE: Here of course we’re assuming that the firmware itself will be locked and secured against any type of tampering, otherwise everything falls apart, already!

So now we only rely on intermediary for the token and not the public key. We’ll use the already known public key to validate the token.

# Validate updated token against original_token/public.pem
validate_token `cat updated_token` ../original_token/public.pem
modified token invalid

And if we validate the modified token against the original public key (which is supposed to be communicated over a secure channel), the token verification fails with invalid signature and only the original untampered token issued by the remote server will have a valid signature. Any kind of tampered token will be rendered invalid.

Okay, looks like we found a way to establish root of trust on an offline device. But there’s another catch, what happens when sender decides to rotate it’s Key Pair. After all, it’s a pretty common practice in cryptography to rotate the keys after a certain period.

So once the sender rotates it’s key pair and starts signing the payload with it’s new Private Key, the receiver would render all these tokens as invalid because it’s still using the old public key for validation. So we need to make a firmware update with the new public key (or somehow communicate the new Public Key over the secure channel) and get it updated on the receiver. Until that is done, the receiver would be rendered useless because it cannot validate even the valid token.

In the next post, we’ll try to bridge this gap in the overall flow and build on top of the setup we established until now to make this whole flow rotation agnostic. Please check it out here.

--

--

Rhythm Chopra

Tech Enthusiast | Software Engineer | Trying out new stuff