Empowering Your Azure Projects with a Secure CLI: A Step-by-Step Guide
Tags: cli, azure, typescript, node
Published: 2023-09-11 06:30:13

In this easy-to-follow blog post, we're going to guide you step by step on how to build a command-line interface (CLI) application using [oclif](https://www.npmjs.com/package/oclif), the NodeJs Open CLI Framework. This application will be your trusted tool for securely logging into your Azure account. 

What makes it even cooler? Just like other popular CLI tools such as `az`, our application securely stores your authenticated token in local storage. This means you can effortlessly use it to execute other CLI commands without having to log in every time. 

Let's dive in and get started!

## Prerequisites

Before we begin, make sure to install the oclif globally by using the following command:

```sh
npm install -g oclif
```

## Step 1: Setting Up Your Project

Initiate your CLI project with the following command:

```sh
oclif generate your-cli-app-name
```

Navigate to your project folder and install necessary packages for Azure identity and HTTP client axios:

```sh
npm install @azure/identity axios
```

## Step 2: Creating the Login Command

Generate your `login` command using the following:

```sh
oclif generate command login
```

This will create a template file for your login command. Open `src/commands/login.ts` and implement the login functionality using the `DefaultAzureCredential` which attempts to use multiple authentication methods based upon the available environment variables:

```typescript
import { Command } from '@oclif/core';
import { DefaultAzureCredential } from '@azure/identity';

export default class Login extends Command {
  static description = 'Login to Azure account';

  async run() {
    const credential = new DefaultAzureCredential();
    
    try {
      const token = await credential.getToken('https://management.azure.com/.default');
      if (token) {
        // Storing the token securely will be implemented in the next steps
        this.log('Login successful.');
      }
    } catch (error: any) {
      this.error(`Login failed: ${error.message}`, { exit: 1 });
    }
  }
}
```

## Step 3: Integrating Keytar for Robust and Secure Token Storage

