Creating a Node.js Application Secured by Azure AD

This post shows how to create a Node.js application using Node.js, Bash for Windows, Visual Studio Code, and Azure Active Directory.  The final solution is available at https://github.com/kaevans/node-aad

Background

I’ve been a C# .NET developer for the past 17 years  Sure, I’ve coded with other languages (C++, Java, JavaScript, Pascal, F#, Visual Basic, Ruby), but given a deadline I would always use C# as that is what I am most productive in, and I am very comfortable with Visual Studio.  I recently decided to leave my comfort zone of Windows, .NET, and SQL to venture into Linux and Node.js.  I wanted to learn how to use various Azure services using Node.js, so the first that I picked was using Azure AD to secure a web application. 

I went to an article, Node.js web app sign-in and sign-out with Azure AD, followed the steps, and it didn’t work.  I went to GitHub and cloned a repository, https://github.com/AzureAD/passport-azure-ad, and it didn’t work.  After wasting a really long time trying to figure it out, I discovered that those two resources (as of today’s date) use Node.js version 4.x, which is the current Long Term Support (LTS) version of Node.  Following the instructions below, I ended up with Node version 7.4.0.  The versions are not backwards-compatible between major releases, which explains why I couldn’t get things to work.

image

You could get things to work by using a Node version manager.  A popular one is “n” which lets you interactively select which version of Node you are using.  The rest of this post assumes Node version 7.4.0.

This post is going to look scary long.  It’s actually not that bad, I am just verbose.  The completed solution is available at https://github.com/kaevans/node-aad

My Environment

I am not using a Linux desktop, just using it as a server.  I used vi in Linux enough to realize I prefer the productivity of menus and a mouse, so I have a Windows 10 desktop running Visual Studio Code.  However, I fell in love with Bash, so I enabled the Bash for Windows feature.  This lets me use the Bash shell for things like Node.js (including npm) and Express web server.  Once I code and test locally, I deploy the code to a Linux VM running in Azure.  If you are using a Mac, you can use the Bash shell natively and you can still install Visual Studio Code. 

Create an Azure AD Application

Go to the Azure portal, under Azure Active Directory / App Registrations and add a new registration.  Create it as a Web app / API with a sign-on URL of https://localhost.

image

Once created, go to the properties of your new app registration.  The App ID URI will have a GUID, you may wish to change that GUID to the name of your app (completely an optional step, but one that I prefer).

image

In this screen shot, item #1 is the application ID, also known as the client ID.  The App ID URI is also known as the Audience URI.  We’ll use those values in our configuration at the end of the post.

If your application will access other APIs, you will need a key.  To do that, go to the Keys setting of your app registration and add a new key.

image

Copy this value (you won’t be able to retrieve it later).  This key is also known as the client secret.

Install Node.js and Express Generator

I am going to assume that you already have Bash on Windows enabled (more details here), and you have Visual Studio Code installed.  Open a command line and just type “bash” to start the Bash shell.  This enables you to install Node.js using a package manager in Bash.  Since Bash on Windows uses Ubuntu, you need to choose the Ubuntu directions for installing Node.js. 

 curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo apt-get install -y build-essential

Now that we have Node.js, we can use the Node Package Manager (NPM) to install dependencies similar to how we’ve used NuGet for packages in Visual Studio.  Let’s install the Express Generator that lets us quickly scaffold Node.js applications that use Express.  We use the “-g” switch to make this a global dependency.

 npm install express-generator -g

So, where to keep our project?  Bash for Windows creates a mounted file system to access your Windows files, so wherever we keep them in Windows is accessible via Bash as well.  As an example, I created a directory “Source” under my user profile.

image

Just for completeness sake, I am showing the same directory using the old CMD shell in Windows to prove you can easily access those files in Windows.

image

Now close the CMD shell, we will only use the Bash shell during this post!

Create the Application

Now that we have a folder to keep our new application, we need to install some libraries.  Change directory to the Source directory and then use the Express Generator to scaffold the application.  In the Bash shell:

 express node-aad

Open Visual Studio Code and then open the node-aad folder that was just created.

image

