Create a CLI with Node.js

Overview

Some of the most useful platforms and developer tools out there have a CLI (command line interface) for interacting with and getting stuff done. Much of what we do as developers or IT professionals is in the command line or terminal. So having a one-liner that will get a task done is extremely helpful and improves productivity.

In my day job, I recently put together a CLI for performing common support requests that my team would regularly get. This allowed us to script them out, package it as an npm package and everyone on the team could pull it down and run it. Doing this in Node.js was ridiculously simple! I'll show you how to do this with a simple task that I find myself needing to do when debugging auth issues with JWT tokens.

1. Setting Up the Project

To start, lets spin up an empty project. You can go with the default values or enter what you'd like. However, be sure to enter ./modules/index.js for the entry point: (index.js).

 1$ npm init 
 2...
 3package name: (jwt-cli)
 4version: (1.0.0)
 5description: CLI for working with JSON Web Tokens that leverages the jsonwebtoken npm package
 6entry point: (index.js) ./modules/index.js
 7test command:
 8git repository:
 9keywords:
10author:
11license: (ISC) MIT
12...
13Is this OK? (yes)

What we end up with is a single package.js file in our project directory. Let's make a few changes to it...

2. Add Dependencies

To support the interface of our CLI and the jsonwebtoken library we are going to leverage for this example we need to add the following dependencies to our package.json. We will discuss these more below:

1  "dependencies": {
2    "chalk": "2.4",
3    "jsonwebtoken": "^8.5.1",
4    "yargs": "13.2"
5  }

Next, lets add the path to our modules. Our modules will be the individual commands supported by our CLI (we will code these in the next step). For this basic example we will be providing support for three individual commands: decoding, signing and verifying a JWT token. Add the following section to the package.json:

1"bin": {
2    "jwt-decode": "./modules/jwt-decode.js",
3    "jwt-sign": "./modules/jwt-sign.js",
4    "jwt-verify": "./modules/jwt-verify.js"
5  },

With our package.json file complete we can run $ npm install to pull down all of our dependencies.

3. Write the Modules

Now we are ready to code the modules themselves that will provide the functionality for our CLI commands.

First, create the modules directory within the root of our project: ./modules.

Next, we are going to add four js files to our modules directory. One will be the index.js and the other three will be the modules themselves as we added to our package.json above.

1./modules/index.js
2./modules/jwt-sign.js
3./modules/jwt-verify.js
4./modules/jwt-decode.js

Let's start with ./modules/index.js. If you recall, this was the file we referenced in the "entry point" option when init'ing our project. It provides the module exports for our CLI commands. Add the following to index.js:

1const jwtDecode = require('./jwt-decode');
2const jwtSign = require('./jwt-sign');
3const jwtVerify = require('./jwt-verify');  
4
5module.exports = {
6    jwtDecode: jwtDecode,
7    jwtSign: jwtSign,
8    jwtVerify: jwtVerify
9};

Next, we will complete the code for the three commands that make up our CLI.

As I mentioned before, we are essentially just building a CLI to wrap the functionality provided by the jsonwebtoken library. For this example, we will be only leveraging the basic usage scenarios but we can obviously expand our code to do much more.

jwt-sign

This command will allow us to sign a payload using a specified secret. All this is doing is a passthrough to the sign() function of the jsonwebtoken library. While there are several other parameters that you can pass in, we will just be providing a payload and secret.

Add the following to ./modules/jwt-sign.js:

 1#!/usr/bin/env node  
 2
 3const chalk = require("chalk");
 4const yargs = require("yargs");
 5var jwt = require('jsonwebtoken');  
 6
 7const main = async () => {
 8    try {
 9      const options = yargs
10      .usage('Usage: jwt-sign -p <Payload> -s <Secret>')
11      .option("p", { alias: "payload", describe: "Payload", type: "string", demandOption: true })
12      .option("s", { alias: "secret", describe: "Secret", type: "string", demandOption: true })
13      .argv;    
14
15      var token = jwt.sign(options.payload, options.secret);
16      console.log(chalk.green.bold(`Token: ${token}`));
17    } catch ( err ) {
18      console.log( chalk.red( err ) );
19    }
20
21   };
22
23   main();

As you can see there really isn't a lot to it but let's discuss the key pieces.

  • At the beginning of the file we have #!/usr/bin/env node referred to as the shebang line. Basically it's purpose is just to allow you to execute a node.js file directly.
  • Next you will notice our usage of yargs. Yargs gives us the ability to easily parse passed in arguments, provides help text/menu support and a bunch of other cool features.
  • The second library we use is Chalk. Chalk gives us all sorts of fancy styling within the terminal. You just pass it in to your calls to console.log as seen above. Check out the various options on their website.
  • Finally we pass our options from yargs into jwt.sign to get the token back.
