In this article, you’ll learn how to generate JSON Web Tokens, commonly referred to as JWTs, in Node.js using TypeScript. Now, I could have just used JavaScript, but bear with me because TypeScript is now widely adopted by companies. It has become the go-to language for modern web development, and I can’t even remember the last time I built a web app without TypeScript.

We’ll dive deep into the process of generating JWTs, exploring different algorithms such as HMAC-SHA256 (HS256) and RSA-SHA256 (RS256), and discussing the pros and cons of each. But don’t worry, I won’t start with a long lecture about JWTs. Instead, we’ll jump straight into the implementation. Towards the end of the article, I’ll highlight the key aspects of HMAC-SHA256 (HS256) and RSA-SHA256 (RS256), along with their respective advantages and disadvantages.

Once we’ve covered the functions responsible for generating JWTs, we’ll also create associated functions for verification. These functions will ensure the integrity of the tokens and validate the authenticity of the transmitted data. With these techniques, you can have confidence in the security of your tokens. So, without further ado, let’s get started on this exciting journey of mastering JWT generation in Node.js with TypeScript.

More practice:

How to Generate and Verify JSON Web Tokens in Node.js

Sign and Verify JWTs using the HS256 Algorithm

Let’s start by diving into the process of signing and verifying JSON Web Tokens (JWTs) using the HS256 algorithm. This algorithm is a widely adopted method known as HMAC-SHA256, which plays a crucial role in securing JWTs and maintaining their integrity.

With the HS256 algorithm, we can easily sign and verify JWTs using cryptographic hashing and a shared secret key. Don’t worry if you’re unfamiliar with these concepts; we’ll delve deeper into them shortly.

To ensure the safety of the shared secret key, we’ll store it in a secure location known as a .env file. Let’s add the following environment variables to your .env file:

.env


# -----------------------------------------------------------------------------
# For HS256 Algorithm
# -----------------------------------------------------------------------------
JWT_SECRET_KEY=my_ultra_secure_jwt_secret_key
JWT_EXPIRES_IN=60

Make sure to replace my_ultra_secure_jwt_secret_key with your own secret key. To ensure the security of your JWTs, it is crucial to use a secret key that is both unique and hard to guess. This uniqueness and complexity help to maintain the confidentiality and integrity of your JWTs, making it more difficult for unauthorized parties to tamper with or access sensitive information.

Additionally, the JWT_EXPIRES_IN value specifies the token’s expiration time, which is set to 60 minutes (1 hour) in this example. Feel free to adjust this value according to your application’s requirements.

Next, we’ll proceed to create a helper function that retrieves the JWT secret and token expiration variables we stored in the .env file. To achieve this, we’ll leverage the dotenv package, which loads the environment variables into the Node.js runtime. By utilizing square bracket notation, we can access these variables from the process.env object.

You can add the following code snippet to your utility functions. Typically, this code would reside in a file named helpers.ts within the src/utils directory. However, for this example, we’ll place the code in a file called src/helpers.ts.

src/helpers.ts


require("dotenv").config();

export function getEnvVariable(key: string): string {
  const value = process.env[key];

  if (!value || value.length === 0) {
    console.error(`The environment variable ${key} is not set.`);
    throw new Error(`The environment variable ${key} is not set.`);
  }

  return value;
}

Make sure to customize the file path and location to match your project’s structure. The getEnvVariable function fetches the value of an environment variable using a specified key. It ensures that the variable is both defined and has a non-empty value. If the variable is missing or empty, an error will be triggered, and a corresponding message will be displayed in the console.

Next, let’s install the jsonwebtoken package, which will enable us to sign and verify the JSON Web Token. Once installed, add the following import statements to your src/utils/token.ts file. However, for the purpose of this example, we will place the code in a separate src/token_hs256.ts file.

Here is the code snippet you need to include:

src/token_hs256.ts


import jwt, { SignOptions } from "jsonwebtoken";
import { getEnvVariable } from "./helpers";

Function to Sign the JWT with the HS256 Algorithm