If you go to the debug menu in VS Code and hit Start, the application will fail with an error “Exception occured Error: Cannot find module 'express'”.  Yes, “occurred” is misspelled, but the more important thing to note is that the packages.json file contains a list of dependencies that haven’t been downloaded yet, kind of like NuGet package refresh.  This allows you to check your project into source control without all the dependency binaries.  To download the binaries, we go back to the Bash shell and change directory to our project’s folder.

 npm install

The output shows all the binaries that were downloaded.

image

To test the application, in the Bash shell try to following:

 npm start

Now switch to a browser and go to https://localhost:3000.  You will see your application.

image

Watch the Bash shell window to see the requests.

image

Hit Ctrl+C to stop the server.  We now have a working Node.js web application.  Let’s secure it with Azure AD.

Update Your Application to Use Azure AD with Passport

No, I don’t mean Passport as in the precursor to Microsoft Accounts (formerly Live accounts, formerly Hotmail accounts, formerly Passport accounts), I mean Passport the authentication middleware for Node.js.  Passport makes it easy to use different strategies for authenticating to services such as Facebook, Twitter, and more.  We will use the passport-azure-ad package that provides an OpenId Connect strategy for authenticating to Azure AD.  Go to the Bash shell and run the following.  This will install the packages method-override, express-session, passport, and passport-azure-ad, and the “—save” switch will update the package.js file to include the references.

 npm install --save method-override express-session passport passport-azure-ad

Go check package.js and you will see that it has been updated.

Enable HTTPS

We are using OAuth, which means we are passing a bearer token in an HTTP header, which means using HTTPS is mandatory.  In fact, this is one of the things that didn’t work when I tried the directions for Node.js 4.x using Node.js 7.4.0… the sample I linked to in the opening paragraph allowed you to use HTTP but that doesn’t work with Node 7.4.0.  Creating an HTTPS server using Express is really simple.  Go to the Bash shell and create the signing key, certificate request, and certificate, then delete the request file.

 openssl genrsa -out key.pem
openssl req -new -key key.pem -out csr.pem
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
rm csr.pem

Now update app.js.  At the top of the file, add the require statements for https and fs.

Requires

  1. var https = require('https');
  2. var fs = require('fs');

At the bottom of the file, just before the last line of code, add the following:

Create HTTPS Server

  1. var options = {
  2.     key: fs.readFileSync('./myappkey.pem'),
  3.     cert: fs.readFileSync('./myappcert.pem')
  4. };
  5. // Create an HTTPS service identical to the HTTP service.
  6. https.createServer(options, app).listen(443);

To test, go to the Bash shell and use “npm start” to start the application, then open a browser to “https://localhost” to see your application now listens to port 443 instead of 3000.  You may get an unhandled error:

image

If that is the case, then try running it using the sudo command:

 sudo npm start app.js

image

Note: this is using a self-signed certificate and is not secure for production use.

Using Passport for Authentication

In Visual Studio Code, add a new file called config.js. 

config.js

  1. exports.creds = {
  2.     returnURL: process.env.RETURN_URL,
  3.     identityMetadata: 'https://login.microsoftonline.com/common/.well-known/openid-configuration', // For using Microsoft you should never need to change this.
  4.     clientID: process.env.CLIENT_ID,
  5.     clientSecret: process.env.CLIENT_SECRET, // if you are doing code or id_token code
  6.     skipUserProfile: true, // for AzureAD should be set to true.
  7.     responseType: 'id_token code', // for login only flows use id_token. For accessing resources use `id_token code`
  8.     responseMode: 'query', // For login only flows we should have token passed back to us in a POST
  9.     scope: ['email', 'profile'] // additional scopes you may wish to pass
  10. };

Notice that we do not put hard-coded values for variable items in this file, and we certainly never ever never ever put secrets (like client ID and client secret) into code. 

Add the following to the list of “require” statements in app.js.

Require config

  1. var config = require('./config');

Beneath the require statements, add the following:

