Secure APIs using Node.js, Azure AD, Cosmos DB and the Azure SDKs
Tags: azure, security
Published: 2021-04-23 00:28:15

With the [recent announcement](https://devblogs.microsoft.com/cosmosdb/role-based-access-control-preview/) that Cosmos DB now supports RBAC and Azure AD authentication, I was too excited to pass on the opportunity to build an API that takes advantage of these new capabilities. By adding support for Azure AD authentication, we can now use the Azure SDKs to securely access Cosmos DB data without having to provide any keys or secrets in our code. Instead, when instantiating a new `CosmosClient` object we can pass a `TokenProvider` curtesy of the [Azure.Identity](https://aka.ms/azsdk) library and let the token provider handle the authentication to our Cosmos DB resource. Secretless apps is the future and I love how all this is powered by Azure AD behind the scenes. 

Finally, to make our API more secure, we’ll add Azure AD authentication so that only authorized calls can call our API endpoints. Let’s build this

## Prerequisites
To follow along with this blog or run the [code sample]() you will need the following:
- An active Azure Subscription (get one for free [here](https://portal.azure.com/free))
- An Azure Active Directory (you can use the one in your Azure subscription or get a free one using the Microsoft 365 Developer program)
- [VS Code]( https://code.visualstudio.com/Download)
- Node.js and NPM (install from [here]( https://nodejs.org/en/download/)
- TypeScript (install globally from [here]( typescript - npm (npmjs.com)))

## Create the project
Open the command prompt of your choice and type the following

```
Mkir securenodeapiwithaad
Cd securenodeapiwithaad
Npm init -y
```
Next, type `code .` to open our project in VS Code and using the **Explorer **create a `tsconfig.json` file and add the following:

```
{
    "compilerOptions": {
        "target":"ES5",
        "sourceMap": true,
        "module": "commonjs",
        "outDir": "out"
    },
    "exclude": [
        "node_modules"
    ]
}
```

This tells VS Code how to deal with TypeScript and where to output our compiled code (in the `out` directory). Make sure to create a new `out` directory at the root of the project so that the output files can be transpiled into. 

We can now add our first (and only) file where our API code will reside. In the Solution view add a new file `index.ts` at the root of the project. 

Finally, we need to create a `launch.json` using the instructions found [here]( https://code.visualstudio.com/Docs/editor/debugging). Although this is not needed to run the Node.js code, I love the fact that VS can launch and debug our code. So if you, like me, want to be able to step through your code, add a `launch.json`. If all’s done correctly, your `launch.json` should look similar to this

```
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Launch API",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}\\index.ts",
            "preLaunchTask": "tsc: build - tsconfig.json",
            "outFiles": [
                "${workspaceFolder}/out/**/*.js"
            ]
        }
    ]
}
``` 

This is a Node.js project so we should add some NPM packages! Open the command prompt at the same directory as the project and type the following:

```
npm i @azure/identity
npm i @azure/cosmos
npm i express
npm i jsonwebtoken
npm i jwks-rsa,
npm i uuid
npm i @types/jsonwebtoken
```
Open `index.ts` and paste all the code as listed below:

```
import jwt = require('jsonwebtoken');
import jwksClient = require('jwks-rsa');
import {
    AzureCliCredential,
    ChainedTokenCredential,
    ManagedIdentityCredential,
    VisualStudioCodeCredential
} from "@azure/identity";
import express = require('express');
import { CosmosClient, Container, Item } from "@azure/cosmos";

const SERVER_PORT = process.env.PORT || 8000;
const jwtKeyDiscoveryEndpoint = "https://login.microsoftonline.com/common/discovery/keys";
const cosmosEndpoint = "https://msi-auth-test.documents.azure.com";
const credential = new ChainedTokenCredential(
    new AzureCliCredential(),
    new ManagedIdentityCredential()
);
let accessToken;
const cosmosClient = new CosmosClient({ 
    endpoint: cosmosEndpoint, 
    aadCredentials: credential 
});

const validateJwt = (req, res, next) => {
    const authHeader = req.headers.authorization;
    if (authHeader) {
        const token = authHeader.split(' ')[1];

        const validationOptions = {
            audience: config.auth.clientId,
            issuer: `${config.auth.authority}/v2.0`
        }

        jwt.verify(token, getSigningKeys, validationOptions, (err, payload) => {
            accessToken = payload;
            if (err) {
                console.log(err);
                return res.sendStatus(403);
            }
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

const getSigningKeys = (header, callback) => {
    var client = jwksClient({
        jwksUri: jwtKeyDiscoveryEndpoint
    });

    client.getSigningKey(header.kid, function (err, key) {
        var signingKey = key.getPublicKey();
        callback(null, signingKey);
    });
};

function confirmRequestHasTheRightScope(scopes:Array<string>): boolean{
    const tokenScopes:Array<string> = accessToken.scp.split(" ");
    scopes.forEach(scope => {
        if(!tokenScopes.includes(scope)){
            return false;
        }
    });
    return true;
}

const config = {
    auth: {
        clientId: "<your Azure AD App Registration Client ID",
        tenantId: "<your  Azure AD Tenant ID>",
        authority: "https://login.microsoftonline.com/<your Azure AD Tenant ID>",
    }
};

// Create Express App and Routes
const app = express();

app.get('/', (req, res)=>{
    var data = {
        "endpoint1": "/getvolcanodata?volcanoname=<name>",
        "endpoint2": "/getCosmosData"
    };
    res.send(data); 
})

app.get('/getCosmosData', validateJwt, async (req, res) => {
    const data = await getCosmosData();
    res.status(200).send(data);
});

app.get('/getVolcanoData', validateJwt, async(req, res)=> {
    const data = await getVolcanoDataByName(req.query.volcanoname.toString());
    res.status(200).send(data);
});

app.listen(SERVER_PORT, () => console.log(`Secure Node Web API listening on port ${SERVER_PORT}!`))

async function getVolcanoDataByName(volcanoName: string): Promise<Array<string>> {
    const container = cosmosClient.database('VolcanoList').container('Volcano');
    const results = await container.items
        .query({
            query: "SELECT * FROM Volcano f WHERE  f.VolcanoName = @volcanoName",
            parameters: [{ name: "@volcanoName", value: volcanoName }]
        })
        .fetchAll();
    return results.resources;
}

async function getCosmosData(): Promise<Array<any>> {
    try {
        let data: any[] = [];
        const container = cosmosClient.database('VolcanoList').container('Volcano');
        const results = await container.items.readAll().fetchAll();
        //get the first 10 items
        let index = 0;
        while (index < 10) {
            data.push(results.resources[index]);
            index++;
        };
        return data;
    }
    catch (error) {
        console.error(error);
    }
    return [];
};
```

Let’s kick off the debugger and run the API in VS Code to ensure that there are no errors in our code.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s3sqskcsu5qevjvo5top.gif)

At the moment, we have 3 endpoints:
1. Home: “/”
2. Get data for a specific volcano: “/getvolcanodata?volcanoname=<name>”
3. Get data for all the volcanos: “/getCosmosData”

Endpoint 2 and 3 expect a valid Access Token so calling them without one or without a valid one will result in a 401 or 403 error message respectively.

## Configure the Azure Service Principal

Since we’re still running on our local environment (outside of Azure) where Managed Identities are not supported, we need to provide the Azure.Identity with some credential so that we can authenticate and access our Cosmos DB data securely. The current code expects an `AzureCLICredential`. We can use a service principal account (an Azure AD service account) to sign in to our local Azure CLI. But first we need to create one.
In the Azure CLI type the following:
```
az login
az ad sp create-for-rbac
```
This should result to the following: 
```
{
  "appId": "1b47786f-2a57-0000-0000-d224665dc135",
  "displayName": "azure-cli-2021-04-21-23-00-17",
  "name": "http://azure-cli-2021-04-21-23-00-17",
  "password": "very secret password",
  "tenant": "e801a3ad-3690-0000-0000-1d77cb360b07"
}
```
Make a note of these values, especially the **appId** and **password** values as we’ll use these credentials later to sign in to our local Azure CLI. Before we move on to the next task, let’s grab the **Object ID **of our SP while we’re at it. In the CLI, type 

```
az ad sp show --id <appId>
```

Next, we need to configure this SP with the right RBAC permissions in Cosmos DB. First open a text editor and create a new json file. Name it `role-definition.json`. Paste the following text

```
{
    "RoleName": "CMReadOnlyRole",
    "Type": "CustomRole",
    "AssignableScopes": ["/"],
    "Permissions": [{
        "DataActions": [
            "Microsoft.DocumentDB/databaseAccounts/readMetadata",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed"
        ]
    }]
}
```

This file contains a Read Only role definition that we will create and assign to our service principal. Save the file. Navigate to this directory in your shell and type the following:

```
resourceGroupName='<myResourceGroup>'
accountName='<myCosmosAccount>'
az cosmosdb sql role definition create --account-name $accountName --resource-group $resourceGroupName --body @role-definition.json
```

Once that’s completed, verify that the role was created successfully:

```
az cosmosdb sql role definition list --account-name $accountName --resource-group $resourceGroupName
```

The result should look like this. Make a not of the role ID as we’ll need it for the assignment.

```
[
  {
    "assignableScopes": [
      "/subscriptions/d8011108-23b2-40d8-8bc4-1f3f77abe795/resourceGroups/identity/providers/Microsoft.DocumentDB/databaseAccounts/az-fun-demo-cm"
    ],
    "id": "/subscriptions/d8011108-23b2-40d8-8bc4-1f3f77abe795/resourceGroups/identity/providers/Microsoft.DocumentDB/databaseAccounts/az-fun-demo-cm/sqlRoleDefinitions/664d69f9-a68f-4dde-8c59-8609fdb65ae4",
    "name": "664d69f9-a68f-4dde-8c59-8609fdb65ae4",
    "permissions": [
      {
        "dataActions": [
          "Microsoft.DocumentDB/databaseAccounts/readMetadata",
          "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
          "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery",
          "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed"
        ],
        "notDataActions": []
      }
    ],
    "resourceGroup": "identity",
    "roleName": "CMReadOnlyRole",
    "sqlRoleDefinitionGetResultsType": "CustomRole",
    "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions"
  }
]
```

To make it all work need to assign this role to our service principal. In the CLI, type the following:

```
readOnlyRoleDefinitionId = '<roleDefinitionId>'  // the is the guid of the role definition
principalId = '<aadPrincipalId>' // this is the object ID of our Service Principal
az cosmosdb sql role assignment create --account-name $accountName --resource-group $resourceGroupName --scope "/" --principal-id $principalId --role-definition-id $readOnlyRoleDefinitionId
```

The result of this command should look like this:

```
{\ Finished …
  "id": "/subscriptions/d8011108-23b2-40d8-8bc4-1f3f77abe795/resourceGroups/identity/providers/Microsoft.DocumentDB/databaseAccounts/az-fun-demo-cm/sqlRoleAssignments/24a3c9c0-9193-46fa-a861-04ac918f4214",
  "name": "24a3c9c0-9193-46fa-a861-04ac918f4214",
  "principalId": "ebb15145-0390-4c65-b4d8-74ef8891bda9",
  "resourceGroup": "identity",
  "roleDefinitionId": "/subscriptions/d8011108-23b2-40d8-8bc4-1f3f77abe795/resourceGroups/identity/providers/Microsoft.DocumentDB/databaseAccounts/az-fun-demo-cm/sqlRoleDefinitions/664d69f9-a68f-4dde-8c59-8609fdb65ae4",
  "scope": "/subscriptions/d8011108-23b2-40d8-8bc4-1f3f77abe795/resourceGroups/identity/providers/Microsoft.DocumentDB/databaseAccounts/az-fun-demo-cm",
  "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments"
}
```

Finally, type the following to login to our local Azure CLI with the Service Principal account we created above:

```
az login --service-principal -u <appId> -p <password> --tenant <tenantId>
```

This should result to the following json output

```
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "e801a3ad-0000-0000-0000-1d77cb360b07",
    "id": "d8011108-0000-0000-0000-1f3f77abe795",
    "isDefault": true,
    "managedByTenants": [],
    "name": "Visual Studio Enterprise",
    "state": "Enabled",
    "tenantId": "e801a3ad-0000-0000-0000-1d77cb360b07",
    "user": {
      "name": "1b47786f-0000-0000-0000-d224665dc135",
      "type": "servicePrincipal"
    }
  }
]
```

At this point we have everything we need to authenticate with the service principal and for our code to use these credentials in order to call into Cosmos DB – without any secrets!!!

## Create the Azure AD App Registrations
For our API to be secure, we need an App Registration in Azure AD. This will be used by our API to validate incoming tokens from client/front-end apps that call into the API. By configuring this we will have a secure end-to-end channel for retrieving data from the API since we don’t want anyone to come in and call our API endpoints. 

In Azure AD, navigate to the App Registration tab and create a new one. Give it a name and press **Register**. Then go to the **Expose an API** and add a new claim: `access_cosmos_data`. Copy the **Client ID** and **Tenant ID** and update your code accordingly.

We also need to create a client app registration so that we can acquire an access token and call our API securely. In the App Registrations, create a new App Registration. Give it a name and press **Register**.

In the **Authentication** tab, press **Add a platform**, select **Web** and type `http://localhost’. In the **Secrets and Certificates** tab, create a new Secret and make sure you copy the value as you won’t be able to get the secret once you navigate away. In the **API Permissions** tab, add a new permission, select **My APIs** and select the App Registration you created for your API. Check the permission checkbox and press **Add permission**. 

That’s all we need from Azure AD!

## Let’s test it
To test that the API works as expected, we can use Postman. We need to use the values from the second AAD App Registration to populate the Authorization section in Postman to get an access token for the API. Make sure to start the API in VS Code and test that everything’s working as expected. If all the steps were followed successfully, we should be treated with a bunch of CosmosDB data as per the example below:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uucul8jv0fwd4cyqry47.gif)

## Source Code
You can get find a fully working sample in this [GitHub repo](https://github.com/425show/secureNodeAPIwithAAD)