Node.js web app sign-in and sign-out with Azure AD

Here we use Passport to:

  • Sign the user in to the app with Azure Active Directory (Azure AD).
  • Display information about the user.
  • Sign the user out of the app.

Passport is authentication middleware for Node.js. Flexible and modular, Passport can be unobtrusively dropped in to any Express-based or restify web application. A comprehensive set of strategies support authentication that uses a username and password, Facebook, Twitter, and more. We have developed a strategy for Microsoft Azure Active Directory. We install this module and then add the Microsoft Azure Active Directory passport-azure-ad plug-in.

To do this, take the following steps:

  1. Register an app.
  2. Set up your app to use the passport-azure-ad strategy.
  3. Use Passport to issue sign-in and sign-out requests to Azure AD.
  4. Print data about the user.

The code for this tutorial is maintained on GitHub. To follow along, you can download the app's skeleton as a .zip file or clone the skeleton:

git clone --branch skeleton https://github.com/AzureADQuickStarts/WebApp-OpenIDConnect-NodeJS.git

The completed application is provided at the end of this tutorial as well.

Step 1: Register an app

  1. Sign in to the Azure portal.

  2. In the menu at the top of the page, select your account. Under the Directory list, choose the Active Directory tenant where you want to register your application.

  3. Select More Services in the menu on the left side of the screen, and then select Azure Active Directory.

  4. Select App registrations, and then select Add.

  5. Follow the prompts to create a Web Application and/or WebAPI.

    • The name of the application describes your application to users.

    • The Sign-On URL is the base URL of your app. The skeleton's default is `http://localhost:3000/auth/openid/return``.

  6. After you register, Azure AD assigns your app a unique application ID. You need this value in the following sections, so copy it from the application page.

  7. From the Settings -> Properties page for your application, update the App ID URI. The App ID URI is a unique identifier for your application. The convention is to use the format https://<tenant-domain>/<app-name>, for example: https://contoso.onmicrosoft.com/my-first-aad-app.

Step 2: Add prerequisites to your directory

  1. From the command line, change directories to your root folder if you're not already there, and then run the following commands:

    • npm install express
    • npm install ejs
    • npm install ejs-locals
    • npm install restify
    • npm install mongoose
    • npm install bunyan
    • npm install assert-plus
    • npm install passport
  2. In addition, you need passport-azure-ad:

    • npm install passport-azure-ad

This installs the libraries that passport-azure-ad depends on.

Step 3: Set up your app to use the passport-node-js strategy

Here, we configure Express to use the OpenID Connect authentication protocol. Passport is used to do various things, including issue sign-in and sign-out requests, manage the user's session, and get information about the user.

  1. To begin, open the config.js file at the root of the project, and then enter your app's configuration values in the exports.creds section.

    • The clientID is the Application Id that's assigned to your app in the registration portal.

    • The returnURL is the Redirect Uri that you entered in the portal.

    • The clientSecret is the secret that you generated in the portal.

  2. Next, open the app.js file in the root of the project. Then add the following call to invoke the OIDCStrategy strategy that comes with passport-azure-ad.

    var OIDCStrategy = require('passport-azure-ad').OIDCStrategy;
    
    // add a logger
    
    var log = bunyan.createLogger({
    name: 'Microsoft OIDC Example Web Application'
    });
    
  3. After that, use the strategy we just referenced to handle our sign-in requests.

    // Use the OIDCStrategy within Passport. (Section 2)
    //
    //   Strategies in passport require a `validate` function that accepts
    //   credentials (in this case, an OpenID identifier), and invokes a callback
    //   with a user object.
    passport.use(new OIDCStrategy({
        callbackURL: config.creds.returnURL,
        realm: config.creds.realm,
        clientID: config.creds.clientID,
        clientSecret: config.creds.clientSecret,
        oidcIssuer: config.creds.issuer,
        identityMetadata: config.creds.identityMetadata,
        skipUserProfile: config.creds.skipUserProfile,
        responseType: config.creds.responseType,
        responseMode: config.creds.responseMode
    },
    function(iss, sub, profile, accessToken, refreshToken, done) {
        if (!profile.email) {
        return done(new Error("No email found"), null);
        }
        // asynchronous verification, for effect...
        process.nextTick(function () {
        findByEmail(profile.email, function(err, user) {
            if (err) {
            return done(err);
            }
            if (!user) {
            // "Auto-registration"
            users.push(profile);
            return done(null, profile);
            }
            return done(null, user);
        });
        });
    }
    ));
    

    Passport uses a similar pattern for all its strategies (Twitter, Facebook, and so on) that all strategy writers adhere to. Looking at the strategy, you see that we pass it a function that has a token and a done as the parameters. The strategy comes back to us after it does its work. Then we want to store the user and stash the token so we don't need to ask for it again.