Configure Passport

  1. // array to hold logged in users
  2. var users = [];
  3.  
  4. //   To support persistent login sessions, Passport needs to be able to
  5. //   serialize users into and deserialize users out of the session.  Typically,
  6. //   this will be as simple as storing the user ID when serializing, and finding
  7. //   the user by ID when deserializing.
  8. passport.serializeUser(function (user, done) {
  9.     done(null, user.oid);
  10. });
  11.  
  12. passport.deserializeUser(function (id, done) {
  13.     findByOID(id, function (err, user) {
  14.         done(err, user);
  15.     });
  16. });
  17.  
  18.  
  19. var findByOID = function (oid, fn) {
  20.     for (var i = 0, len = users.length; i < len; i++) {
  21.         var user = users[i];
  22.  
  23.         if (user.oid === oid) {
  24.             return fn(null, user);
  25.         }
  26.     }
  27.     console.log("Did not find user by OID: " + oid);
  28.     return fn(null, null);
  29. };
  30.  
  31. // Use the OIDCStrategy within Passport. (Section 2)
  32. //
  33. //   Strategies in passport require a `validate` function, which accept
  34. //   credentials (in this case, an OpenID identifier), and invoke a callback
  35. //   with a user object.
  36. passport.use(new OIDCStrategy({
  37.     callbackURL: config.creds.returnURL,
  38.     redirectUrl: config.creds.redirectUrl,
  39.     realm: config.creds.realm,
  40.     clientID: config.creds.clientID,
  41.     clientSecret: config.creds.clientSecret,
  42.     oidcIssuer: config.creds.issuer,
  43.     identityMetadata: config.creds.identityMetadata,
  44.     skipUserProfile: config.creds.skipUserProfile,
  45.     responseType: config.creds.responseType,
  46.     responseMode: config.creds.responseMode,
  47.     scope: config.creds.scope
  48. },
  49.     function (iss, sub, profile, accessToken, refreshToken, done) {
  50.         if (!profile.oid) {
  51.             console.log(util.inspect(profile));
  52.             return done(new Error("No OID found"), null);
  53.         }
  54.         // asynchronous verification, for effect...
  55.         process.nextTick(function () {
  56.             findByOID(profile.oid, function (err, user) {
  57.                 if (err) {
  58.                     return done(err);
  59.                 }
  60.                 if (!user) {
  61.                     // "Auto-registration"
  62.                     users.push(profile);
  63.                     return done(null, profile);
  64.                 }
  65.                 return done(null, user);
  66.             });
  67.         });
  68.     }
  69. ));

Next, we create the app object and tell it what middleware to inject.

App middleware

  1. var app = express();
  2.  
  3. // view engine setup
  4. app.set('views', path.join(__dirname, 'views'));
  5. app.set('view engine', 'jade');
  6.  
  7. // uncomment after placing your favicon in /public
  8. //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
  9. app.use(logger('dev'));
  10. app.use(methodOverride());
  11. app.use(bodyParser.json());
  12. app.use(bodyParser.urlencoded({ extended: true }));
  13. app.use(cookieParser());
  14. app.use(session({
  15.     secret: 'keyboard cat',
  16.     resave: true,
  17.     saveUninitialized: false
  18. }));
  19. //Initialize Passport
  20. app.use(passport.initialize());
  21. app.use(passport.session());
  22. app.use(express.static(path.join(__dirname, 'public')));

Finally we add our routes.