To sign a JWT using the HS256 algorithm, we’ll define a function called signJwt. This function takes two parameters: payload and options.

The payload parameter holds the data you want to include in the JWT. It should be an object containing the necessary key-value pairs for encoding.

The options parameter allows you to specify additional options for signing the token, such as the token’s expiration time, not before (nbf) time, and audience (aud) claim, among others. These options are provided through the SignOptions type.

src/token_hs256.ts


export const signJwt = (payload: Object, options: SignOptions) => {
  return jwt.sign(payload, getEnvVariable("JWT_SECRET_KEY"), {
    ...(options && options),
    algorithm: "HS256",
  });
};

Within the function, we’ll utilize the jwt.sign() method to sign the token. This method takes the payload, which represents the data to be included in the JWT, along with the JWT secret key obtained using the getEnvVariable helper function.

Additionally, an options object can be provided, which can be merged with any additional options using the spread syntax. In our case, we’ve hardcoded the algorithm to “HS256” to utilize the HS256 signing algorithm. Once the token is signed, the function will return the generated JWT.

Function to Verify the JWT with the HS256 Algorithm

Now, let’s move on to creating a function that will verify the JWT and extract the stored payload. We’ll call this function verifyJwt. It takes a token parameter, which represents the JWT string to be verified.

Here’s the code for the verifyJwt function:

src/token_hs256.ts


export const verifyJwt = <T>(token: string): T | null => {
  try {
    const decoded = jwt.verify(token, getEnvVariable("JWT_SECRET_KEY")) as T;

    return decoded;
  } catch (error) {
    return null;
  }
};

We made this function generic so that we can specify the structure of the payload that verifyJwt will return. Inside the function, we use the jwt.verify method to verify the authenticity of the JWT. It decodes the JWT and validates its signature using the JWT secret key obtained from the getEnvVariable helper function. The decoded JWT payload is then assigned to the decoded variable, ensuring it matches the generic type T specified in the function signature.

Since the verification process can potentially encounter errors, we wrap it in a try-catch block to handle any exceptions. If the verification is successful and no errors occur, the decoded payload is returned from the function.

However, if an error does occur during verification, such as an invalid or tampered token, the function returns null, indicating that the token could not be verified.

Sign and Verify the JWT with the HS256 Algorithm

Having implemented the functions for signing and verifying JWTs, it’s time to put them to the test. We’ll demonstrate their usage in action to ensure that the code we’ve written functions correctly. Typically, you would utilize the signJwt function in a login route handler and the verifyJwt function in a middleware guard. However, for the purpose of testing the logic, we’ll call them subsequently in the same file.

Include the following code in your file:

src/token_hs256.ts


const user = {
  id: "3894stve8376gdhdj663h",
  name: "Admin",
  email: "admin@admin.com",
};

// Sign the JWT
const token = signJwt(
  { sub: user.id },
  {
    expiresIn: `${getEnvVariable("JWT_EXPIRES_IN")}m`,
  }
);

console.log({ token });

// Verify the JWT
const payload = verifyJwt<{ sub: string }>(token);
if (payload) {
  console.log("✅Token is valid");
} else {
  console.error("🔥 Token is invalid or user doesn't exists");
}

To compile and run the code defined in the src/token_hs256.ts file, you’ll need to install the tsx package. Once installed, run the following command in your terminal to see the output in the terminal:


pnpm tsx ./src/token_hs256.ts

Sign and Verify JWTs using the RS256 Algorithm

Now it’s time to sign and verify the JWTs using the RS256 algorithm. RS256, which stands for RSA-SHA256, utilizes asymmetric encryption with a public-private key pair. This algorithm is typically used in scenarios where you need to verify the authenticity of the JWT from different entities or systems.

RS256 is commonly used in multi-party systems, where the JWT is signed by one party (using the private key) and verified by another party (using the public key). This approach provides enhanced security and allows for more flexible trust relationships.

In this example, we will use the private keys to sign both access and refresh tokens. The access token will grant access to protected routes, while the refresh token will only be exchanged for a new access token. Subsequently, we will utilize the corresponding public keys to verify the authenticity of the access and refresh tokens.