Important

The previous code takes any user that happens to authenticate to our server. This is known as auto-registration. We recommend that you don't let anyone authenticate to a production server without first having them register via a process that you decide on. This is usually the pattern you see in consumer apps, which allow you to register with Facebook but then ask you to provide additional information. If this weren't a sample application, we could have extracted the user's email address from the token object that is returned and then asked the user to fill out additional information. Because this is a test server, we add them to the in-memory database.

  1. Next, let's add the methods that enable us to track the signed-in users as required by Passport. These methods include serializing and deserializing the user's information.

    
            // Passport session setup. (Section 2)
    
            //   To support persistent sign-in sessions, Passport needs to be able to
            //   serialize users into the session and deserialize them out of the session. Typically,
            //   this is done simply by storing the user ID when serializing and finding
            //   the user by ID when deserializing.
            passport.serializeUser(function(user, done) {
            done(null, user.email);
            });
    
            passport.deserializeUser(function(id, done) {
            findByEmail(id, function (err, user) {
                done(err, user);
            });
            });
    
            // array to hold signed-in users
            var users = [];
    
            var findByEmail = function(email, fn) {
            for (var i = 0, len = users.length; i < len; i++) {
                var user = users[i];
            log.info('we are using user: ', user);
                if (user.email === email) {
                return fn(null, user);
                }
            }
            return fn(null, null);
            };
    
  2. Next, let's add the code to load the Express engine. Here we use the default /views and /routes pattern that Express provides.

    
        // configure Express (section 2)
    
            var app = express();
            app.configure(function() {
          app.set('views', __dirname + '/views');
          app.set('view engine', 'ejs');
          app.use(express.logger());
          app.use(express.methodOverride());
          app.use(cookieParser());
          app.use(expressSession({ secret: 'keyboard cat', resave: true, saveUninitialized: false }));
          app.use(bodyParser.urlencoded({ extended : true }));
          // Initialize Passport!  Also use passport.session() middleware, to support
          // persistent login sessions (recommended).
          app.use(passport.initialize());
          app.use(passport.session());
          app.use(app.router);
          app.use(express.static(__dirname + '/../../public'));
        });
    
  3. Finally, let's add the routes that hand off the actual sign-in requests to the passport-azure-ad engine:

   ```JavaScript

    // Our Auth routes (section 3)

    // GET /auth/openid
    //   Use passport.authenticate() as route middleware to authenticate the
    //   request. The first step in OpenID authentication involves redirecting
    //   the user to their OpenID provider. After authenticating, the OpenID
    //   provider redirects the user back to this application at
    //   /auth/openid/return.
    app.get('/auth/openid',
    passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
    function(req, res) {
        log.info('Authentication was called in the Sample');
        res.redirect('/');
    });

        // GET /auth/openid/return
        //   Use passport.authenticate() as route middleware to authenticate the
        //   request. If authentication fails, the user is redirected back to the
        //   sign-in page. Otherwise, the primary route function is called,
        //   which, in this example, redirects the user to the home page.
        app.get('/auth/openid/return',
          passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
          function(req, res) {
            log.info('We received a return from AzureAD.');
            res.redirect('/');
          });

        // POST /auth/openid/return
        //   Use passport.authenticate() as route middleware to authenticate the
        //   request. If authentication fails, the user is redirected back to the
        //   sign-in page. Otherwise, the primary route function is called,
        //   which, in this example, redirects the user to the home page.
        app.post('/auth/openid/return',
          passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
          function(req, res) {
            log.info('We received a return from AzureAD.');
            res.redirect('/');
          });
   ```

Step 4: Use Passport to issue sign-in and sign-out requests to Azure AD