Routes

  1. // GET /auth/openid
  2. //   Use passport.authenticate() as route middleware to authenticate the
  3. //   request. The first step in OpenID authentication involves redirecting
  4. //   the user to their OpenID provider. After authenticating, the OpenID
  5. //   provider redirects the user back to this application at
  6. //   /auth/openid/return.
  7. app.get('/auth/openid',
  8.     passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
  9.     function (req, res) {
  10.         console.log('Authentication was called in the Sample');
  11.         res.redirect('/');
  12.     });
  13.  
  14. // GET /auth/openid/return
  15. //   Use passport.authenticate() as route middleware to authenticate the
  16. //   request. If authentication fails, the user is redirected back to the
  17. //   sign-in page. Otherwise, the primary route function is called,
  18. //   which, in this example, redirects the user to the home page.
  19. app.get('/auth/openid/return',
  20.     passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
  21.     function (req, res) {
  22.         console.log('We received a return from AzureAD.');
  23.         res.redirect('/');
  24.     });
  25.  
  26. // POST /auth/openid/return
  27. //   Use passport.authenticate() as route middleware to authenticate the
  28. //   request. If authentication fails, the user is redirected back to the
  29. //   sign-in page. Otherwise, the primary route function is called,
  30. //   which, in this example, redirects the user to the home page.
  31. app.post('/auth/openid/return',
  32.     passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
  33.     function (req, res) {
  34.         console.log('We received a return from AzureAD.');
  35.         res.redirect('/');
  36.     });
  37.  
  38.  
  39.  
  40. //Routes (section 4)
  41.  
  42. app.get('/', index);
  43.  
  44. app.get('/account', ensureAuthenticated, function (req, res) {
  45.     res.render('account', { user: req.user });
  46. });
  47.  
  48. app.get('/login',
  49.     passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
  50.     function (req, res) {
  51.         console.log('Login was called in the Sample');
  52.         res.redirect('/');
  53.     });
  54.  
  55. app.get('/logout', function (req, res) {
  56.     req.logout();
  57.     res.redirect('/');
  58. });
  59.  
  60.  
  61. // catch 404 and forward to error handler
  62. app.use(function (req, res, next) {
  63.     var err = new Error('Not Found');
  64.     err.status = 404;
  65.     next(err);
  66. });
  67.  
  68. // error handler
  69. app.use(function (err, req, res, next) {
  70.     // set locals, only providing error in development
  71.     res.locals.message = err.message;
  72.     res.locals.error = req.app.get('env') === 'development' ? err : {};
  73.  
  74.     // render the error page
  75.     res.status(err.status || 500);
  76.     res.render('error');
  77. });
  78.  
  79.  
  80. // Simple route middleware to ensure user is authenticated. (section 4)
  81.  
  82. //   Use this route middleware on any resource that needs to be protected. If
  83. //   the request is authenticated (typically via a persistent sign-in session),
  84. //   the request proceeds. Otherwise, the user is redirected to the
  85. //   sign-in page.
  86. function ensureAuthenticated(req, res, next) {
  87.     if (req.isAuthenticated()) { return next(); }
  88.     res.redirect('/login')
  89. }

Making It Readable

We now turn to making it readable so someone can actually use our application.  Let’s update the controller for the Index view to pass both the user and the title to the view.

Index.js

  1. var express = require('express');
  2. var router = express.Router();
  3.  
  4. /* GET home page. */
  5. router.get('/', function (req, res, next) {
  6.     console.log('index was called');
  7.     res.render('index', { user: req.user, title: 'Express' });
  8. });
  9.  
  10. module.exports = router;

Now let’s update the view. 

index.jade

  1. extends layout
  2.  
  3. block content
  4.   h1= title
  5.   p Welcome to #{title}

Notice the Jade file starts with “extends layout”.  We can use this like a master page in ASP.NET (or a layout in ASP.NET MVC).  Update the layout.jade file.

layout.jade

  1. doctype html
  2. html
  3.   head
  4.     title= title
  5.     link(rel='stylesheet', href='/stylesheets/style.css')
  6.   body
  7.     div(id="navbar")
  8.       if user      
  9.         span
  10.           a(href="/") Home
  11.         span
  12.           a(href="/account") Account
  13.         span
  14.           a(href="/logout") Log out
  15.             else
  16.         span
  17.           a(href="/") Home
  18.         span
  19.           a(href="/login") Log in
  20.     block content

In our app.js file, there’s a route for /account.  We need a view for that as well.  Add a new “account.jade” file in the views folder.

account.jade

  1. extends layout
  2.  
  3. block content
  4.   if user
  5.     p displayName: #{user.displayName}
  6.     p givenName: #{user.givenName}
  7.     p familyName: #{user.familyName}
  8.     p UPN: #{user._json.upn }
  9.       p Profile ID: #{user.id}
  10.     p=JSON.stringify(user)
  11.     a(href="/logout") Log out
  12.   else
  13.     h1 Please log in
  14.     a(href="/login") Log in


The last thing we need to do it to add a little styling so the navigation is easier to read.  Update style.css in the public/stylesheets folder to include padding.