Generate the RSA Private and Public Keys

To sign the access and refresh tokens, we need to generate RSA key pairs. There are multiple ways to generate RSA keys, but for simplicity and speed, I recommend using OpenSSL. Follow the steps below to generate the private and public keys:

1. Generate the private key by running the following command:


openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:4096

This command will create a file named private_key.pem containing the private key.

2. Derive the corresponding public key from the private key using the following command:


openssl rsa -pubout -in private_key.pem -out public_key.pem

The public key will be saved in the public_key.pem file.

Now that you have the private and public keys, you can add them to your environment variables. However, keep in mind that directly adding the keys to the .env file may cause warnings when using Docker Compose to read environment variables. To avoid this, follow these additional steps:

  1. Convert the private key to base64 format by copying its contents from private_key.pem and encoding it using a base64 encoder tool.
  2. Add the base64-encoded private key as the value of the ACCESS_TOKEN_PRIVATE_KEY variable in the .env file.
  3. Similarly, convert the public key to base64 format by copying its contents from public_key.pem and encoding it.
  4. Add the base64-encoded public key as the value of the ACCESS_TOKEN_PUBLIC_KEY variable in the .env file.

These steps are specifically for the access token. Now you need to repeat the same process for the refresh token’s RSA key pair. Generate a new private key, derive the corresponding public key, and encode them in base64 format. Add the encoded private key as the value of the REFRESH_TOKEN_PRIVATE_KEY variable, and add the encoded public key as the value of the REFRESH_TOKEN_PUBLIC_KEY variable in the .env file.

.env


# -----------------------------------------------------------------------------
# For RS256 Algorithm
# -----------------------------------------------------------------------------

ACCESS_TOKEN_EXPIRES_IN=15
REFRESH_TOKEN_EXPIRES_IN=60

ACCESS_TOKEN_PRIVATE_KEY=
ACCESS_TOKEN_PUBLIC_KEY=
REFRESH_TOKEN_PRIVATE_KEY=
REFRESH_TOKEN_PUBLIC_KEY=

If you’re wondering why we chose a key size of 4096 bits, it’s because the jsonwebtoken package recommends a minimum key size of 2048 bits for RSA keys. By using a larger key size of 4096 bits, you ensure even greater security and stay on the safe side in case the minimum recommended limit increases in the future.

Function to Sign the JWT with the RS256 Algorithm

Now that we have generated the RSA key pairs for signing both the access and refresh tokens, we can proceed to create a function that will handle the signing process. Typically, we would need separate functions for signing the access token and the refresh token. However, thanks to the power of TypeScript, we can create a single function that can handle both cases.

Let’s define the signJwt function for signing the JWT using the RS256 algorithm. It takes three parameters:

  1. payload: The payload object to be included in the JWT. This object typically contains information such as the user’s ID, expiration time, or any custom data.
  2. keyName: A string that specifies which private key to use for signing the JWT. It can be either “ACCESS_TOKEN_PRIVATE_KEY” or “REFRESH_TOKEN_PRIVATE_KEY“.
  3. options: Additional options that can be passed to the jwt.sign method.

Here’s the implementation of the signJwt function:

src/token_rs256.ts


export const signJwt = (
  payload: Object,
  keyName: "ACCESS_TOKEN_PRIVATE_KEY" | "REFRESH_TOKEN_PRIVATE_KEY",
  options: SignOptions
) => {
  const privateKey = Buffer.from(getEnvVariable(keyName), "base64").toString(
    "ascii"
  );
  return jwt.sign(payload, privateKey, {
    ...(options && options),
    algorithm: "RS256",
  });
};

In this function, we first retrieve the base64-encoded private key from the environment variables using the getEnvVariable function. We then convert it back to ASCII format. Next, we call the jwt.sign method with the payload, the retrieved private key, and any additional options provided. Finally, we return the signed JWT.

Function to Verify the JWT with the RS256 Algorithm

