Tutorial: Call a web API from your Node.js daemon application

This tutorial is the final part of a series that demonstrates how to prepare your Node.js daemon client app using the Open Authorization (OAuth) 2.0 client credentials grant flow, then configure it to acquire an access token for calling a web API. in Part 1 of this series, you registered a web API and daemon app in the Microsoft Entra admin center and granted permissions. This final step demonstrates how to build a Node.js application using the Microsoft Authentication Library (MSAL) for Node to simplify adding authorization to your app.

In this tutorial;

  • Create a Node.js app in Visual Studio Code, then install dependencies.
  • Enable the Node.js app to acquire an access token for calling a web API.

Prerequisites

Create the Node.js daemon project

Create a folder to host your Node.js daemon application, such as ciam-call-api-node-daemon:

  1. In your terminal, change directory into your Node daemon app folder, such as cd ciam-call-api-node-daemon, then run npm init -y. This command creates a default package.json file for your Node.js project. This command creates a default package.json file for your Node.js project.

  2. Create additional folders and files to achieve the following project structure:

        ciam-call-api-node-daemon/
        ├── auth.js
        └── authConfig.js
        └── fetch.js
        └── index.js 
        └── package.json
    

Install app dependencies

In your terminal, install axios, yargs and @azure/msal-node packages by running the following command:

npm install axios yargs @azure/msal-node   

Create MSAL configuration object

In your code editor, open authConfig.js file, then add the following code:

require('dotenv').config();

/**
 * Configuration object to be passed to MSAL instance on creation.
 * For a full list of MSAL Node configuration parameters, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
 */    
const msalConfig = {
    auth: {
        clientId: process.env.CLIENT_ID || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
        authority: process.env.AUTHORITY || 'https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/', // Replace "Enter_the_Tenant_Subdomain_Here" with your tenant subdomain
        clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app 
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: 'Info',
        },
    },
};    
const protectedResources = {
    apiToDoList: {
        endpoint: process.env.API_ENDPOINT || 'https://localhost:44351/api/todolist',
        scopes: [process.env.SCOPES || 'api://Enter_the_Web_Api_Application_Id_Here'],
    },
};

module.exports = {
    msalConfig,
    protectedResources,
};

The msalConfig object contains a set of configuration options that you use to customize the behavior of your authorization flow.

In your authConfig.js file, replace:

  • Enter_the_Application_Id_Here with the Application (client) ID of the client daemon app that you registered earlier.

  • Enter_the_Tenant_Subdomain_Here and replace it with the Directory (tenant) subdomain. For example, if your tenant primary domain is contoso.onmicrosoft.com, use contoso. If you don't have your tenant name, learn how to read your tenant details.

  • Enter_the_Client_Secret_Here with the client daemon app secret value that you copied earlier.

  • Enter_the_Web_Api_Application_Id_Here with the Application (client) ID of the web API app that you copied earlier.

Notice that the scopes property in the protectedResources variable is the resource identifier (application ID URI) of the web API that you registered earlier. The complete scope URI looks similar to api://Enter_the_Web_Api_Application_Id_Here/.default.

Acquire an access token

In your code editor, open auth.js file, then add the following code:

const msal = require('@azure/msal-node');
const { msalConfig, protectedResources } = require('./authConfig');
/**
 * With client credentials flows permissions need to be granted in the portal by a tenant administrator.
 * The scope is always in the format '<resource-appId-uri>/.default'. For more, visit:
 * https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
 */
const tokenRequest = {
    scopes: [`${protectedResources.apiToDoList.scopes}/.default`],
};

const apiConfig = {
    uri: protectedResources.apiToDoList.endpoint,
};

/**
 * Initialize a confidential client application. For more info, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/initialize-confidential-client-application.md
 */
const cca = new msal.ConfidentialClientApplication(msalConfig);
/**
 * Acquires token with client credentials.
 * @param {object} tokenRequest
 */
async function getToken(tokenRequest) {
    return await cca.acquireTokenByClientCredential(tokenRequest);
}

