DEV Community

Cover image for Zero Trust at the Edge (part 2)
Tom Mount
Tom Mount

Posted on

Zero Trust at the Edge (part 2)

Creating and Validating your own JSON Web Tokens

Take charge of your authorization needs.

Recap

In Part 1 we looked at what a JSON Web Token is, how it's created, and how it's validated. If you need a primer, go back and read that. I'm going to assume you're broadly familiar with JWTs from here on out.

Also, you can roll your own JWT creation and validation code. You definitely should not do that. There are any number of libraries that will do this for you that have been tested, vetted, and tested again, from reputable organizations like Okta, Auth0, etc. These are companies that do authz as a core business and pay people quite comfortably to maintain these packages and respond to security issues. Do not roll your own crypto. You have been warned.

(The featured image for this post comes from Classic Programmer Paintings, a disaster of a scene titled "We rolled our own crypto.")

The process for signing, and later validating, your JWTs is pretty straightforward, such that a reasonably competent JavaScript developer could probably write their own library to do that using only the built-in crypto libraries for either Node or the Web Crypto API. But I'm a firm believer in not reinventing the wheel unless absolutely necessary. When you're building out performant edge-based applications, you should absolutely use a trusted third-party library; but you also should take into account the size of that library and how well it will perform in an edge environment. Most edge compute platforms have restrictions in place around how long any request can go unanswered, how much CPU time and memory can be used by the script, and the response size. When I built out this example for my former employer, those restrictions were very tight and so I ended up using the jsrsasign library instead of the more common express-jwt or jwt-decode libraries from Okta; those libraries were either too big to run properly in my environment, and/or they relied on the Web Crypto API, which was not available in the edge execution environment.

Validation

Setting up a validation middleware (assuming you aren't using express-jwt directly) with jsrsasign is pretty simple. The middleware should do the heavy lifting of parsing the token, and on success, it should put the payload's token into the request bag and allow the request to continue. Any failure (bad algorithm in the header, tampered token, etc.) should short-circuit the request and throw an appropriate error. This middleware can then be imported into your main Express server and applied on any route or routes that require token validation.

The most common ways of sending a token with a request are to use a request cookie or an Authorization header, but those aren't the only ways. For this demo, though, we'll stick to an authorization header in the format Authorization: Bearer [token].

To begin, start off with a basic Express server, along with the jsrsasign library:

npm install express body-parser cors jsrsasign jsrsasign-util
Enter fullscreen mode Exit fullscreen mode

I also use dotenv which automatically loads variables from .env when you enter a directory, and unloads them when you leave the directory. Set up a .env file with a few values:

PORT=3000
JWT_SECRET=your-256-bit-secret
Enter fullscreen mode Exit fullscreen mode

Your express server should look something like this:

const express = require('express')
const path = require('path')
const cors = require('cors')
const bodyParser = require('body-parser')

const app = express()
app.use(express.static(path,join(__dirname, 'public')))
app.use(cors())
app.use(bodyParser.json())

const port = process.env.PORT || 3000
app.listen(port, () => {
  console.log(`Server running on port ${port}`)
})
Enter fullscreen mode Exit fullscreen mode

Let's build out the validation middleware. Create a middleware/Validate.js file. (Most of the critical functionality is in the comments so I won't go into more detail here.)

const { KJUR, KEYUTIL } = require('jsrsasign')
const { readFile } = require('jsrsasign-util')
const path = require('path')

const fromBase64Url = (str) => Buffer.from(str, 'base64url').toString()

const jwtSecret = process.env.JWT_SECRET
const publicKey = KEYUTIL.getKey(readFile(path.join(__dirname, '..', '..', 'certs', 'pubkey.pem')).replace(/[\n\r]+/g, ''))