1var token = jwt.sign(options.payload, options.secret);

That's it for our first module. Let's see the code for our last two modules...

jwt-verify

Add the following to ./modules/jwt-verify.js:

 1#!/usr/bin/env node 
 2
 3const chalk = require("chalk");
 4const yargs = require("yargs");
 5var jwt = require('jsonwebtoken'); 
 6
 7const main = async () => {
 8    try {
 9      const options = yargs
10      .usage("Usage: jwt-verify -t <Token> -s <Secret>")
11      .option("t", { alias: "token", describe: "Token", type: "string", demandOption: true })  
12      .option("s", { alias: "secret", describe: "Secret", type: "string", demandOption: true })  
13      .argv;           
14
15      var decoded = jwt.verify(options.token, options.secret);
16      console.log(chalk.green.bold(`Decoded: ${JSON.stringify(decoded, null, 4)}`));                  
17
18    } catch ( err ) {
19      console.log( chalk.red( err ) );
20    }
21    
22   };
23
24   main();

jwt-decode

Add the following to ./modules/jwt-decode.js:

 1#!/usr/bin/env node  
 2
 3const chalk = require("chalk");
 4const yargs = require("yargs");
 5var jwt = require('jsonwebtoken');  
 6
 7const main = async () => {
 8    try {
 9      const options = yargs
10      .usage(`Usage: jwt-decode -t <Token>\n
11      Warning: This will not verify whether the signature is valid. You should not use this for untrusted messages. You most likely want to use jwt.verify instead.
12      `)
13      .option("t", { alias: "token", describe: "Token", type: "string", demandOption: true })    
14      .argv;     
15
16      var decoded = jwt.decode(options.token, {complete: true});        
17      console.log(chalk.green.bold(`Decoded: ${JSON.stringify(decoded, null, 4)}`));      
18
19    } catch ( err ) {
20      console.log( chalk.red( err ) );
21    }
22
23   };
24
25   main();

4. Run It

The final step is to allow us to run our commands from the command line. To do that simply run: $ npm install -g . From the root of the project. This will install our modules globally so that we can run it within any terminal session.

Now we can run it! Here are some examples of our CLI in action. As you can see, when not passing in any arguments, yargs will output the help text as shown below.

jwt-sign

1$ jwt-sign
2Usage: jwt-sign -p <Payload> -s <Secret>
3
4Options:
5  --help         Show help                                             [boolean]
6  --version      Show version number                                   [boolean]
7  -p, --payload  Payload                                     [string] [required]
8  -s, --secret   Secret                                      [string] [required]

Example Usage:

1$ jwt-sign -p "{ foo: bar }" -s "super_duper_secret"
2Token: eyJhbGciOiJIUzI1NiJ9.eyBmb286IGJhciB9.955OkamnT6QlT0VSZonoivBjZm_hSOZ23SR55G8Zr3Q

jwt-verify

1$ jwt-verify
2Usage: jwt-verify -t <Token> -s <Secret>
3
4Options:
5  --help        Show help                                              [boolean]
6  --version     Show version number                                    [boolean]
7  -t, --token   Token                                        [string] [required]
8  -s, --secret  Secret                                       [string] [required]

Example Usage:

1$ jwt-verify -t eyJhbGciOiJIUzI1NiJ9.eyBmb286IGJhciB9.955OkamnT6QlT0VSZonoivBjZm_hSOZ23SR55G8Zr3Q -s "super_duper_secret"
2Decoded: "{ foo: bar }"

jwt-decode

 1$ jwt-decode
 2Usage: jwt-decode -t <Token>
 3
 4      Warning: This will not verify whether the signature is valid. You should
 5      not use this for untrusted messages. You most likely want to use
 6      jwt.verify instead.
 7
 8
 9Options:
10  --help       Show help                                               [boolean]
11  --version    Show version number                                     [boolean]
12  -t, --token  Token                                         [string] [required]

Example Usage:

1$ jwt-decode -t eyJhbGciOiJIUzI1NiJ9.eyBmb286IGJhciB9.955OkamnT6QlT0VSZonoivBjZm_hSOZ23SR55G8Zr3Q
2Token: eyJhbGciOiJIUzI1NiJ9.eyBmb286IGJhciB9.955OkamnT6QlT0VSZonoivBjZm_hSOZ23SR55G8Zr3Q...
3Decoded: {
4    "header": {
5        "alg": "HS256"
6    },
7    "payload": "{ foo: bar }",
8    "signature": "955OkamnT6QlT0VSZonoivBjZm_hSOZ23SR55G8Zr3Q"
9}

That's it!

You can find the full code for this example on my GitHub https://github.com/daveschmalz/jwt-cli. Feel free to submit a PR if you find it useful and want to enhance it.

comments powered by Disqus