February 6, 2020

Mocking JSON Web Tokens with Express and Auth0

I'm writing an Express app, integrated with Auth0 for authentication, that has JSON Web Token (JWT) secured endpoints. If you're in the same boat and want to unit test JWT secured endpoints, look no further.

I'm not going to go into the nuts and bolts of JWT here but if you need a primer, here's a couple of resources: One from jwt.io and one from YouTube.

tldr;

If you want to skip to the code and a bare bones description of what needs to be done, check out this companion repo.

Assumptions

If this doesn't apply to you, the general principles should still translate but you'll have to do some extrapolation for your own needs.

The Authorization Middleware

I'm handling authorization with Auth0's jwksRsa and express-jwt packages. A simple middleware implementation would look like this:

const expressJwt = require('express-jwt')
const jwksRsa = require('jwks-rsa')

...

const checkJwt = expressJwt({
  audience: 'some-audience',
  issuer: 'https://myapp.auth0.com/',
  algorithms: ['RS256'],

  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'https://myapp.auth0.com/.well-known/jwks.json'
  }),
})

app.use(checkJwt)

With this code JWT authorization is required for every Express endpoint in your app. So, how could we unit test one of these?

Well, we need to...

  1. Generate a JWT that the checkJwt middleware thinks is valid.
  2. Attach said JWT to the request.
  3. Profit.

Understanding Our Middleware

First, a useful digression. How exactly is our middleware validating tokens?

Let's look at that expressJwt instantiation again:

expressJwt({
  audience: 'some-audience',
  issuer: 'https://myapp.auth0.com/',
  algorithms: ['RS256'],

  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'https://myapp.auth0.com/.well-known/jwks.json'
  }),
})

Some of these are straightforward, the audience and issuer come from our Auth0 project, algorithms: ['RS256'] looks like it's specifying that our JWT uses RSA.

Then there's secret. It's using this jwksRsa.expressJwtSecret thing which looks like it's probably triggering a request for a weird file called jwks.json which must do... something?

WTF is jwks.json??

As it turns out, jwks.json is real important here. JWKS stands for JSON Web Key Set which is basically a JSON object that specifies a set of public keys for validating JWTs that have been signed with RS256.

If you want to know what it looks like, just visit that https://myapp.auth0.com/.well-known/jwks.json and take a peak. You should see something like this:

This here folks gives our middleware the rest of the information to validate the token that came with the request. You are probably wondering what this stuff means... Here's the docs but the gist is:

kid, n and e turn out to be the important bits:

If you're unfamiliar with RSA you should read my post on asymmetric encryption but basically if you know n and e you can validate that something signed with an RSA key is legit.

kid is an identifier for which key among the key set should be used to validate the token.

So, end to end what's happening?

Auth0 is using a private RSA key to generate the JWT that was included in the request and the JWKS lets our middleware verify it.

Epiphany! If we can generate a JWT using a private RSA key we know, then intercept the jwks.json request and respond with a JWKS object that has our RSA key's n and the e values the token should validate.

So, let's get ourselves a dang RSA key.

Generating an RSA key

We're gonng use a Python REPL for this. Do the following:

$ python3
>>> from Crypto.PublicKey import RSA
>>> key = RSA.generate(2048)

We've got ourselves an RSA key. Write it to a file like so:

>>> f = open('mykey.pem','wb')
>>> f.write(key.exportKey('PEM'))
>>> f.close()

Finally, we need n and e. Turns out they are properties on the key object we just generated:

>>> key.n
24086915249478732991938203669055712768534500988438861615427827285552016416527701655104314858980987156088353691818974778626460739552545113785775327192323958973552716763686395735410867984308112435247222824478851256029180301938533117606478881913972835778389337664671212086750112908465188999663747594934470615710417122058028672874117866458305850029277470948053163457976528276738784340660575757377821742177301712425884163931617846708080422121157930489954360307618338831641623216785077400373075504182583579973396951740867260363486495761206503200810323528514399011109759541765601949070711097826312283096624099275945165911113
>>> key.e
65537

Now, you might notice, these are numbers. In the jwks.json file they were a string. It may not be obvious but it's not standard Base64 encoding. So what is it?

Encoding Our n and e Values

With some digging I found this documentation which says that they are encoded as "Base64urlUInt." Oookaaay... It took me a minute but this Stack Overflow post shows a real easy way to encode these suckers properly.

You'll need the pyjwkest module. Install it from the command line with pip:

$ pip install pyjwkest

Then:

$ python3
>>> from jwkest import long_to_base64
>>> long_to_base64(65537)
'AQAB'