const validate = function(req, res, next) {
  // Get the token from the Authorization header.
  const token = req.headers['authorization'].replace('Bearer ', '')

  // Alternatively, you can take it from a cookie; here we're looking for one called "jwt".
  // const cookies = req.headers.cookie.split('; ')
  // const token = cookies.find(c => /^jwt=/.test(c)).split('=')[1].trim()

  // Get the token header, payload, and signing algorithm used.
  const [ header, payload ] = token.split('.')
  const { alg } = JSON.parse(fromBase64Url(header))

  let validationComponent = null,
      isValid = false

  try {
    if (/^HS/.test(alg)) {
      // HSxxx algorithms use a shared secret.
      validationComponent = jwtSecret
    } else if (/^[REP]S/.test(alg)) {
      // RSxxx, ESxxx, and PSxxx algorithms all use a public key.
      validationComponent = publicKey
    } else {
      // Those are the only options; if another one was specified, throw an error.
      console.log(`Invalid algorithm specified: ${alg}`)
      res.status(501).send('Not Implemented')
    }
  } catch (e) {
    console.log(e)
    res.status(500)
  }

  // Validate the JWT using the jsrsasign library.
  isValid = KJUR.jws.JWS.verifyJWT(token, validationComponent, { alg: [alg] })

  if (isValid) {
    // Only if the signature is valid, decode the payload and add it to the request.
    req.payload = JSON.parse(fromBase64Url(payload))
    next()
  } else {
    // If the signature is invalid, abort the request with a 401.
    res.status(401).send('Unauthorized')
  }
}

module.exports = validate
Enter fullscreen mode Exit fullscreen mode

Note that for the asymmetric algorithms (basically anything other than HMAC SHA) we need a public key. I chose to add them in a certs directory outside of the Express server's src folder:

Folder Structure

For simple testing purposes, I went to https://jwt.io/ and changed the "Algorithm" dropdown to RS512. Towards the bottom of the screen, in the "Verify Signature" area, they include both the public and private keys used to sign their sample JWT. I simply copied both into the privkey.pem and pubkey.pem files you see above.

The middleware here is aware of both the shared secret key and the public key. When invoked, it parses the JWT from whatever part of the request has the JWT, validates it, and if successful, stores the decoded payload in the request. If validation fails for any reason, the middleware cancels the request.

The last thing we need to do is add the middleware to a route. Back in src/index.js, let's create a new API route and protect it with our JWT middleware:

//...
const bodyParser = require('body-parser')
const validate = require('./middleware/Validate.js')

// ...
app.use(cors())
app.use(bodyParser.json())

app.post('/api/validate', validate, (req, res) => {
  res.json(req.payload)
})
//...
Enter fullscreen mode Exit fullscreen mode

We import the middleware from the file we just created, then supply the method as the second argument in our route handler for /api/validate. This will ensure that the middleware is invoked before any code in the handler.

To test, we can use Postman to re-use requests, save token values, etc.; but just for a quick test it's easy to use curl on the command line. Make sure you start your server with node src/index.js, then run the following curl command:

curl -X POST http://localhost:3000/api/validate -H "Authorization: Bearer [token]"
Enter fullscreen mode Exit fullscreen mode

Make sure to replace [token] with a token from jwt.io. If everything is wired up properly, you should get back the payload of the token you sent in the header.

$ curl _X POST http://localhost:3000/api/validate -H "Authorization: Bearer [token]
{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}
Enter fullscreen mode Exit fullscreen mode

Putting it All Together

Now that you have a functioning middleware, you can build out other routes that require authorization. Try creating another API route that returns some other piece of static data, for example using the name portion of the payload in the response (eg. Hello {name}!). Similar to Part 1, perhaps you can update the middleware to add elements from the payload as request headers and then dispatch the request to https://httpbin.org/anything to inspect the final request.

The concept of an inline token validator doesn't have to stop with Express, either. Most modern web frameworks, such as Next.js or Nuxt, have their own concept of middleware. With just a few modifications, this concept can be applied to those frameworks to provide much richer front-end experiences.

Check It Out

The code for this Express server with middleware is on my GitHub at https://github.com/tmountjr/express-jwt. Feel free to clone it, play around with it, etc.!

Top comments (0)