In this step, we're going to enhance our application's security by integrating [Keytar](https://www.npmjs.com/package/keytar), a node module that leverages the operating system's secure services for storing sensitive information. This prevents any unauthorized access to the tokens, safeguarding your user credentials effectively.

First off, let's install Keytar to manage the secure storage of the token acquired during the login process:

```sh
npm install keytar
```

Next, we modify the login command to utilize Keytar. This will allow us to store the authentication token in a safe and encrypted manner, a strategy commonly adopted to prevent token theft and ensure overall application security. Here is how you can do it:

```typescript
import * as keytar from 'keytar';
// ... (rest of the imports)

// ... (within the try block in the run method)
if (token) {
  await keytar.setPassword('your-cli-app-name', 'azureToken', token.token);
  this.log('Login successful.');
}
// ...
```

By implementing Keytar, not only are we fortifying our application's security, but we are also creating a seamless user experience. Keytar facilitates automatic and secure retrievals of tokens, which means users won't have to re-enter their credentials every time they run a command, thus aligning with best practices observed in well-established CLI tools such as `az`.

Remember, a secure application is a trusted application. Let's move forward, ensuring the safety of our user's data with the reliable functionalities provided by Keytar!

## Step 4: Creating the Logout Command

Generate your `logout` command:

```sh
oclif generate command logout
```

Now, populate the `src/commands/logout.ts` with the following content to enable logging out functionality:

```typescript
import { Command } from '@oclif/core';
import * as keytar from 'keytar';

export default class Logout extends Command {
  static description = 'Logout from Azure account';

  async run() {
    await keytar.deletePassword('your-cli-app-name', 'azureToken');
    this.log('Logout successful.');
  }
}
```

## Step 5: Creating the Account Show Command with Stored Access Token

As we venture further, we'll be exploring how to utilize the stored access token in crafting our custom CLI commands. Let's start with something simple yet functional, like an 'account show' command that fetches and displays the username of the connected Azure account.

First, initiate the process by generating the `account show` command. Use the following command to do this:

```sh
oclif generate command account:show
```

Before we populate the newly created command file, we need to craft a utility function that will fetch the username of the logged-in Azure account. This utility function will leverage the stored access token we saved during the login process.

But before we proceed, ensure to install axios - a promise-based HTTP client for the browser and Node.js:

```sh
npm install axios
```

Next, we'll create a utility module to encapsulate the logic to fetch the logged-in username. Create a new file at `src/utils/azureUtils.ts` and insert the following content:

```typescript
import axios from 'axios';
import * as keytar from 'keytar';

export async function getLoggedInUserName(): Promise<string> {
  try {
    // Retrieve the stored token
    const token = await keytar.getPassword('your-cli-app-name', 'azureToken');
    if (!token) throw new Error('No token found');

    // Use the token to fetch the user information from Azure
    const response = await axios.get('https://management.azure.com/me?api-version=2020-10-01', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    // Return the user principal name or a default string if it's not available
    return response.data.userPrincipalName || 'Unknown User';
  } catch (error: any) {
    throw new Error(`Could not fetch user info: ${error.message}`);
  }
}
```

With our utility function ready, let’s now create a command that makes use of this function to display the Azure account details on the terminal. Open `src/commands/account/show.ts` and implement the command as follows:

```typescript
import { Command } from '@oclif/core';
import { getLoggedInUserName } from '../../utils/azureUtils';

export default class AccountShow extends Command {
  static description = 'Show the connected Azure account username';

  async run() {
    try {
      // Fetch the username using the utility function
      const username = await getLoggedInUserName();
      // Log the username to the terminal
      this.log('Connected account username:', username);
    } catch (error: any) {
      // Handle any errors that occur during the process
      this.error(`Error fetching username: ${error.message}`, { exit: 1 });
    }
  }
}
```


---

This section now elucidates the usage of the stored access token and how it integrates seamlessly in creating a new CLI command. Let me know if there is anything else you'd like to add or modify!

## Step 6: Understanding Scopes in Azure

When using the `DefaultAzureCredential` class to authenticate, it is essential to understand scopes. Scopes define the resources and permissions your token will have access to in the Azure ecosystem. Different Azure resources will require different scopes. Adjust the scopes as per your needs. For example:

```typescript
const token = await credential.getToken('https://management.azure.com/.default');
```

Here, we are requesting a token with the scope of managing resources on Azure. Make sure to select the appropriate scope for your application needs.

## Step 7: Testing Your Commands

Test your CLI application using the following commands:

```sh
./bin/dev login
./bin/dev logout
./bin/dev account show
```

You should see the expected outputs for each command if everything is set up correctly.

Absolutely! Here's how you can extend the guide to include building, publishing, and using the CLI application:

---

## Step 8: Building the Final CLI Application

After thoroughly testing your commands, it's time to compile your CLI application into a version that's ready for production. Here's how you can do this:

```sh
npm run build
```

This command should bundle your application and prepare it for distribution. Make sure to check for any errors and fix them before proceeding to the next step.

## Step 9: Publishing Your CLI Application

Now that you have built your CLI application, the next step is to publish it so that it can be easily installed and used by others. Here’s how you can publish your application to the npm registry:

1. Ensure that you are logged into your npm account by running:
   
   ```sh
   npm login
   ```

2. Publish your package using:
   
   ```sh
   npm publish
   ```

This command will upload your package to the npm registry, making it available for others to install and use.

## Step 10: Sample Usage

Your CLI application is now live! Here's how users can install it and a few sample commands they can use to get started:

1. To install the application, users should run:

   ```sh
   npm install -g your-cli-app-name
   ```

2. Once installed, they can start using the application with commands such as:

   ```sh
   your-cli-app-name login
   your-cli-app-name logout
   your-cli-app-name account show
   ```

Remember to provide a comprehensive README with your package, detailing all available commands and usage examples to assist users in navigating through your application efficiently.

## Conclusion

Congratulations on building a secure CLI application with oclif, integrated with Azure authentication, featuring secure login, logout, and account detail retrieval functionalities. Keep exploring the power of CLI tools with oclif!