Now let’s proceed to create a function that will verify the integrity and authenticity of the JWT and extract the payload stored in it. We will call this function verifyJwt, which is a generic function that allows us to specify the structure of the payload during its invocation. It takes two parameters:

  1. token: The JWT string that needs to be verified.
  2. keyName: A string that specifies which public key to use for verification. It can be either “ACCESS_TOKEN_PUBLIC_KEY” or “REFRESH_TOKEN_PUBLIC_KEY“.

Here’s the implementation of the verifyJwt function:

src/token_rs256.ts


export const verifyJwt = <T>(
  token: string,
  keyName: "ACCESS_TOKEN_PUBLIC_KEY" | "REFRESH_TOKEN_PUBLIC_KEY"
): T | null => {
  try {
    const publicKey = Buffer.from(getEnvVariable(keyName), "base64").toString(
      "ascii"
    );
    const decoded = jwt.verify(token, publicKey) as T;

    return decoded;
  } catch (error) {
    console.log(error);
    return null;
  }
};

Within this function, we first retrieve the base64-encoded public key from the environment variables using the getEnvVariable function. Then, we convert it back to ASCII format.

Next, we call the jwt.verify method with the token and the retrieved public key to verify the validity and integrity of the token. If the verification is successful, the decoded payload is returned as type T. However, if an error occurs during the verification process, it is logged to the console, and null is returned.

Sign and Verify the JWT with the RS256 Algorithm

It’s time to put the signJwt and verifyJwt functions to the test. In a typical project, the signJwt function is used to sign the access and refresh tokens within the login route handler, after performing other authentication methods on the credentials included in the request body. However, for this example, we will simply generate the tokens sequentially and verify them in the same manner.

Normally, the verifyJwt function is utilized in a middleware guard to verify the authenticity of the access token included in the request. This is done to determine whether the user is authorized to access a specific resource or perform certain actions.

Furthermore, you will have another route handler that returns a new access token. This route will only accept an HTTP-only cookie containing the refresh token, which was included in the response during the login process. If the refresh token is valid, the route function will sign a new access token and return it.

It’s important to note that there’s no need to sign a new refresh token in this scenario, as doing so would result in the user being logged in indefinitely. Therefore, this function should only sign a new access token. If the refresh token has expired, the user will need to re-authenticate by logging in again to obtain new access and refresh tokens.

src/token_rs256.ts


const user = {
  id: "3894stve8376gdhdj663h",
  name: "Admin",
  email: "admin@admin.com",
};

// Sign Access and Refresh tokens
const access_token = signJwt({ sub: user.id }, "ACCESS_TOKEN_PRIVATE_KEY", {
  expiresIn: `${getEnvVariable("ACCESS_TOKEN_EXPIRES_IN")}m`,
});

const refresh_token = signJwt({ sub: user.id }, "REFRESH_TOKEN_PRIVATE_KEY", {
  expiresIn: `${getEnvVariable("REFRESH_TOKEN_EXPIRES_IN")}m`,
});

console.log({ access_token, refresh_token });

// Verify Access and Refresh tokens
const access_token_payload = verifyJwt<{ sub: string }>(
  access_token,
  "ACCESS_TOKEN_PUBLIC_KEY"
);

if (access_token_payload) {
  console.log("✅Access token is valid");
} else {
  console.log("🔥Access token is invalid or has expired");
}

const refresh_token_payload = verifyJwt<{ sub: string }>(
  refresh_token,
  "REFRESH_TOKEN_PUBLIC_KEY"
);

if (refresh_token_payload) {
  console.log("✅Refresh token is valid");
} else {
  console.log("🔥Refresh token is invalid or has expired");
}

While reviewing the code above, you might have observed that we only included the user’s ID in the token payload. However, it’s important to note that you have the flexibility to store additional information in the payload as needed. Nonetheless, it is crucial to exercise caution when adding sensitive data, as the token can be decoded, potentially revealing its contents.

Generate the RSA Private and Public Keys Online