module.exports = {
    apiConfig: apiConfig,
    tokenRequest: tokenRequest,
    getToken: getToken,
};

In the code:

  • Prepare the tokenRequest and apiConfig object. The tokenRequest contains the scope for which you request an access token. The scope looks something like api://Enter_the_Web_Api_Application_Id_Here/.default. The apiConfig object contains the endpoint to your web API. Learn more about OAuth 2.0 client credentials flow.

  • You create a confidential client instance by passing the msalConfig object to the ConfidentialClientApplication class' constructor.

    const cca = new msal.ConfidentialClientApplication(msalConfig);
    
  • You then use the acquireTokenByClientCredential function to acquire an access token. You implement this logic in the getToken function:

    cca.acquireTokenByClientCredential(tokenRequest);
    

Once you acquire an access token, you can proceed to call an API.

Call an API

In your code editor, open fetch.js file, then add the following code:

const axios = require('axios');

/**
 * Calls the endpoint with authorization bearer token.
 * @param {string} endpoint
 * @param {string} accessToken 
 */
async function callApi(endpoint, accessToken) {

    const options = {
        headers: {
            Authorization: `Bearer ${accessToken}`
        }
    };

    console.log('request made to web API at: ' + new Date().toString());

    try {
        const response = await axios.get(endpoint, options);
        return response.data;
    } catch (error) {
        console.log(error)
        return error;
    }
};

module.exports = {
    callApi: callApi
};

In this code, you make a call to the web API, by passing the access token as a bearer token in the request Authorization header:

 Authorization: `Bearer ${accessToken}`

You use the access token that you acquired earlier in Acquire an access token.

Once the web API receives the request, it evaluates it then determines that it's an application request. If the access token is valid, the web API returns requested data. Otherwise, the API returns a 401 Unauthorized HTTP error.

Finalize your daemon app

In your code editor, open index.js file, then add the following code:

#!/usr/bin/env node

// read in env settings

require('dotenv').config();

const yargs = require('yargs');
const fetch = require('./fetch');
const auth = require('./auth');

const options = yargs
    .usage('Usage: --op <operation_name>')
    .option('op', { alias: 'operation', describe: 'operation name', type: 'string', demandOption: true })
    .argv;

async function main() {
    console.log(`You have selected: ${options.op}`);

    switch (yargs.argv['op']) {
        case 'getToDos':
            try {
                const authResponse = await auth.getToken(auth.tokenRequest);
                const todos = await fetch.callApi(auth.apiConfig.uri, authResponse.accessToken);                
            } catch (error) {
                console.log(error);
            }

            break;
        default:
            console.log('Select an operation first');
            break;
    }
};

main();

This code is the entry point to your app. You use the yargs JavaScript command-line argument parsing library for Node.js apps to interactively fetch an access token, then call API. You use the getToken and callApi functions you defined earlier:

const authResponse = await auth.getToken(auth.tokenRequest);
const todos = await fetch.callApi(auth.apiConfig.uri, authResponse.accessToken);                

Run and test daemon app and API

At this point, you're ready to test your client daemon app and web API:

  1. Use the steps you learned in Secure an ASP.NET web API tutorial to start your web API. Your web API is now ready to serve client requests. If you don't run your web API on port 44351 as specified in the authConfig.js file, make sure you update the authConfig.js file to use the correct web API's port number.

  2. In your terminal, make sure you're in the project folder that contains your daemon Node.js app such as ciam-call-api-node-daemon, then run the following command:

    node . --op getToDos
    

If your daemon app and web API run successfully, you should find the data returned by the web API endpoint todos variable, similar to the following JSON array, in your console window:

{
    id: 1,
    owner: '3e8....-db63-43a2-a767-5d7db...',
    description: 'Pick up grocery'
},
{
    id: 2,
    owner: 'c3cc....-c4ec-4531-a197-cb919ed.....',
    description: 'Finish invoice report'
},
{
    id: 3,
    owner: 'a35e....-3b8a-4632-8c4f-ffb840d.....',
    description: 'Water plants'
}

Next step