Simple JWT Implementation in Node.js: Symmetric Variation

6 April 2019

In this article, I'll explain how easily you can implement authorization and authentication via JWT using only standard modules in Node.js. So, it might be interesting to you, if you really want to know how it works.

If you don't know what JWT is, you can read this article first. In few words, JWT is a JSON-based open standard for creating access tokens. Let's say you have a system with some REST API, and you want to securily detect a user who calls methods from this API. JWT is quite simple and effective solution for this problem.

JWT is a string that has following format:

'encodedHeaderInBase64.encodedPayloadInBase64.encodedSignatureInBase64'

Let's go through typical workflow of using JWT.

  1. User signs in a system with some sensitive data like password and simple user data like email or user name. After successfull authentication user gets an access token from server. This token has expiration time, so user cannot use it forever.

  2. We save it to local storage, so user can always have it for calling REST API methods.

  3. Every time user calls a method from API, we send JWT in headers of request and server verifies that token. If it's valid we are allowed to use API method, otherwise we must sign in the system again to get new access token. It's important to use secure protocols like https, because JWT is sensitive data.

  4. If you signs out the system, we delete access token from local storage.

For creating JWT we need two objects: header and payload.

Header is a simple json:

{
 "alg" : "HS256",
 "typ" : "JWT"
}

alg identifies which algorithm is used to generate the signature, and type tell us that we use JSON Web Token. I would recommend you to hardcode this object as a header, if you want to use symmetric algorithms. I don't see any other reason to parametrize this object, so you can avoid a lot of checks on alg property and reduce complexity of your code. Or you can use asymmetric algorithms such as RS256, and allow your users to identify themselves via their public keys. But in this article we will consider only symmetric approach (HS256).

Payload is an object that identifies your user, so it has to contain unique data for specific user like email. It can also store some other user data, but you definetely shouldn't store there sensitive information like password. Also, it's useful to store expiration time of access token there.

{
  "email": "some@email",
  "exp": 1554540588448
}

You can use following function to add expiration time to your payload:

function payloadWithExpirationTime (payload, minutesFromNow) {
  let date = new Date()
  date.setMinutes(date.getMinutes() + minutesFromNow)
  payload.exp = date.getTime()
  return payload
} 

You might wonder why some property names are just three charatecrs long. I have no idea. I read that's sort of some optimisations, but I don't think that's so important.

So, we have header and payload as json structures. And we need to convert them in base64 encoded strings. For doing this, use following function:

function base64UrlEncodeJSON (json) {
  return Buffer.from(
    JSON.stringify(json)
  ).toString('base64')
   .replace(/\+/g, '-')
   .replace(/\//g, '_')
}

Now we have to generate a signature for our token. For doing that, we need a some secret. It's a just simple string that only your server knows and it's very important to not compromise it.

Use following function to generate a signature:

const crypto = require('crypto')

function generateSignature (str, secret) {
  return crypto
      .createHmac('sha256', secret)
      .update(str)
      .digest('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
}

Function generateSignature also encodes string to base64.

So, for creating our access token we do something like this:

const encodedHeaderInBase64 = base64UrlEncodeJSON(header)
const encodedPayloadInBase64 = base64UrlEncodeJSON(payload)
const encodedSignatureInBase64 = generateSignature(`${encodedHeaderInBase64}.${encodedPayloadInBase64}`, 'some-secret')
const token = `${encodedHeaderInBase64}.${encodedPayloadInBase64}.${encodedSignatureInBase64}`

Now we need to be able to verify our token from 'Authorization' request header:

// Returns true if token is valid, otherwise returns false
function isValid (token, secret) {
  const parts = token.split('.')
  const header = base64UrlDecodeToJSON(parts[0])
  const payload = base64UrlDecodeToJSON(parts[1])
  if (header.alg !== 'HS256' || header.typ !== 'JWT') {
    return false
  }
  const signature = parts[2]
  const exp = payload.exp
  if (exp) {
    if (exp < new Date().getTime()) {
      return false
    }
  }
  return generateSignature(`${parts[0]}.${parts[1]}`, secret) === signature
}

function base64UrlDecodeToJSON (str) {
  return JSON.parse(
    Buffer.from(
      str.replace(/-/g, '+').replace(/_/g, '/'), 'base64'
    ).toString('utf8')
  )
}

For testing your access token I would suggest this service.

So, that's it. You can also check this sample app that can show how you can use JWT with Google OAuth and GitHub OAuth.

References