Your app is now properly configured to communicate with the endpoint by using the OpenID Connect authentication protocol. passport-azure-ad has taken care of all the details of crafting authentication messages, validating tokens from Azure AD, and maintaining user sessions. All that remains is giving your users a way to sign in and sign out, and gathering additional information about the signed-in users.

  1. First, let's add the default, sign-in, account, and sign-out methods to our app.js file:

    
        //Routes (section 4)
    
        app.get('/', function(req, res){
          res.render('index', { user: req.user });
        });
    
        app.get('/account', ensureAuthenticated, function(req, res){
          res.render('account', { user: req.user });
        });
    
        app.get('/login',
          passport.authenticate('azuread-openidconnect', { failureRedirect: '/login' }),
          function(req, res) {
            log.info('Login was called in the Sample');
            res.redirect('/');
        });
    
        app.get('/logout', function(req, res){
          req.logout();
          res.redirect('/');
        });
    
  2. Let's review these in detail:

    • The /route redirects to the index.ejs view, passing the user in the request (if it exists).
    • The /account route first ensures we are authenticated (we implement that in the following example), and then passes the user in the request so that we can get additional information about the user.
    • The /login route calls our azuread-openidconnect authenticator from passport-azuread. If that doesn't succeed, it redirects the user back to /login.
    • The /logout route simply calls the logout.ejs (and route), which clears cookies and then returns the user back to index.ejs.
  3. For the last part of app.js, let's add the EnsureAuthenticated method that is used in /account, as shown earlier.

    
        // Simple route middleware to ensure user is authenticated. (section 4)
    
        //   Use this route middleware on any resource that needs to be protected. If
        //   the request is authenticated (typically via a persistent sign-in session),
        //   the request proceeds. Otherwise, the user is redirected to the
        //   sign-in page.
        function ensureAuthenticated(req, res, next) {
          if (req.isAuthenticated()) { return next(); }
          res.redirect('/login')
        }
    
  4. Finally, let's create the server itself in app.js:


        app.listen(3000);

Step 5: To display our user in the website, create the views and routes in Express

Now app.js is complete. We simply need to add the routes and views that show the information we get to the user, as well as handle the /logout and /login routes that we created.

  1. Create the /routes/index.js route under the root directory.

                /*
                 * GET home page.
                 */
    
                exports.index = function(req, res){
                  res.render('index', { title: 'Express' });
                };
    
  2. Create the /routes/user.js route under the root directory.

             ```JavaScript
             /*
              * GET users listing.
              */
    
             exports.list = function(req, res){
               res.send("respond with a resource");
             };
             ```
    

    These pass along the request to our views, including the user if present.

  3. Create the /views/index.ejs view under the root directory. This is a simple page that calls our login and logout methods and enables us to grab account information. Notice that we can use the conditional if (!user) as the user being passed through in the request is evidence we have a signed-in user.

    <% if (!user) { %>
        <h2>Welcome! Please log in.</h2>
        <a href="/login">Log In</a>
    <% } else { %>
        <h2>Hello, <%= user.displayName %>.</h2>
        <a href="/account">Account Info</a></br>
        <a href="/logout">Log Out</a>
    <% } %>
    
  4. Create the /views/account.ejs view under the root directory so that we can view additional information that passport-azuread has put in the user request.

    <% if (!user) { %>
        <h2>Welcome! Please log in.</h2>
        <a href="/login">Log In</a>
    <% } else { %>
    <p>displayName: <%= user.displayName %></p>
    <p>givenName: <%= user.name.givenName %></p>
    <p>familyName: <%= user.name.familyName %></p>
    <p>UPN: <%= user._json.upn %></p>
    <p>Profile ID: <%= user.id %></p>
    ##Next steps  <p>Full Claimes</p>
    <%- JSON.stringify(user) %>
    <p></p>
    <a href="/logout">Log Out</a>
    <% } %>
    
  5. Let's make this look good by adding a layout. Create the '/views/layout.ejs' view under the root directory.

    
    <!DOCTYPE html>
    <html>
        <head>
            <title>Passport-OpenID Example</title>
        </head>
        <body>
            <% if (!user) { %>
                <p>
                <a href="/">Home</a> |
                <a href="/login">Log In</a>
                </p>
            <% } else { %>
                <p>
                <a href="/">Home</a> |
                <a href="/account">Account</a> |
                <a href="/logout">Log Out</a>
                </p>
            <% } %>
            <%- body %>
        </body>
    </html>
    

Next steps

Finally, build and run your app. Run node app.js, and then go to http://localhost:3000.

Sign in with either a personal Microsoft account or a work or school account, and notice how the user's identity is reflected in the /account list. You now have a web app that's secured with industry standard protocols that can authenticate users with both their personal and work/school accounts.

For reference, the completed sample (without your configuration values) is provided as a .zip file. Alternatively, you can clone it from GitHub:

git clone --branch complete https://github.com/AzureADQuickStarts/WebApp-OpenIDConnect-NodeJS.git

You can now move onto more advanced topics. You might want to try:

Secure a Web API with Azure AD

Additional resources

Get security updates for our products

We encourage you to get notifications of when security incidents occur by visiting the TechCenter page for Microsoft technical security notifications and subscribing to security advisory alerts.