Home

Awesome

jwt-pwd · NPM Tests License Neap

jwt-pwd is a tiny crypto helper that helps building JWT (JSON Web Token, pronounced jot), hashing/salting and validating passwords using methods such as md5, sha1, sha256, sha512, ripemd160 and finally encrypt data using either AES or triple DES. It aims at facilitating the development of token based authentication and authorization APIs (e.g., REST, GraphQL). This package wraps the jsonwebtoken package, the native NodeJS crypto package and the node_hash package.

Table of Contents

Install

npm i jwt-pwd

Getting started

Generating & validating JWTs

const Crypto = require('jwt-pwd')
const { jwt } = new Crypto({ secret: 'your-jwt-secret' })
// Or you can also user
// const { jwt } = new Crypto()
// jwt.setKey('your-jwt-secret')

const claims = {
	id:1,
	email: 'you@coolcompany.com'
}

// 1. Create JWT
jwt.create(claims)
	.then(token => {
		console.log(`Your JWT: ${token}`)
		// 2. Validate JWT
		return jwt.validate(token) // validate returns a promise.
	})
	.then(validateClaims => {
		console.log(`User ID: ${validateClaims.id} - User email: ${validateClaims.email}`)
	})
	.catch(err => console.log(`Invalid token: ${err.message}`))

WARNING: If the algorithm uses asymmetric keys, the public key has to be passed as follow to validate the token:

jwt.validate(token, { key:publicKey })

To learn more about using private/public keys, please refer to the example in the Private/public keys for asymmetric algorithms? section.

To change the default algorithm, pass an option parameter as follow:

jwt.create(claims, { algorithm:'HS512' })

The supported cryptographic algorithms are:

The key concept you must understand when it comes to choosing one of those algorithms is that they are mainly split in two categories:

Choose an asymmetric algorithm if you must let clients verifying the JWT without your intervention, othersise choose a symmetric algorithm as they are simpler to start with. Once you've choosen which type of algorithm fits your requirements, choosing a specific algorithm depends on the types of signature your ecosystem supports. If the JWT travels throughout multiple existing systems that must verify its integrity, then do some research on those systems to see what is the most secured common denominator between all those systems (i.e., the most secured asymmetric algorithm they all support), and then choose that one. If you are not constrained by third-party systems, and still need an asymmetric algorithm, ES256 is a good compromise between security and adoption. To learn more about generating keys, please refer to the How to generate a keys? section.

Hashing and salting password

IMPORTANT: Hashing and salting is not the same as encrypting. Please refer to the Auth0 article Adding Salt to Hashing: A Better Way to Store Passwords to learn more. If you want to encrypt data, please jump to the Encrypting data section.

const Crypto = require('jwt-pwd')
const { pwd } = new Crypto()

const password = 'your-super-safe-password'
const alg = 'sha512' // other options: md5, sha1, sha256, sha512, ripemd160

// 1. Hash and salt
const { salt, hashedSaltedPassword } = pwd.hashAndSalt({ password, alg })
console.log(`Encrypted password: ${hashedSaltedPassword} - Salt: ${salt}`)

// 2. Validate
console.log('Password validation result: ', pwd.validate({ password, hashedSaltedPassword, salt, alg })) 
console.log('Password validation result: ', pwd.validate({ password: '123', hashedSaltedPassword, salt, alg }))

Encrypting data

AES (recommended)

The exact cipher is aes-256-cbc.

const Crypto = require('jwt-pwd')
const { encryption } = new Crypto()

const data = { firstName:'Nic', secret:1234 }

// Randomly creates and sets the AES private key.
const encryptionKey = encryption.aes.setKey()
// Randomly creates and sets the initialization vector.
const initializationVector = encryption.aes.setIv()

console.log({
	encryptionKey,
	initializationVector
})

const { cipher, encrypted } = encryption.aes.encrypt(JSON.stringify(data))

console.log({ cipher, encrypted })

const decryptedData= JSON.parse(encryption.aes.decrypt(encrypted))

console.log(decryptedData)

Notice that the AES needs an encryptionKey and an initializationVector to function properly. Those variables must fits certain criteria based on the type of cipher used.

The following snippet shows how to use your own key and iv:

const Crypto = require('jwt-pwd')
const { encryption } = new Crypto()

encryption.aes.setKey(process.env.ENCRYPTION_KEY)
encryption.aes.setIv(process.env.IV)

Triple DES

If you do not wish to use an initialization vector, you can use an older and less secure aldorithm called triple DES as follow:

const Crypto = require('jwt-pwd')
const { encryption } = new Crypto()

const data = { firstName:'Nic', secret:1234 }

// Randomly creates and sets the DES private key.
const encryptionKey = encryption.des.setKey()

console.log({
	encryptionKey
})

const { cipher, encrypted } = encryption.des.encrypt(JSON.stringify(data))

console.log({ cipher, encrypted })

const decryptedData= JSON.parse(encryption.des.decrypt(encrypted))

console.log(decryptedData)

Authorizing HTTP Request With a JWT Token (Express)

The following piece of code assume that a JWT token containing claims { firstName:'Nic' } is passed to each request in the Authorization header. If the request is successfully authenticated, a new claims property is added to the req object. That property contains all the claims. If, on the contrary, the request fails the authentication handler, then a 403 code is immediately returned.