Look at that, AQAB matches the e value in the auth0 jwks.json file. We must be onto something. Do the same with that big old number n as well (spoiler, this one won't match).

Taking Stock

Let's consider what we've got so far:

  1. We have a private key (which you saved in mykey.pem.
  2. We have a properly encoded n value.
  3. We have a properly encoded e value.

This is everything. We can generate our token with the private key and mock a valid jwks.json using n and e.

Tricking Our Secured Endpoint

Now that we've got the key, we need to do three things.

  1. Intercept the request for https://myapp.auth0.com/.well-known/jwks.json
  2. Reply with our own mocked keyset data.
  3. Generate a JWT using our private key.

Intercepting The Request

We can intercept requests made by our application code using the nock package. It's pretty simple:

const nock = require('nock')

nock('https://myapp.auth0.com')
  .persist()
  .get('/.well-known/jwks.json')
  .reply(200, nockReply)

With this instantiated, if a request is made to https://myapp.auth0.com/.well-known/jwks.json, nock will intercept it and reply with nockReply. So what should nockReply be?

Replying With Mocked Keyset Data

nockReply needs to look like the object in jwks.json at that URL, although not every field is important. Here's what you need:

const nockReply = {
  keys: [{
    alg: 'RS256',
    kty: 'RSA',
    use: 'sig',
    n: '<your-encoded-n-value>`,
    e: '<your-encoded-e-value>`,
    kid: '0',
  }]
}

Recall that we got our final n and e values by encoding them with long_to_base64(...). Those are the strings you'll enter here. n will be really long and e should be AQAB.

We're also setting kid to 0 for simplicity. You'll see how it works later.

Generating A JWT With Our Private Key

Let's write some code to generate a friggen JSON Web Token. We can use the node-jsonwebtoken package for this:

const jwt = require('jsonwebtoken')

...

const getToken = () => {
  const email = 'someone@gmail.com'

  // A real payload will generally have a lot of stuff in it
  // these fields are common but it doesn't really matter.
  const payload = {
    nickname: email.split('@').shift(),
    name: email,
  }
  
  const options = {
    header: { kid: '0' }, // Needs to match the `kid` in our jwks.json mock 
    algorithm: 'RS256', // Needs to match our expressJwt instance
    expiresIn: '1d',
    audience: 'some-audience', // Needs to match our expressJwt instance
    issuer: 'https://myapp.auth0.com/', // Needs to match our expressJwt instance
  }

  let token
  try {
    token = jwt.sign(payload, privateKey, options)
  }
  catch(err) {
    console.log(err)
    throw err
  }

  return token
}

module.exports = {
  getToken
}

By now, hopefully what's in options makes sense. The most important thing here is token = jwt.sign(payload, privateKey, options).

Also note that we're including the same kid in the header for this JWT, that allows us to match the one in the mocked jwks.json.

Now, copy your private key from mykey.pem and paste that sucker into a const above your getToken() code like so:

const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwaZ3afW0/zYy3HfJwAAr83PDdZvADuSJ6jTZk1+jprdHdG6P
...
bKhNO4+rlwh2VzIi5tVMQTYoUbaab8n17fdNxtfTsG0h4vj8q+7ab59GYf1TKn0R
JEn/NS0gRKfNh6bwZaSTfhFxALmKApVNTPm2UT9G5hADTcw4xTQf
-----END RSA PRIVATE KEY-----`

That's it! We've go the code to generate a token that our endpoint will validate.

Adding The Generated JWT To The Request

Here's a re-usable function making auth'd calls from unit tests (make sure to npm i --save request-promise-native).

const request = require('request-promise-native')
const jwtMock = require('./fixtures/jwt.js')

...

async function makeAuthdRequest(method, uri, body) {
  // Generate that gd token!
  const token = jwtMock.getToken()

  return await request({
    baseUrl: `http://localhost:${port}`,
    method,
    uri,
    body,
    // This is the important bit.
    headers: {
      Authorization: `Bearer ${token}`
    },
    resolveWithFullResponse: true,
    json: true,
  }).catch(err => {
    console.dir(err.message)
    throw(err)
  })
}

...
jwtMock (which I saved at ./fixtures.jwt.js contains all the code we set up at the beginning, the private key, the nock, the getToken(), etc.

Now you can call this function, passing the method (POST, GET, etc), the URI (like /user/:id), and the request body as a JSON object. It'll automagically add a token that'll pass authorization.

Remember, I've got a repo set up that demo's all of this. Check it out.

Enjoy!

Comments powered by Disqus