Secure Node.js Web API with Azure Active Directory

This article demonstrates how to secure a Restify API endpoint with Passport using the passport-azure-ad module to handle communication with Azure Active Directory (AAD).

The scope of this tutorial covers the concerns regarding securing API endpoints. The concerns of signing in and retaining authentication tokens are not implemented here and are the responsibility of a client application. For details surrounding a client implementation, review Node.js web app sign-in and sign-out with Azure AD.

The full code sample associated with this article is available on GitHub.

Create the sample project

This server application requires a few package dependencies to support Restify and Passport as well as account information that is passed to AAD.

To begin, add the following code into a file named package.json:

{
  "name": "node-aad-demo",
  "version": "0.0.1",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "passport": "0.4.0",
    "passport-azure-ad": "3.0.8",
    "restify": "6.0.1",
    "restify-plugins": "1.6.0"
  }
}

Once package.json is created, run npm install in your command prompt to install the package dependencies.

Configure the project to use Active Directory

To get started configuring the application, there are a few account-specific values you can obtain from the Azure CLI. The easiest way to get started with the CLI is to use the Azure Cloud Shell.

Launch Azure Cloud Shell

The Azure Cloud Shell is a free Bash shell that you can run directly within the Azure portal. It has the Azure CLI preinstalled and configured to use with your account. Click the Cloud Shell button on the menu in the upper-right of the Azure portal.

Cloud Shell

The button launches an interactive shell that you can use to run the steps in this topic:

Screenshot showing the Cloud Shell window in the portal

Enter the following command in the cloud shell:

az ad app create --display-name node-aad-demo --homepage http://localhost --identifier-uris http://node-aad-demo

The arguments for the create command include:

Argument Description
display-name Friendly name of the registration
homepage Url where users can sign in and use your application
identifier-uris Space separated unique URIs that Azure AD can use for this application

Before you can connect to Azure Active Directory, you need the following information:

Name Description Variable Name in Config File
Tenant Name Tenant name you want to use for authentication tenantName
Client ID Client ID is the OAuth term used for the AAD Application ID. clientID

From the registration response in the Azure Cloud Shell, copy the appId value and create a new file named config.js. Next, add in the following code and replace your values with the bracketed tokens:

const tenantName    = //<YOUR_TENANT_NAME>;
const clientID      = //<YOUR_APP_ID_FROM_CLOUD_SHELL>;
const serverPort    = 3000;

module.exports.serverPort = serverPort;

module.exports.credentials = {
  identityMetadata: `https://login.microsoftonline.com/${tenantName}.onmicrosoft.com/.well-known/openid-configuration`, 
  clientID: clientID
};

For more information regarding the individual configuration settings, review the passport-azure-ad module documentation.

Implement the server

The passport-azure-ad module features two authentication strategies: OIDC and Bearer strategies. The server implemented in this article uses the Bearer strategy to secure the API endpoint.

Step 1: Import dependencies

Create a new file named app.js and paste in the following text:

const
      restify = require('restify')
    , restifyPlugins = require('restify-plugins')
    , passport = require('passport')
    , BearerStrategy = require('passport-azure-ad').BearerStrategy
    , config = require('./config')
    , authenticatedUserTokens = []
    , serverPort = process.env.PORT || config.serverPort
;

In this section of code:

  • The restify and restify-plugins modules are referenced in order to set up a Restify server.

  • The passport and passport-azure-ad modules are responsible for communicating with AAD.

  • The config variable is initialized with values from the config.js file created in the previous step.

  • An array is created for authenticatedUserTokens to store user tokens as they are passed into secured endpoints.

  • The serverPort is either defined from the process environment's port or from the configuration file.

Step 2: Instantiate an authentication strategy

As you secure an endpoint, you must provide a strategy responsible for determining whether or not the current request originates from an authenticated user. Here the authenticatonStrategy variable is an instance of the passport-azure-ad BearerStrategy class. Add the following code after the require statements.

const authenticationStrategy = new BearerStrategy(config.credentials, (token, done) => {
    let userToken = authenticatedUserTokens.find((user) => user.sub === token.sub);

    if(!userToken) {
        authenticatedUserTokens.push(token);
    }

    return done(null, user, token);
});

This implementation uses auto-registration by adding authentication tokens into the authenticatedUserTokens array if they do not already exist.

Once a new instance of the strategy is created, you must pass it into Passport via the use method. Add the following code to app.js to use the strategy in Passport.

passport.use(authenticationStrategy);

Step 3: Server configuration

With the authentication strategy defined, you can now set up the Restify server with some basic settings and set to use Passport for security.

const server = restify.createServer({ name: 'Azure Active Directroy with Node.js Demo' });
server.use(restifyPlugins.authorizationParser());
server.use(passport.initialize());
server.use(passport.session());

This server is initialized and configured to parse authorization headers and then set to use Passport.

Step 4: Define routes

You can now define routes and decide which to secure with AAD. This project includes two routes where the root level is open and the /api route is set to require authentication.

In app.js add the following code for the root level route:

server.get('/', (req, res, next) => {
    res.send(200, 'Try: curl -isS -X GET http://127.0.0.1:3000/api');
    next();
});

The root route allows all requests through the route and returns a message that includes a command to test the /api route. By contrast, the /api route is locked down using passport.authenticate. Add the following code after the root route.

server.get('/api', passport.authenticate('oauth-bearer', { session: false }), (req, res, next) => {
    res.json({ message: 'response from API endpoint' });
    return next();
});

This configuration only allows authenticated requests that include a bearer token access to /api. The option of session: false is used to disable sessions to require that a token is passed with each request to the API.

Finally, the server is set to listen on the configured port by calling the listen method.

server.listen(serverPort);

Run the sample

Now that the server is implemented, you can start the server by opening up a command prompt and enter:

npm start

With the server running, you can submit a request to the server to test the results. To demonstrate the response from the root route, open a bash shell and enter the following code:

curl -isS -X GET http://127.0.0.1:3000/

If you have configured your server correctly, the response should look similar to:

HTTP/1.1 200 OK
Server: Azure Active Directroy with Node.js Demo
Content-Type: application/json
Content-Length: 49
Date: Tue, 10 Oct 2017 18:35:13 GMT
Connection: keep-alive

Try: curl -isS -X GET http://127.0.0.1:3000/api

Next, you can test the route that requires authentication by entering the following command into your bash shell:

curl -isS -X GET http://127.0.0.1:3000/api

If you have configured the server correctly, then the server should respond with a status of Unauthorized.

HTTP/1.1 401 Unauthorized
Server: Azure Active Directroy with Node.js Demo
WWW-Authenticate: token is not found
Date: Tue, 10 Oct 2017 16:22:03 GMT
Connection: keep-alive
Content-Length: 12

Unauthorized

Now that you have created a secure API, you can implement a client that is able to pass authentication tokens to the API.

Next steps

As stated in the introduction, you must implement a client counterpart to connect to the server that handles signing in, signing out and managing tokens. For code-based examples, you may refer to the client applications in iOS and Android. For a step-by-step tutorial refer to the following article: