Generating a JWT token in native NodeJS

Gus Thompson
4 min readJan 19, 2022

Plenty of people have written about what JWT tokens are and what they contain. I wanted to provide easy practical instructions on JWT token generation. This article is part of a series, including:

  • How to generate a JWT signing keys (this story)
  • Generating a JWT token in native NodeJS
  • Decoding and Validating a JWT token in native NodeJS

TL; DR;

NodeJS provides a crypto library natively in the ability to sign a JWT token using rsa private / public key pairs.

// Header
_base64( JSON.stringify(headerObj) ) + '.' +
// Payload
_base64( JSON.stringify(payloadObj) ) + '.' +
// Signature
_base64( crypto.sign(
'RSA-SHA256',
_base64( JSON.stringify(headerObj) ) + '.' +
_base64( JSON.stringify(payloadObj) ),{
key : PRIV_KEY,
passphrase : PASSPHRASE
})
)

In the above example _base64 converts each element from ascii to base64url

Structure of a JWT

A JWT (JSON Web Token) is a small piece of data that has been encoded to a standard with a signature that allows the receiver of that data to validate who sent the data.

Example JWT Token:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.D_GlkbFwmfBAkaL2O07Tv9pK3nBYLwF2IsAdk7_Y29xg1shsuc-QRQub2dICWrSxGeRaTpg1WmOBRojNX6cVxL_LNJTjmPCMu9gdLLElSy47XUy_tqENLXf9dGqvBI5VO4sJIFyLMzf_ZLbJDOnu1Gey_kck5TZ8xYXSYnEgHJpvgltJ6HkJcNF2eoJ_GFgmP7hqa-xrvENbwo1VWFP3MTQbb10EyEmn5GwsONFveim37IOArTHNtzjhod6FIaEdRIPrGIQ9o_b2_Q-myb-gcBB5pO01aGCjyDDvlYx_QsivKrk5jiH1jxJsy5mud6-NspZ1qMKTZd0EZrYXf-fa2MGPJoV1QsqHrEZ6lyFKLyPiK5Y0rmN-dMiaoHzFvcS83-1eMv-TPJyelusojrWe-6a7UW7Fh-kN8GIMKQsl6CkkMEopQ_xxhYSHZa6KtJyiL5Aw29qAJYLAEwwH-Xhfo6uVH5RNCAuD2mWeR4B3yb1IJUj2FGffzl2BJQ_ilnh6Pksrs7R2vuF_HkIekb_3VMFLh-y9w5uH12s9VZei8eygsfGglgN4sJGog5zxCq_1yQ_ey8A3isdfxc--3t6YbOnEj-XbCXm9SOTzP4jWgvFMe4NvREvj4pR2L-SCMV4mzLJLA0QxLuOxdhWTk7my0h8eag0xpk2zAWQQBNu8Hdc

The data in the JWT is encoded into Base64url format and separated into 3 sections:

  • Header
  • Payload
  • Signature

Represented in the token as:

< header > . < payload > . < signature >

In the above token the Header is section of the JWT token upto the first . ie:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9

We can easily do a conversion of this from Base64url using the following:

console.log( Buffer.from('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9', 'base64').toString('ascii') );## Output
## {"alg":"RS256","typ":"JWT"}

The Payload section is contained between the first . and the second . , in the above example it is here:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0## Base64 decoded as
## {"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022 }

Finally the Signature is generated by using (in our case) an RSA265 hash as specified in the header of the rsa private key that generated the token. The rsa public key can be used to verify this was generated by the private key, but itself cannot generate a signature.

This is the magic of JWT tokens using RSA public / private key pairs

Coding the JWT token

Prerequisites

  • Generate a rsa private and public key pair using my other article : How to generate a JWT signing keys
  • Install node : macOS brew install node windows install link
  • In a new folder, start a new project npm init -y

Because we are using standard node libraries no additional libraries or install is required.

The Code

Below is a code from the repo here.

I will breakdown the code line by line to provide a little commentary and hopefully explain what is going on.

const crypto = require('crypto'); NodeJS has included a version of crypto since v0.10 (i.e. from the start). We are using this crypto library to sign the JWT token using an rsa private key

optional const fs = require('fs'); Using NodeJS’s internal file system library allows us to load the private key from an external file. The key can be included in a const or environment variable as well.

optional const PASSPHRASE = process?.env?.passphrase || 'mykey'; rsa private keys allow a passphase to be set on them to make them more secure. This code loads the PASSPHRASE from an environment variable that can be setup during your CI/CD process or defaults to mykey

function _base64( element ) {
return ( Buffer.isBuffer(element) ? element : Buffer.from( element ) ).toString('base64')
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}

The _base64 function converts ascii to base64url format. The base64url¹ format is a part of the JWT standard. This function will convert a string or buffer into a base64url format. We achieve this by leveraging NodeJS’s Buffer class.

const headerObj = {... The header for a JWT token is pretty standard, it must contain the typ: 'JWT' and in our case we are using an rsa key pair so the alg: 'RS256' .

const payloadObj = {... The payload contains the information you wish to share as part of the token.

const PRIV_KEY = fs=readFileSync(__dirname + '\\myRSA256.key', 'utf8'); We load the rsa private key from a file, the key can be encrypted using a passphrase or not, the code will work with either form. Generate your own rsa private and public key pair using this article.

Here is an example rsa private key, save as myRSA256.keythe passphrase for this key is mykey :

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,6E32882A84D36C285A2F483AD0188B3E
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
-----END RSA PRIVATE KEY-----

Finally we get to the build the JWT token and signing it, for ease of testing I just output the token to console log.

_base64(JSON.stringify(headerObj)) + '.' + // Headerand _base64(JSON.stringify(payloadObj)) + '.' + // PayloadInside a JWT the header and payload are both just JSON.stringify‘d and base64url encoded.

_base64( crypto.sign(
'RSA-SHA256',
_base64( JSON.stringify(headerObj) ) + '.' +
_base64( JSON.stringify(payloadObj) ),{
key : PRIV_KEY,
passphrase : PASSPHRASE
})
) // Signature

Inside the JWT the signature is once again base64url encoded, the contents of the encoding is an RSASHA256 hash of the header and payload.

  • crypto.sign is a NodeJS function what provides broad crypto functionality,
  • 'RSA-SHA256' tells crypto to create a SHA256 hash using the rsa key,
  • _base64( JSON.stringify(headerObj) ) + '.' + _base64( JSON.stringify(payloadObj) ), provides the payload or data that is being hashed
  • { key : PRIV_KEY, passphrase : PASSPHRASE } provides the key and passphrase to be used for the hash. If the rsa private key does not contain a passphrase this will still work.

I hope this helps someone, when I first started using JWT they did not make a lot of sense and I though I needed a library to encode and decode them, but the reality is much simpler. They are just base64 encoded, with a signature that can be verified, to help with that process I have a corresponding article about Decoding JWT token using native NodeJS.

[1] : https://datatracker.ietf.org/doc/html/rfc4648#section-5

--

--