Build Node.js Express apps with Microsoft Graph
This tutorial teaches you how to build a Node.js Express web app that uses the Microsoft Graph API to retrieve calendar information for a user.
Tip
If you prefer to just download the completed tutorial, you can download it in two ways.
- Download the Node.js quick start to get working code in minutes.
- Download or clone the GitHub repository.
Prerequisites
Before you start this demo, you should have Node.js installed on your development machine. If you do not have Node.js, visit the previous link for download options.
Note
This tutorial was written with Node version 10.7.0. The steps in this guide may work with other versions, but that has not been tested.
Feedback
Please provide any feedback on this tutorial in the GitHub repository.
Create a Node.js Express web app
In this exercise you will use Express to build a web app. If you don't already have the Express generator installed, you can install it from your command-line interface (CLI) with the following command.
npm install express-generator -g
Open your CLI, navigate to a directory where you have rights to create files, and run the following command to create a new Express app that uses Handlebars as the rendering engine.
express --hbs graph-tutorial
The Express generator creates a new directory called graph-tutorial
and scaffolds an Express app. Navigate to this new directory and enter the following command to install dependencies.
npm install
Once that command completes, use the following command to start a local web server.
npm start
Open your browser and navigate to http://localhost:3000
. If everything is working, you will see a "Welcome to Express" message. If you don't see that message, check the Express getting started guide.
Before moving on, install some additional gems that you will use later:
- dotenv for loading values from a .env file.
- moment for formatting date/time values.
- connect-flash to flash error messages in the app.
- express-session to store values in an in-memory server-side session.
- passport-azure-ad for authenticating and getting access tokens.
- simple-oauth2 for token management.
- microsoft-graph-client for making calls to Microsoft Graph.
Run the following command in your CLI.
npm install dotenv@6.1.0 moment@2.22.2 connect-flash@0.1.1 express-session@1.15.6
npm install passport-azure-ad@4.0.0 simple-oauth2@2.2.1 @microsoft/microsoft-graph-client@1.3.0
Now update the application to use the connect-flash
and express-session
middleware. Open the ./app.js
file and add the following require
statement to the top of the file.
var session = require('express-session');
var flash = require('connect-flash');
Add the following code immediately after the var app = express();
line.
// Session middleware
// NOTE: Uses default in-memory session store, which is not
// suitable for production
app.use(session({
secret: 'your_secret_value_here',
resave: false,
saveUninitialized: false,
unset: 'destroy'
}));
// Flash middleware
app.use(flash());
// Set up local vars for template layout
app.use(function(req, res, next) {
// Read any flashed errors and save
// in the response locals
res.locals.error = req.flash('error_msg');
// Check for simple error string and
// convert to layout's expected format
var errs = req.flash('error');
for (var i in errs){
res.locals.error.push({message: 'An error occurred', debug: errs[i]});
}
next();
});
Design the app
Start by creating the global layout for the app. Open the ./views/layout.hbs
file and replace the entire contents with the following code.
<!DOCTYPE html>
<html>
<head>
<title>Node.js Graph Tutorial</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css"
integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<link rel='stylesheet' href='/stylesheets/style.css' />
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"
integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container">
<a href="/" class="navbar-brand">Node.js Graph Tutorial</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a href="/" class="nav-link{{#if active.home}} active{{/if}}">Home</a>
</li>
{{#if user}}
<li class="nav-item" data-turbolinks="false">
<a href="/calendar" class="nav-link{{#if active.calendar}} active{{/if}}">Calendar</a>
</li>
{{/if}}
</ul>
<ul class="navbar-nav justify-content-end">
<li class="nav-item">
<a class="nav-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>Docs
</a>
</li>
{{#if user}}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true"
aria-expanded="false">
{{#if user.avatar}}
<img src="{{ user.avatar }}" class="rounded-circle align-self-center mr-2" style="width: 32px;">
{{else}}
<i class="far fa-user-circle fa-lg rounded-circle align-self-center mr-2" style="width: 32px;"></i>
{{/if}}
</a>
<div class="dropdown-menu dropdown-menu-right">
<h5 class="dropdown-item-text mb-0">{{ user.displayName }}</h5>
<p class="dropdown-item-text text-muted mb-0">{{ user.email }}</p>
<div class="dropdown-divider"></div>
<a href="/auth/signout" class="dropdown-item">Sign Out</a>
</div>
</li>
{{else}}
<li class="nav-item">
<a href="/auth/signin" class="nav-link">Sign In</a>
</li>
{{/if}}
</ul>
</div>
</div>
</nav>
<main role="main" class="container">
{{#each error}}
<div class="alert alert-danger" role="alert">
<p class="mb-3">{{ this.message }}</p>
{{#if this.debug }}
<pre class="alert-pre border bg-light p-2"><code>{{ this.debug }}</code></pre>
{{/if}}
</div>
{{/each}}
{{{body}}}
</main>
</body>
</html>
This code adds Bootstrap for simple styling, and Font Awesome for some simple icons. It also defines a global layout with a nav bar.
Now open ./public/stylesheets/style.css
and replace its entire contents with the following.
body {
padding-top: 4.5rem;
}
.alert-pre {
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
Now update the default page. Open the ./views/index.hbs
file and replace its contents with the following.
<div class="jumbotron">
<h1>Node.js Graph Tutorial</h1>
<p class="lead">This sample app shows how to use the Microsoft Graph API to access Outlook and OneDrive data from Node.js</p>
{{#if user}}
<h4>Welcome {{ user.displayName }}!</h4>
<p>Use the navigation bar at the top of the page to get started.</p>
{{else}}
<a href="/auth/signin" class="btn btn-primary btn-large">Click here to sign in</a>
{{/if}}
</div>
Open the ./routes/index.js
file and replace the existing code with the following.
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
let params = {
active: { home: true }
};
res.render('index', params);
});
module.exports = router;
Save all of your changes and restart the server. Now, the app should look very different.
Register the app in the portal
In this exercise, you will create a new Azure AD web application registration using the Application Registry Portal (ARP).
Open a browser and navigate to the Application Registration Portal. Login using a personal account (aka: Microsoft Account) or Work or School Account.
Select Add an app at the top of the page.
Note
If you see more than one Add an app button on the page, select the one that corresponds to the Converged apps list.
On the Register your application page, set the Application Name to Node.js Graph Tutorial and select Create.
On the Node.js Graph Tutorial Registration page, under the Properties section, copy the Application Id as you will need it later.
Scroll down to the Application Secrets section.
Select Generate New Password.
In the New password generated dialog, copy the contents of the box as you will need it later.
Important: This password is never shown again, so make sure you copy it now.
Scroll down to the Platforms section.
Select Add Platform.
In the Add Platform dialog, select Web.
In the Web platform box, enter the URL
http://localhost:3000/auth/callback
for the Redirect URLs.
Scroll to the bottom of the page and select Save.
Add Azure AD authentication
In this exercise you will extend the application from the previous exercise to support authentication with Azure AD. This is required to obtain the necessary OAuth access token to call the Microsoft Graph. In this step you will integrate the passport-azure-ad library into the application.
Create a new file named .env
file in the root of your application, and add the following code.
OAUTH_APP_ID=YOUR_APP_ID_HERE
OAUTH_APP_PASSWORD=YOUR_APP_PASSWORD_HERE
OAUTH_REDIRECT_URI=http://localhost:3000/auth/callback
OAUTH_SCOPES='profile offline_access user.read calendars.read'
OAUTH_AUTHORITY=https://login.microsoftonline.com/common
OAUTH_ID_METADATA=/v2.0/.well-known/openid-configuration
OAUTH_AUTHORIZE_ENDPOINT=/oauth2/v2.0/authorize
OAUTH_TOKEN_ENDPOINT=/oauth2/v2.0/token
Replace YOUR APP ID HERE
with the application ID from the Application Registration Portal, and replace YOUR APP SECRET HERE
with the password you generated.
Important
If you're using source control such as git, now would be a good time to exclude the .env
file from source control to avoid inadvertently leaking your app ID and password.
Open ./app.js
and add the following line to the top of the file to load the .env
file.
require('dotenv').config();
Implement sign-in
Locate the line var indexRouter = require('./routes/index');
in ./app.js
. Insert the following code before that line.
var passport = require('passport');
var OIDCStrategy = require('passport-azure-ad').OIDCStrategy;
// Configure passport
// In-memory storage of logged-in users
// For demo purposes only, production apps should store
// this in a reliable storage
var users = {};
// Passport calls serializeUser and deserializeUser to
// manage users
passport.serializeUser(function(user, done) {
// Use the OID property of the user as a key
users[user.profile.oid] = user;
done (null, user.profile.oid);
});
passport.deserializeUser(function(id, done) {
done(null, users[id]);
});
// Callback function called once the sign-in is complete
// and an access token has been obtained
async function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) {
if (!profile.oid) {
return done(new Error("No OID found in user profile."), null);
}
// Save the profile and tokens in user storage
users[profile.oid] = { profile, accessToken };
return done(null, users[profile.oid]);
}
// Configure OIDC strategy
passport.use(new OIDCStrategy(
{
identityMetadata: `${process.env.OAUTH_AUTHORITY}${process.env.OAUTH_ID_METADATA}`,
clientID: process.env.OAUTH_APP_ID,
responseType: 'code id_token',
responseMode: 'form_post',
redirectUrl: process.env.OAUTH_REDIRECT_URI,
allowHttpForRedirectUrl: true,
clientSecret: process.env.OAUTH_APP_PASSWORD,
validateIssuer: false,
passReqToCallback: false,
scope: process.env.OAUTH_SCOPES.split(' ')
},
signInComplete
));
This code initializes the Passport.js library to use the passport-azure-ad
library, and configures it with the app ID and password for the app.
Now pass the passport
object to the Express app. Locate the line app.use('/', indexRouter);
in ./app.js
. Insert the following code before that line.
// Initialize passport
app.use(passport.initialize());
app.use(passport.session());
Create a new file in the ./routes
directory named auth.js
and add the following code.
var express = require('express');
var passport = require('passport');
var router = express.Router();
/* GET auth callback. */
router.get('/signin',
function (req, res, next) {
passport.authenticate('azuread-openidconnect',
{
response: res,
prompt: 'login',
failureRedirect: '/',
failureFlash: true
}
)(req,res,next);
},
function(req, res) {
res.redirect('/');
}
);
router.post('/callback',
function(req, res, next) {
passport.authenticate('azuread-openidconnect',
{
response: res,
failureRedirect: '/',
failureFlash: true
}
)(req,res,next);
},
function(req, res) {
// TEMPORARY!
// Flash the access token for testing purposes
req.flash('error_msg', {message: 'Access token', debug: req.user.accessToken});
res.redirect('/');
}
);
router.get('/signout',
function(req, res) {
req.session.destroy(function(err) {
req.logout();
res.redirect('/');
});
}
);
module.exports = router;
This defines a router with three routes: signin
, callback
, and signout
.
The signin
route calls the passport.authenticate
method, causing the app to redirect to the Azure login page.
The callback
route is where Azure redirects after the signin is complete. The code calls the passport.authenticate
method again, causing the passport-azure-ad
strategy to request an access token. Once the token is obtained, the next handler is called, which redirects back to the home page with the access token in the temporary error value. We'll use this to verify that our sign-in is working before moving on. Before we test, we need to configure the Express app to use the new router from ./routes/auth.js
.
The signout
method logs the user out and destroys the session.
Insert the following code before the var app = express();
line.
var authRouter = require('./routes/auth');
Then insert the following code after the app.use('/', indexRouter);
line.
app.use('/auth', authRouter);
Start the server and browse to https://localhost:3000
. Click the sign-in button and you should be redirected to https://login.microsoftonline.com
. Login with your Microsoft account and consent to the requested permissions. The browser redirects to the app, showing the token.
Get user details
Start by creating a new file to hold all of your Microsoft Graph calls. Create a new file in the root of the project named graph.js
and add the following code.
var graph = require('@microsoft/microsoft-graph-client');
module.exports = {
getUserDetails: async function(accessToken) {
const client = getAuthenticatedClient(accessToken);
const user = await client.api('/me').get();
return user;
}
};
function getAuthenticatedClient(accessToken) {
// Initialize Graph client
const client = graph.Client.init({
// Use the provided access token to authenticate
// requests
authProvider: (done) => {
done(null, accessToken);
}
});
return client;
}
This exports the getUserDetails
function, which uses the Microsoft Graph SDK to call the /me
endpoint and return the result.
Update the signInComplete
method in /app.s
to call this function. First, add the following require
statements to the top of the file.
var graph = require('./graph');
Replace the existing signInComplete
function with the following code.
async function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) {
if (!profile.oid) {
return done(new Error("No OID found in user profile."), null);
}
try{
const user = await graph.getUserDetails(accessToken);
if (user) {
// Add properties to profile
profile['email'] = user.mail ? user.mail : user.userPrincipalName;
}
} catch (err) {
done(err, null);
}
// Save the profile and tokens in user storage
users[profile.oid] = { profile, accessToken };
return done(null, users[profile.oid]);
}
The new code updates the profile
provided by Passport to add an email
property, using the data returned by Microsoft Graph.
Finally, add code to ./app.js
to load the user profile into the locals
property of the response. This will make it available to all of the views in the app.
Add the following after the app.use(passport.session());
line.
app.use(function(req, res, next) {
// Set the authenticated user in the
// template locals
if (req.user) {
res.locals.user = req.user.profile;
}
next();
});
Storing the tokens
Now that you can get tokens, it's time to implement a way to store them in the app. Currently, the app is storing the raw access token in the in-memory user storage. Since this is a sample app, for simplicity's sake, you'll continue to store them there. A real-world app would use a more reliable secure storage solution, like a database.
However, storing just the access token doesn't allow you to check expiration or refresh the token. In order to enable that, update the sample to wrap the tokens in an AccessToken
object from the simple-oauth2
library.
First, in ./app.js
, add the following code before the signInComplete
function.
// Configure simple-oauth2
const oauth2 = require('simple-oauth2').create({
client: {
id: process.env.OAUTH_APP_ID,
secret: process.env.OAUTH_APP_PASSWORD
},
auth: {
tokenHost: process.env.OAUTH_AUTHORITY,
authorizePath: process.env.OAUTH_AUTHORIZE_ENDPOINT,
tokenPath: process.env.OAUTH_TOKEN_ENDPOINT
}
});
Then, update the signInComplete
function to create an AccessToken
from the raw tokens passed in and store that in the user storage. Replace the existing signInComplete
function with the following.
async function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) {
if (!profile.oid) {
return done(new Error("No OID found in user profile."), null);
}
try{
const user = await graph.getUserDetails(accessToken);
if (user) {
// Add properties to profile
profile['email'] = user.mail ? user.mail : user.userPrincipalName;
}
} catch (err) {
done(err, null);
}
// Create a simple-oauth2 token from raw tokens
let oauthToken = oauth2.accessToken.create(params);
// Save the profile and tokens in user storage
users[profile.oid] = { profile, oauthToken };
return done(null, users[profile.oid]);
}
Update the callback
route in ./routes/auth.js
to remove the req.flash
line with the access token. The callback
route should look like the following.
router.post('/callback',
function(req, res, next) {
passport.authenticate('azuread-openidconnect',
{
response: res,
failureRedirect: '/',
failureFlash: true
}
)(req,res,next);
},
function(req, res) {
res.redirect('/');
}
);
Restart the server and go through the sign-in process. You should end up back on the home page, but the UI should change to indicate that you are signed-in.
Click the user avatar in the top right corner to access the Sign Out link. Clicking Sign Out resets the session and returns you to the home page.
Refreshing tokens
At this point your application has an access token, which is sent in the Authorization
header of API calls. This is the token that allows the app to access the Microsoft Graph on the user's behalf.
However, this token is short-lived. The token expires an hour after it is issued. This is where the refresh token becomes useful. The refresh token allows the app to request a new access token without requiring the user to sign in again.
To manage this, create a new file in the root of the project named tokens.js
to hold token management functions. Add the following code.
module.exports = {
getAccessToken: async function(req) {
if (req.user) {
// Get the stored token
var storedToken = req.user.oauthToken;
if (storedToken) {
if (storedToken.expired()) {
// refresh token
var newToken = await storedToken.refresh();
// Update stored token
req.user.oauthToken = newToken;
return newToken.token.access_token;
}
// Token still valid, just return it
return storedToken.token.access_token;
}
}
}
};
This method first checks if the access token is expired or close to expiring. If it is, then it uses the refresh token to get new tokens, then updates the cache and returns the new access token. You'll use this method whenever you need to get the access token out of storage.
Get calendar data
In this exercise you will incorporate the Microsoft Graph into the application. For this application, you will use the microsoft-graph-client library to make calls to Microsoft Graph.
Get calendar events from Outlook
Start by adding a new method to the ./graph.js
file to get the events from the calendar. Add the following function inside the module.exports
in ./graph.js
.
getEvents: async function(accessToken) {
const client = getAuthenticatedClient(accessToken);
const events = await client
.api('/me/events')
.select('subject,organizer,start,end')
.orderby('createdDateTime DESC')
.get();
return events;
}
Consider what this code is doing.
- The URL that will be called is
/me/events
. - The
select
method limits the fields returned for each events to just those the view will actually use. - The
orderby
method sorts the results by the date and time they were created, with the most recent item being first.
Create a new file in the ./routes
directory named calendar.js
, and add the following code.
var express = require('express');
var router = express.Router();
var tokens = require('../tokens.js');
var graph = require('../graph.js');
/* GET /calendar */
router.get('/',
async function(req, res) {
if (!req.isAuthenticated()) {
// Redirect unauthenticated requests to home page
res.redirect('/')
} else {
let params = {
active: { calendar: true }
};
// Get the access token
var accessToken;
try {
accessToken = await tokens.getAccessToken(req);
} catch (err) {
res.json(err);
}
if (accessToken && accessToken.length > 0) {
try {
// Get the events
var events = await graph.getEvents(accessToken);
res.json(events.value);
} catch (err) {
res.json(err);
}
}
}
}
);
module.exports = router;
Update ./app.js
to use this new route. Add the following line before the var app = express();
line.
var calendarRouter = require('./routes/calendar');
Then add the following line after the app.use('/auth', authRouter);
line.
app.use('/calendar', calendarRouter);
Now you can test this. Sign in and click the Calendar link in the nav bar. If everything works, you should see a JSON dump of events on the user's calendar.
Display the results
Now you can add a view to display the results in a more user-friendly manner. First, add the following code in ./app.js
after the app.set('view engine', 'hbs');
line.
var hbs = require('hbs');
var moment = require('moment');
// Helper to format date/time sent by Graph
hbs.registerHelper('eventDateTime', function(dateTime){
return moment(dateTime).format('M/D/YY h:mm A');
});
This implements a Handlebars helper to format the ISO 8601 date returned by Microsoft Graph into something more human-friendly.
Create a new file in the ./views
directory named calendar.hbs
and add the following code.
<h1>Calendar</h1>
<table class="table">
<thead>
<tr>
<th scope="col">Organizer</th>
<th scope="col">Subject</th>
<th scope="col">Start</th>
<th scope="col">End</th>
</tr>
</thead>
<tbody>
{{#each events}}
<tr>
<td>{{this.organizer.emailAddress.name}}</td>
<td>{{this.subject}}</td>
<td>{{eventDateTime this.start.dateTime}}</td>
<td>{{eventDateTime this.end.dateTime}}</td>
</tr>
{{/each}}
</tbody>
</table>
That will loop through a collection of events and add a table row for each one. Now update the route in ./routes/calendar.js
to use this view. Replace the existing route with the following code.
router.get('/',
async function(req, res) {
if (!req.isAuthenticated()) {
// Redirect unauthenticated requests to home page
res.redirect('/')
} else {
let params = {
active: { calendar: true }
};
// Get the access token
var accessToken;
try {
accessToken = await tokens.getAccessToken(req);
} catch (err) {
req.flash('error_msg', {
message: 'Could not get access token. Try signing out and signing in again.',
debug: JSON.stringify(err)
});
}
if (accessToken && accessToken.length > 0) {
try {
// Get the events
var events = await graph.getEvents(accessToken);
params.events = events.value;
} catch (err) {
req.flash('error_msg', {
message: 'Could not fetch events',
debug: JSON.stringify(err)
});
}
}
res.render('calendar', params);
}
}
);
Save your changes, restart the server, and sign in to the app. Click on the Calendar link and the app should now render a table of events.
Congratulations!
You've completed the Node.js Microsoft Graph tutorial. Now that you have a working app that calls Microsoft Graph, you can experiment and add new features. Visit the Overview of Microsoft Graph to see all of the data you can access with Microsoft Graph.
Feedback
Please provide any feedback on this tutorial in the GitHub repository.