const Crypto = require('jwt-pwd')
const { bearerHandler } = new Crypto({ jwtSecret: 'your-jwt-secret' })

app.get('/sayhi', bearerHandler(), (req,res) => res.status(200).send(`${req.claims.firstName} says hi.`))

bearerHandler(options)

Other Utils

Authorizing HTTP Request With an API Key (Express)

The following piece of code assume that an API key is passed in each request in the header x-api-key (this header key is configurable). If the request is successfully authenticated, the rest of the code is executed. If, on the contrary, the request fails the authentication handler, then a 403 code is immediately returned.

const Crypto = require('jwt-pwd')
const { apiKeyHandler } = new Crypto({ jwtSecret: 'your-jwt-secret' })

app.get('/sayhello', apiKeyHandler({ key: 'x-api-key', value: 'your-api-key' }), (req,res) => res.status(200).send(`Hello`))

NOTE: In this case, the jwtSecret is not involved in any encryption or validation. The apiKeyHandler is just a handy helper.

FAQ

How to generate keys?

The method to generate a keys or secrets depends on your business requirements. If you need to let third parties verify that JWTs have not been tampered, then you need an asymmetric algorithm so you can safely share the public key. If on the other hand signing your JWT is a one-way street, you can use a symmetric algorithm and generate a single secret.

Single key for symmetric algorithm

There are various way to do it. The quickest way is to use the native NodeJS core library crypto as follow:

require('crypto').randomBytes(50).toString('base64')

Alternatively, there are plenty of websites that generate random key such as https://keygen.io/ or https://randomkeygen.com/.

Private/public keys for asymmetric algorithms?

Conceptually, those algorithms require:

  1. Cipher type: Cyber security needs to constantly adapt to new threats. As of 2020, the most commonly used family of cipher on the internet is RSA but it is slowly being replaced by ECDSA.
  2. Key pair: A key pair is made of a private and a public key that meet certain mathematical criteria. Those criteria depends on the type of cipher used. For RSA, the keypair strength is associated with their length, whereas ECDSA uses curves. Those keys are used for signing and encrypting messages. Signature is done with the private key so that the owners of the public key can verify the message was not tampered and that it comes from the owner of the private key. Encryption is done with the public key so that only the owner of the private key can read the message.
  3. Hashing function. With the keypairs generated in the previous step, the message can be signed or/and encrypted using different hashing function. This is typically a SHA type. As of 2020, the most commonly supported SHA is SHA-256.

RSA key pair

  1. Create a private RSA key:
openssl genrsa -out private.pem 2048

With RSA, the key length is what makes it harder to crack. As of 2020, the recommended length is 2048.

  1. Create a public RSA key:
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
  1. Test the algorithm with one of the hashing function (e.g., RS256, RS384, RS512):
const Crypto = require('jwt-pwd')
const fs = require('fs')

const alg = 'RS256'
const privateKey = fs.readFileSync('./private.pem').toString()
const publicKey = fs.readFileSync('./public.pem').toString()
const { jwt } = new Crypto()
jwt.setKey(privateKey)
const claims = {
	id:1,
	email: 'nic@neap.co'
}

jwt.create(claims, { algorithm:alg }).then(token => jwt.validate(token, { key:publicKey, algorithms:[alg] })).then(console.log)

ECDSA key pair

  1. Create a private ECDSA key:
openssl ecparam -genkey -name secp256k1 -noout -out private.pem

With ECDSA, the curve is what makes it harder to crack. As of 2020, a widely accepted curve is secp256k1. To get a list of all the available ECDSA algorithms use this command: openssl ecparam -list_curves

  1. Create a public RSA key:
openssl ec -in private.pem -pubout > public.pem
  1. Test the algorithm with one of the hashing function (e.g., ES256, ES384, ES512):
const Crypto = require('jwt-pwd')
const fs = require('fs')

const alg = 'ES256'
const privateKey = fs.readFileSync('./private.pem').toString()
const publicKey = fs.readFileSync('./public.pem').toString()
const { jwt } = new Crypto()
jwt.setKey(privateKey)
const claims = {
	id:1,
	email: 'nic@neap.co'
}

jwt.create(claims, { algorithm:alg }).then(token => jwt.validate(token, { key:publicKey, algorithms:[alg] })).then(console.log)

Why bearer tokens stored in cookies are not prefixed with bearer?

Some libraries (e.g., axios) can use the access token stored in a cookie to automatically pass it into the Authorization header. When they do so, those libraries may add the Bearer prefix automatically. If the token was already prefixed with Bearer, the resulting token passed in the Authorization headers with be prefixed twice (e.g., Bearer Bearer ...) which would break the server side authentication.

This Is What We re Up To

We are Neap, an Australian Technology consultancy powering the startup ecosystem in Sydney. We simply love building Tech and also meeting new people, so don't hesitate to connect with us at https://neap.co.

Our other open-sourced projects:

GraphQL

React & React Native

General Purposes

Google Cloud Platform

License

Copyright (c) 2017-2019, Neap Pty Ltd. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL NEAP PTY LTD BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

<p align="center"><a href="https://neap.co" target="_blank"><img src="https://neap.co/img/neap_color_horizontal.png" alt="Neap Pty Ltd logo" title="Neap" height="89" width="200"/></a></p>