If you are unable to generate RSA key pairs using OpenSSL or if you are using a Windows machine without OpenSSL installed, you can utilize an Online RSA Key Generator as an alternative.

To generate the key pair, follow these steps:

  1. Visit the Online RSA Key Generator website.
  2. Set the key size to 4096 bits since the jsonwebtoken package requires a minimum of 2048 bits. Using 4096 bits ensures greater security and future compatibility.
  3. Enable the Async option as the key generation may take some time.
  4. Click on the “Generate New Keys” button to initiate the key generation process.

Once the keys have been generated, proceed with the following steps:

  1. Copy the private key from the generator.
  2. Use a base64 encoding tool, such as the one provided by this website: https://www.base64encode.org/.
  3. Encode the copied private key in base64 format.
  4. Open the .env file and set the value of the ACCESS_TOKEN_PRIVATE_KEY key to the encoded private key obtained in the previous step.
  5. Return to the token generation website and copy the corresponding public key.
  6. Encode the copied public key in base64 format using the same base64 encoding tool.
  7. In the .env file, assign the encoded public key to the ACCESS_TOKEN_PUBLIC_KEY key.
  8. Repeat the above steps to obtain and encode the private and public keys for the refresh token.
  9. Store the encoded private key in REFRESH_TOKEN_PRIVATE_KEY and the encoded public key in REFRESH_TOKEN_PUBLIC_KEY within the .env file.

# -----------------------------------------------------------------------------
# For RS256 Algorithm
# -----------------------------------------------------------------------------

ACCESS_TOKEN_EXPIRES_IN=15
REFRESH_TOKEN_EXPIRES_IN=60

ACCESS_TOKEN_PRIVATE_KEY=
ACCESS_TOKEN_PUBLIC_KEY=
REFRESH_TOKEN_PRIVATE_KEY=
REFRESH_TOKEN_PUBLIC_KEY=

Pros and Cons of HS256 (HMAC-SHA256)

Here’s an overview of the pros and cons of using the HS256 algorithm:

Pros:

  1. Simplicity: HS256 is relatively simple and easy to implement compared to asymmetric algorithms like RS256.
  2. Performance: HS256 is generally faster than RS256 because it involves symmetric cryptography, which is computationally less expensive.
  3. Key management: With HS256, you only need to manage a single secret key for both signing and verifying tokens, simplifying key management.

Cons:

  1. Key secrecy: The shared secret key used in HS256 must be kept confidential and known only to the parties involved. If the key is compromised, it can be used to forge or tamper with tokens.
  2. Key distribution: Distributing and securely sharing the secret key among multiple systems can be challenging, especially in distributed environments.
  3. Lack of key rotation: Since HS256 uses a single shared secret key, rotating the key without affecting existing tokens can be complex. It requires coordination and careful handling to ensure a smooth transition.

Pros and Cons of RS256 (RSA-SHA256)

Here’s a breakdown of the pros and cons of using the RS256 algorithm:

Pros:

  1. Asymmetric cryptography: RS256 utilizes public-key cryptography, providing stronger security guarantees compared to symmetric algorithms like HS256.
  2. Key pairs: RS256 uses a pair of public and private keys, allowing for secure communication without the need to share the private key.
  3. Key rotation: With RS256, key rotation is easier as the public key can be updated without affecting existing tokens.

Cons:

  1. Performance: RS256 is computationally more expensive than HS256 due to the asymmetric cryptographic operations involved.
  2. Complexity: Implementing RS256 requires managing public and private key pairs, which adds complexity compared to HS256.
  3. Key size: RSA keys used in RS256 are typically larger, resulting in larger token sizes and potentially increased network overhead.

Conclusion

And we are done! You can access the source code for this project on GitHub at https://github.com/wpcodevo/hs256-rs256-jwt-nodejs.

Throughout this article, we have explored the process of signing and verifying JWTs in Node.js using the HS256 and RS256 algorithms. I hope you found this article helpful and enjoyable. If you have any feedback or questions, please don’t hesitate to leave them in the comments section. I’ll be more than happy to respond promptly. Thank you for taking the time to read this article.