Simple JWT Implementation in Node.js: Asymmetric Variation

7 April 2019

In the previous article, I explained how you can implement authorization and authentication via JWT using symmetric algorithms such as HS256. Now, I'll try to explain asymmetric approach (RS256).

You might ask, why do we need another way of creating JWT? Well, this answer can help you to understand why.

Workflow of using JWT still is the same that we used in the symmetric approach. The only difference is that now we don't use secret for creating and validating access token. Instead, we need RSA public/private pair of keys. We use private key only for creating JWT, and public key only for validating it. So, you don't have to keep some secret information for validating tokens. You can generate such pair of keys using following commands:

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem

So, for generating JWT we do following

const encodedHeaderInBase64 = base64UrlEncodeJSON(header)
const encodedPayloadInBase64 = base64UrlEncodeJSON(payload)
const encodedSignatureInBase64 = generateSignature(`${encodedHeaderInBase64}.${encodedPayloadInBase64}`, privateKey)
const token = `${encodedHeaderInBase64}.${encodedPayloadInBase64}.${encodedSignatureInBase64}`

Where generateSignature in this case is

const crypto = require('crypto')

function generateSignature (str, privateKey) {
  const sign = crypto.createSign('RSA-SHA256')
  sign.update(str)
  return sign.sign(privateKey, 'base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
}

And base64UrlEncodeJSON is the same.

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

And for validation we can use following function

// 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 !== 'RS256' || header.typ !== 'JWT') {
    return false
  }
  const signature = parts[2]
  const exp = payload.exp
  if (exp) {
    if (exp < new Date().getTime()) {
      return false
    }
  }
  const verify = crypto.createVerify('RSA-SHA256')
  verify.update(unescape(`${parts[0]}.${parts[1]}`))
  return verify.verify(publicKey, signature, 'base64')
}

function base64UrlDecodeToJSON (str) {
  return JSON.parse(
    Buffer.from(
      unescape(str), 'base64'
    ).toString('utf8')
  )
}

function unescape (str) {
  return str.replace(/-/g, '+').replace(/_/g, '/')
}

For testing your access token you can also use this service. Just change alg property to RS256.

Also, I just released cutie-jwt. So, if you want to use JWT in the declarative style, you should definitely try it.

References