style.css

  1. body {
  2.   padding: 50px;
  3.   font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
  4. }
  5.  
  6. a {
  7.   color: #00B7FF;
  8. }
  9.  
  10. #navbar span{
  11.   padding-right: 20px;  
  12. }

Check It In

We want to commit the code to git, but we don’t need to commit all those dependencies.  In the root of the project, create a file “.gitignore” and add the following:

.gitignore

  1. # .gitignore

  2. node_modules

  3. .vscode

  4. *.pem

  5. *.log

This will prevent checking in any files in the node_modules folder, the .vscode folder (will be created in the next step), the certificates for the HTTPS server, or any log files.

In VS Code, you can now go to the Git tool and initialize the repository and check your changes in.

image

Test It Out

Our config.js file uses a lot of environment variables.  We need a way to pass those into our program when debugging locally.  One way is to use the Bash command:

sudo CLIENT_ID=YOUR CLIENT_ID CLIENT_SECRET=YOUR_APP_SECRET_KEY REDIRECT_URL=https://localhost/auth/openid/return node app.js

Replace the placeholders for client ID and app secret with the values from your Azure AD app. 

Another way to do this is to set the environment information within VS Code.  Go to the debugging tab, then click Settings. 

image

That will create a .vscode/launch.json file that makes it easy for you to configure the environment variables necessary for debugging your application without creating a script or actually setting those variables for your environment.  Replace with the values for your Azure AD application.

launch.json

  1. {
  2.   // Use IntelliSense to learn about possible Node.js debug attributes.
  3.   // Hover to view descriptions of existing attributes.
  4.   // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  5.   "version": "0.2.0",
  6.   "configurations":
  7.   [
  8.     {
  9.       "type": "node",
  10.       "request": "launch",
  11.       "name": "Launch Program",
  12.       "program": "${workspaceRoot}/app.js",
  13.       "env":
  14.       {
  15.         "CLIENT_ID": "2f992fd9-04dc-4809-931f-3b5eca6405f3",
  16.         "CLIENT_SECRET": "6abc/rABCabcaaABCabcabcdefcjABsABCu1ABGabSg=",
  17.         "REDIRECT_URL": "https://localhost/auth/openid/return",
  18.         "AUDIENCE": "https://BlueSkyAbove.onmicrosoft.com/node-aad",
  19.         "IDENTITY_METADATA": "https://login.microsoftonline.com/blueskyabove.onmicrosoft.com/.well-known/openid-configuration"
  20.       }
  21.     },
  22.     {
  23.       "type": "node",
  24.       "request": "attach",
  25.       "name": "Attach to Process",
  26.       "address": "localhost",
  27.       "port": 5858
  28.     }
  29.   ]
  30. }


In VS Code, you can now click the Debug button to start debugging, giving you access to breakpoints, step-through, watch locals, etc.

image

Now open a browser in InPrivate (Internet Explorer) or Incognito (Chrome) to ignore existing cookies.  Navigate to https://localhost and you will see your new application!

image

Click Log In and see that you are prompted to log into Azure AD.

image

Once you log in, the UI changes to indicate you are logged in.

image

Click Account to see details about the claims in your token.

image

Git the Source

If you weren’t able to follow along or just want to download the source, the completed solution is available at https://github.com/kaevans/node-aad.  At a command line:

 git clone https://github.com/kaevans/node-aad.git

To run the application, you will need to complete a few steps:

  1. The node modules are not checked into source, but are referenced in the package.json file.  Just change directory to the project root and use the command “npm install” to download all of the dependencies so that you can debug locally.  Checking these into source would bloat your repository needlessly. 
  2. The certificates are not checked into source (these are secrets).  Follow the directions above for using openssl to generate the certificates.  Do not check these into source!
  3. The .vscode/launch.json is not checked into source (it has secrets in it).  Follow the directions above for creating/modifying the .vscode/launch.json file.  Do not check this into source!

For More Information

Bash on Windows

Creating an HTTPS server using Express

https://github.com/kaevans/node-aad – Completed source code

Node.js web app sign-in and sign-out with Azure AD – instructions to build a Node.js 4.x application, some changes required for a 7.x application.

https://github.com/AzureAD/passport-azure-ad – Completed sample for a Node.js 4.x application, some changes required for a 7.x application.