Building Office 365 Applications with Node.js and the Azure AD v2 app model

Earlier today I authored a post on the new Azure AD v2 app model that converges the developer experience across consumer and commercial applications. The post outlines the key differences in the v2 app model and illustrates how to perform a manual OAuth flow with it. Most developers won’t have to perform this manual flow, because the Azure AD team is building authentication libraries (ADAL) to handle OAuth on most popular platforms. ADAL is a great accelerator for application developers working with Microsoft connected services. However, the lack of an ADAL library doesn’t prevent a platform from working in this new app model. In this post, I’ll share a Node.js application that doesn’t use any special libraries to perform OAuth in the v2 app model.

[View:https://www.youtube.com/watch?v=5r5JmdoP3J4]

NOTE: Node.js has an ADAL library, but wasn’t updated to support the v2 app model flows at the time of this post. The Azure AD team is working hard on an update Node.js library. The Outlook/Exchange team has published a sample that uses the simple-oauth2 library for Node.js

authHelper.js

The solution uses an authHelper.js file, containing application registration details (client id, client secret, reply URL, permission scopes, etc) and utility functions for interacting with Azure AD. The three primary utility functions are detailed below:

  • getAuthUrl returns the authorization end-point in Azure AD with app details concatenated as URL parameters. The application can redirect to this end-point to initiate the first step of OAuth.
  • getTokenFromCode returns an access token using the app registration details and a provided authorization code (that is returned to the application after user signs in and authorizes the app)
  • getTokenFromRefreshToken returns an access token using the app registration details and a provided refresh token (that might come from cache)

authHelper.js

var https = require('https');

var appDetails = { authority: 'https://login.microsoftonline.com/common', client_id: '1d9e332b-6c7d-4554-8b51-d398fef5f8a7', client_secret: 'Y0tgHpYAy3wQ0eF9NPkMPOf', redirect_url: 'https://localhost:5858/login', scopes: 'openid+https://outlook.office.com/contacts.read+offline_access'};

//builds a redirect url based on app detailfunction getAuthUrl(res) { return appDetails.authority + '/oauth2/v2.0/authorize' +  '?client_id=' + appDetails.client_id +  '&scope=' + appDetails.scopes +  '&redirect_uri=' + appDetails.redirect_url +  '&response_type=code';};

//gets a token given an authorization codefunction getTokenFromCode(code, callback) { var payload = 'grant_type=authorization_code' +  '&redirect_uri=' + appDetails.redirect_url +  '&client_id=' + appDetails.client_id +  '&client_secret=' + appDetails.client_secret +  '&code=' + code +  '&scope=' + appDetails.scopes;  postJson('login.microsoftonline.com',   '/common/oauth2/v2.0/token',  payload,  function(token) {   callback(token);  });};

//gets a new token given a refresh tokenfunction getTokenFromRefreshToken(token, callback) { var payload = 'grant_type=refresh_token' +  '&redirect_uri=' + appDetails.redirect_url +  '&client_id=' + appDetails.client_id +  '&client_secret=' + appDetails.client_secret +  '&refresh_token=' + token +  '&scope=' + appDetails.scopes;  postJson('login.microsoftonline.com',   '/common/oauth2/v2.0/token',  payload,  function(token) {   callback(token);  });};

//performs a generic http POST and returns JSONfunction postJson(host, path, payload, callback) {  var options = {    host: host,     path: path,     method: 'POST',    headers: {       'Content-Type': 'application/x-www-form-urlencoded',      'Content-Length': Buffer.byteLength(payload, 'utf8')    }  };

  var reqPost = https.request(options, function(res) {    var body = '';    res.on('data', function(d) {      body += d;    });    res.on('end', function() {      callback(JSON.parse(body));    });    res.on('error', function(e) {      callback(null);    });  });    //write the data  reqPost.write(payload);  reqPost.end();};

exports.getAuthUrl = getAuthUrl;exports.getTokenFromCode = getTokenFromCode;exports.getTokenFromRefreshToken = getTokenFromRefreshToken;exports.TOKEN_CACHE_KEY = 'TOKEN_CACHE_KEY';

 

Application Routes

The Node.js solution was built to using express and handlebars. Two routes handle the entire flow:

Index Route

  • If the user has a cached refresh token, use it to get a new token
    • If the new token is valid, get and display data
    • If the new token is invalid, send the user to login
  • If the user doesn’t have a cached refresh token, send the user to login

Login Route

  • If the URL contains an authorization code, use it to get tokens
    • If the token is valid, cache the refresh token and send the user back to index
    • If the token is invalid, and error must have occurred
  • If the URL doesn’t contain and authorization code, get the redirect URL for authorization and send user there

Here is the JavaScript implementation of this.

Route Controller Logic

var express = require('express');var router = express.Router();var authHelper = require('../authHelper.js');var https = require('https');

/* GET home page. */router.get('/', function(req, res, next) {  if (req.cookies.TOKEN_CACHE_KEY === undefined)    res.redirect('/login');  else {    //get data    authHelper.getTokenFromRefreshToken(req.cookies.TOKEN_CACHE_KEY, function(token) {      if (token !== null) {        getJson('outlook.office.com', '/api/v1.0/me/contacts', token.access_token, function(contacts) {          if (contacts.error && contacts.error.code === 'MailboxNotEnabledForRESTAPI')            res.render('index', { title: 'My Contacts', contacts: [], restDisabled: true });          else            res.render('index', { title: 'My Contacts', contacts: contacts['value'], restDisabled: false });        });      }      else {        //TODO: handle error      }    });  }});

router.get('/login', function(req, res, next) {  //look for code from AAD reply  if (req.query.code !== undefined) {    //use the code to get a token    authHelper.getTokenFromCode(req.query.code, function(token) {      //check for null token      if (token !== null) {        res.cookie(authHelper.TOKEN_CACHE_KEY, token.refresh_token);        res.redirect('/');      }      else {        //TODO: handle error      }    });  }  else {    res.render('login', { title: 'Login', authRedirect: authHelper.getAuthUrl });    }});

//perform a fet based on parameters and return a JSON objectfunction getJson(host, path, token, callback) {  var options = {    host: host,     path: path,     method: 'GET',    headers: {       'Content-Type': 'application/json',      'Accept': 'application/json',      'Authorization': 'Bearer ' + token    }  };

  https.get(options, function(res) {    var body = '';    res.on('data', function(d) {      body += d;    });    res.on('end', function() {      callback(JSON.parse(body));    });    res.on('error', function(e) {      callback(null);    });  });};

module.exports = router;

 

Conclusion

Authentication libraries make great solution accelerators, but certainly aren’t necessary to leverage the Azure AD v2 app model or consume Microsoft connected services. You can get the full Node.js solution on GitHub using the link below:

Sample use in Blog
https://github.com/OfficeDev/Contacts-API-NodeJS-AppModelV2

Outlook/Exchange Team Sample
https://dev.outlook.com/RestGettingStarted/Tutorial/node